@kabmo-labs/kabmo 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (351) hide show
  1. package/README.md +7 -0
  2. package/dist/client/index.html +1 -1
  3. package/dist/client/index.js +403 -384
  4. package/dist/client/static/js/{index-0wvbca0b.js → index-bpptfkz2.js} +905 -875
  5. package/dist/client/static/js/index-k315p1e8.css +1 -0
  6. package/package.json +10 -1
  7. package/src/client/components/common/code-surface/documents.test.ts +120 -0
  8. package/src/client/components/common/code-surface/documents.ts +201 -0
  9. package/src/client/components/common/code-surface/extensions.test.ts +110 -0
  10. package/src/client/components/common/code-surface/extensions.ts +162 -0
  11. package/src/client/components/common/code-surface/handle.ts +102 -0
  12. package/src/client/components/common/code-surface/index.tsx +253 -314
  13. package/src/client/components/common/code-surface/keymap.test.ts +133 -0
  14. package/src/client/components/common/code-surface/keymap.ts +267 -0
  15. package/src/client/components/common/code-surface/marks.ts +74 -0
  16. package/src/client/components/common/code-surface/shiki.ts +234 -0
  17. package/src/client/components/common/code-surface/sync.test.ts +185 -0
  18. package/src/client/components/common/code-surface/sync.ts +58 -0
  19. package/src/client/components/common/code-surface/theme.test.ts +202 -0
  20. package/src/client/components/common/code-surface/theme.ts +111 -0
  21. package/src/client/components/common/editor-view-buttons/index.test.tsx +148 -0
  22. package/src/client/components/common/editor-view-buttons/index.tsx +109 -0
  23. package/src/client/components/common/header-controls/index.tsx +24 -1
  24. package/src/client/components/common/header-controls/shell-chrome.test.tsx +272 -0
  25. package/src/client/components/common/shell-update-button/Modal.tsx +163 -0
  26. package/src/client/components/common/shell-update-button/index.test.tsx +251 -0
  27. package/src/client/components/common/shell-update-button/index.tsx +125 -0
  28. package/src/client/components/common/toolbar-row/index.test.tsx +166 -0
  29. package/src/client/components/common/toolbar-row/index.tsx +189 -0
  30. package/src/client/components/common/workspace-options-menu/index.tsx +19 -1
  31. package/src/client/components/layout/desktop-layout/TopNavbar.tsx +5 -6
  32. package/src/client/components/layout/desktop-layout/WorkspacePanels.tsx +41 -78
  33. package/src/client/components/layout/desktop-layout/index.tsx +55 -69
  34. package/src/client/components/layout/desktop-layout/resizer/index.tsx +6 -3
  35. package/src/client/components/layout/session-sidebar/CategoryHeader.tsx +24 -0
  36. package/src/client/components/layout/session-sidebar/CategoryHeader.worktree.test.tsx +126 -0
  37. package/src/client/components/layout/session-sidebar/CategoryItem.tsx +4 -0
  38. package/src/client/components/layout/session-sidebar/Footer.tsx +8 -15
  39. package/src/client/components/layout/session-sidebar/SessionItem.tsx +12 -0
  40. package/src/client/components/layout/session-sidebar/SessionList.tsx +4 -0
  41. package/src/client/components/layout/session-sidebar/SessionsView.tsx +24 -0
  42. package/src/client/components/layout/session-sidebar/WorktreeBadge.test.ts +48 -0
  43. package/src/client/components/layout/session-sidebar/WorktreeBadge.tsx +55 -0
  44. package/src/client/components/mobile/LayoutWrapper.tsx +23 -16
  45. package/src/client/components/mobile/RightSidebar.tsx +21 -10
  46. package/src/client/components/mobile/SessionSidebar.tsx +8 -0
  47. package/src/client/components/mobile/header-titlebar.test.tsx +172 -0
  48. package/src/client/components/mobile/mobile-main-view/Header.tsx +10 -1
  49. package/src/client/components/mobile/mobile-right-sidebar/FullDiff.tsx +3 -0
  50. package/src/client/components/mobile/mobile-right-sidebar/FullEditor.tsx +63 -38
  51. package/src/client/components/mobile/mobile-right-sidebar/OutlineButton.test.tsx +137 -0
  52. package/src/client/components/mobile/mobile-right-sidebar/OutlineButton.tsx +14 -3
  53. package/src/client/components/mobile/mobile-session-sidebar/Footer.tsx +9 -21
  54. package/src/client/components/mobile/mobile-session-sidebar/Header.tsx +6 -2
  55. package/src/client/components/mobile/mobile-session-sidebar/Item.tsx +16 -0
  56. package/src/client/components/mobile/mobile-session-sidebar/List.tsx +4 -0
  57. package/src/client/components/mobile/mobile-session-sidebar/SessionRow.tsx +5 -0
  58. package/src/client/components/mobile/mobile-session-sidebar/WorktreeDialog.tsx +49 -0
  59. package/src/client/components/mobile/worktree-session.ts +51 -0
  60. package/src/client/components/settings/Modal.tsx +8 -0
  61. package/src/client/components/settings/categories/AppearanceSettings.tsx +3 -0
  62. package/src/client/components/settings/categories/ProjectSettings.tsx +22 -8
  63. package/src/client/components/settings/categories/appearance-settings/EditorThemeSection.test.tsx +167 -0
  64. package/src/client/components/settings/categories/appearance-settings/EditorThemeSection.tsx +184 -0
  65. package/src/client/components/settings/categories/project-settings/WorktreeSettings.tsx +146 -0
  66. package/src/client/components/settings/categories/project-settings/worktree-row.test.ts +83 -0
  67. package/src/client/components/settings/categories/project-settings/worktree-row.ts +47 -0
  68. package/src/client/components/workspace/chat-timeline/EmptyWorkspacePrompt.tsx +36 -55
  69. package/src/client/components/workspace/chat-timeline/PendingChatView.tsx +8 -0
  70. package/src/client/components/workspace/chat-timeline/SystemNotice.tsx +9 -4
  71. package/src/client/components/workspace/chat-timeline/TaskResultContent.tsx +9 -7
  72. package/src/client/components/workspace/chat-timeline/index.tsx +5 -0
  73. package/src/client/components/workspace/chat-timeline/target-picker/WorkspacePicker.test.tsx +132 -0
  74. package/src/client/components/workspace/chat-timeline/target-picker/WorkspacePicker.tsx +120 -0
  75. package/src/client/components/workspace/chat-timeline/target-picker/WorktreePicker.test.tsx +319 -0
  76. package/src/client/components/workspace/chat-timeline/target-picker/WorktreePicker.tsx +285 -0
  77. package/src/client/components/workspace/chat-timeline/tool-renderers/ask-panel/index.test.ts +2 -0
  78. package/src/client/components/workspace/chat-timeline/tool-renderers/hashline-patch/index.tsx +34 -29
  79. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/AstEdit.tsx +11 -9
  80. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Bash.tsx +10 -7
  81. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Edit.tsx +25 -8
  82. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Eval.test.tsx +146 -0
  83. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Eval.tsx +32 -17
  84. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/FileMatches.test.ts +114 -0
  85. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/FileMatches.tsx +111 -0
  86. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Read.tsx +7 -7
  87. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/SearchTool.tsx +9 -59
  88. package/src/client/components/workspace/chat-timeline/tool-renderers/shared/ArtifactBody.tsx +2 -7
  89. package/src/client/components/workspace/chat-timeline/tool-renderers/shared/DiffView.test.ts +27 -28
  90. package/src/client/components/workspace/chat-timeline/tool-renderers/shared/DiffView.tsx +39 -26
  91. package/src/client/components/workspace/chat-timeline/tool-renderers/shared/ExcerptCode.tsx +8 -8
  92. package/src/client/components/workspace/chat-timeline/tool-renderers/shared/HighlightedCode.tsx +35 -0
  93. package/src/client/components/workspace/chat-timeline/tool-renderers/shared/JsonCodeBlock.tsx +4 -4
  94. package/src/client/components/workspace/diff-panel/MergeDiffView.tsx +179 -0
  95. package/src/client/components/workspace/diff-panel/ThemedMergeDiffView.tsx +24 -0
  96. package/src/client/components/workspace/diff-panel/Toolbar.tsx +224 -186
  97. package/src/client/components/workspace/diff-panel/ToolbarSegments.tsx +98 -0
  98. package/src/client/components/workspace/diff-panel/index.tsx +76 -145
  99. package/src/client/components/workspace/diff-panel/merge.ts +315 -0
  100. package/src/client/components/workspace/editor/Body.tsx +4 -7
  101. package/src/client/components/workspace/editor/CommandPalette.tsx +2 -2
  102. package/src/client/components/workspace/editor/FileDocument.tsx +58 -63
  103. package/src/client/components/workspace/editor/FilePane.tsx +4 -7
  104. package/src/client/components/workspace/editor/FindWidget.tsx +7 -2
  105. package/src/client/components/workspace/editor/Toolbar.tsx +220 -123
  106. package/src/client/components/workspace/editor/index.tsx +7 -9
  107. package/src/client/components/workspace/file-explorer/ContextMenu.tsx +2 -1
  108. package/src/client/components/workspace/file-explorer/Modals.tsx +40 -12
  109. package/src/client/components/workspace/file-explorer/OutlineSection.test.tsx +18 -0
  110. package/src/client/components/workspace/file-explorer/OutlineSection.tsx +10 -3
  111. package/src/client/components/workspace/file-explorer/RootMenu.tsx +14 -5
  112. package/src/client/components/workspace/file-explorer/Toolbar.tsx +74 -0
  113. package/src/client/components/workspace/file-explorer/TreeItem.tsx +38 -3
  114. package/src/client/components/workspace/file-explorer/filter.ts +47 -0
  115. package/src/client/components/workspace/file-explorer/index.tsx +77 -64
  116. package/src/client/components/workspace/file-explorer/local-edit.test.ts +135 -0
  117. package/src/client/components/workspace/search-panel/Body.tsx +1 -1
  118. package/src/client/components/workspace/worktree-modal/index.test.tsx +118 -0
  119. package/src/client/components/workspace/worktree-modal/index.tsx +261 -0
  120. package/src/client/hooks/chat/omp/reconnect.test.ts +338 -0
  121. package/src/client/hooks/chat/omp/reconnect.ts +187 -0
  122. package/src/client/hooks/chat/omp/run-liveness.test.ts +71 -0
  123. package/src/client/hooks/chat/omp/run-liveness.ts +65 -0
  124. package/src/client/hooks/chat/omp/session-list.activity.test.ts +114 -0
  125. package/src/client/hooks/chat/omp/session-list.test-util.ts +164 -0
  126. package/src/client/hooks/chat/omp/session-list.test.ts +30 -150
  127. package/src/client/hooks/chat/omp/session-list.tsx +69 -9
  128. package/src/client/hooks/chat/timeline/generating.ts +19 -2
  129. package/src/client/hooks/chat/timeline/index.ts +27 -28
  130. package/src/client/hooks/chat/timeline/lost-run.test.ts +329 -0
  131. package/src/client/hooks/chat/timeline/lost-run.ts +177 -0
  132. package/src/client/hooks/chat/timeline/send-execute.test.ts +2 -0
  133. package/src/client/hooks/chat/timeline/send-steer.test.ts +2 -0
  134. package/src/client/hooks/chat/timeline/send.test.ts +2 -0
  135. package/src/client/hooks/chat/timeline/send.ts +11 -1
  136. package/src/client/hooks/chat/timeline/session-load.test-util.ts +205 -0
  137. package/src/client/hooks/chat/timeline/session-load.test.ts +61 -162
  138. package/src/client/hooks/chat/timeline/session-load.ts +69 -37
  139. package/src/client/hooks/chat/timeline/session-switch.test.ts +65 -0
  140. package/src/client/hooks/chat/timeline/session-switch.ts +63 -0
  141. package/src/client/hooks/chat/timeline/spawn-target.ts +52 -0
  142. package/src/client/hooks/chat/timeline/tool-open.test.ts +74 -0
  143. package/src/client/hooks/chat/timeline/tool-open.ts +52 -8
  144. package/src/client/hooks/code/deferred-highlight.test.ts +182 -0
  145. package/src/client/hooks/code/deferred-highlight.ts +129 -0
  146. package/src/client/hooks/editor/find-state.ts +1 -1
  147. package/src/client/hooks/editor/theme.test.ts +206 -0
  148. package/src/client/hooks/editor/theme.ts +82 -0
  149. package/src/client/hooks/editor/use-editor-drop.test.ts +101 -0
  150. package/src/client/hooks/editor/use-editor-drop.ts +67 -14
  151. package/src/client/hooks/editor/use-editor-find.ts +7 -7
  152. package/src/client/hooks/editor/use-outline-view.test.ts +182 -0
  153. package/src/client/hooks/editor/use-outline-view.ts +94 -7
  154. package/src/client/hooks/editor/use-scroll-restore.test.ts +55 -2
  155. package/src/client/hooks/editor/use-scroll-restore.ts +124 -4
  156. package/src/client/hooks/editor/use-search-reveal.ts +1 -1
  157. package/src/client/hooks/ui/long-press.test.ts +174 -0
  158. package/src/client/hooks/ui/long-press.ts +112 -0
  159. package/src/client/hooks/ui/settings-modal.ts +56 -0
  160. package/src/client/hooks/ui/updates/shell-download.test.ts +75 -0
  161. package/src/client/hooks/ui/updates/shell-download.ts +153 -28
  162. package/src/client/hooks/ui/updates/shell-offer.test.ts +213 -0
  163. package/src/client/hooks/ui/updates/shell-update.test.ts +140 -0
  164. package/src/client/hooks/ui/updates/shell-update.ts +119 -0
  165. package/src/client/hooks/workspace/diff-file.ts +29 -10
  166. package/src/client/hooks/workspace/diff-git-actions.ts +147 -0
  167. package/src/client/hooks/workspace/editor-view.ts +61 -0
  168. package/src/client/hooks/workspace/file-create.ts +41 -11
  169. package/src/client/hooks/workspace/file-listing.ts +56 -1
  170. package/src/client/hooks/workspace/file-tabs.test.ts +62 -5
  171. package/src/client/hooks/workspace/file-tabs.ts +24 -11
  172. package/src/client/hooks/workspace/file-tree-actions.ts +37 -5
  173. package/src/client/hooks/workspace/navbar-session-actions.ts +72 -0
  174. package/src/client/hooks/workspace/worktree/index.ts +165 -0
  175. package/src/client/lib/desktop/bridge.ts +45 -0
  176. package/src/client/lib/search-reveal.ts +1 -1
  177. package/src/client/main.tsx +5 -1
  178. package/src/client/tailwind.css +56 -8
  179. package/src/server/index.ts +7 -2
  180. package/src/server/lib/auth/guard.test.ts +28 -0
  181. package/src/server/lib/auth/guard.ts +10 -0
  182. package/src/server/lib/bundler/titlebar-css.test.ts +42 -1
  183. package/src/server/lib/db/schema.ts +4 -0
  184. package/src/server/lib/db/worktree-registry.server.ts +91 -0
  185. package/src/server/lib/fs/canonical.ts +52 -0
  186. package/src/server/lib/fs/relative-path.ts +48 -0
  187. package/src/server/lib/fs/root-worktree.test.ts +116 -0
  188. package/src/server/lib/fs/root.ts +39 -10
  189. package/src/server/lib/fs/worktree/carry.ts +134 -0
  190. package/src/server/lib/fs/worktree/create.test.ts +249 -0
  191. package/src/server/lib/fs/worktree/create.ts +262 -0
  192. package/src/server/lib/fs/worktree/errors.ts +43 -0
  193. package/src/server/lib/fs/worktree/list.test.ts +207 -0
  194. package/src/server/lib/fs/worktree/list.ts +142 -0
  195. package/src/server/lib/fs/worktree/location.test.ts +206 -0
  196. package/src/server/lib/fs/worktree/location.ts +114 -0
  197. package/src/server/lib/fs/worktree/project-id.ts +53 -0
  198. package/src/server/lib/fs/worktree/remove.test.ts +242 -0
  199. package/src/server/lib/fs/worktree/remove.ts +152 -0
  200. package/src/server/lib/fs/worktree/settings.server.ts +56 -0
  201. package/src/server/lib/fs/worktree/topology.ts +142 -0
  202. package/src/server/lib/lifecycle/claim-port.test.ts +156 -0
  203. package/src/server/lib/lifecycle/port-guard.test.ts +20 -164
  204. package/src/server/lib/lifecycle/port-guard.ts +39 -7
  205. package/src/server/lib/lifecycle/probe-health.test.ts +166 -0
  206. package/src/server/lib/lifecycle/probe.test.ts +77 -164
  207. package/src/server/lib/lifecycle/probe.ts +121 -13
  208. package/src/server/lib/omp/rpc/frame-fold-signals.test.ts +112 -0
  209. package/src/server/lib/omp/rpc/frame-fold.ts +10 -5
  210. package/src/server/lib/omp/session/sidebar-activity.server.test.ts +162 -0
  211. package/src/server/lib/omp/session/sidebar-activity.server.ts +70 -0
  212. package/src/server/lib/omp/session/sidebar-data.canonical.test.ts +155 -0
  213. package/src/server/lib/omp/session/sidebar-data.server.ts +28 -1
  214. package/src/server/lib/omp/session/telemetry/compute.ts +13 -1
  215. package/src/server/lib/omp/session/telemetry.test.ts +33 -0
  216. package/src/server/lib/panels/local-edits.server.ts +33 -0
  217. package/src/server/lib/panels/sync.fixture.test-util.ts +92 -0
  218. package/src/server/lib/panels/sync.local-edits.test.ts +150 -0
  219. package/src/server/lib/panels/sync.server.test.ts +125 -147
  220. package/src/server/lib/panels/sync.server.ts +153 -48
  221. package/src/server/lib/realtime/signals.server.ts +10 -0
  222. package/src/server/lib/realtime/topics/catalog.server.ts +4 -1
  223. package/src/server/lib/realtime/topics.server.ts +16 -0
  224. package/src/server/lib/updates/shell-assets.test.ts +114 -0
  225. package/src/server/lib/updates/shell-assets.ts +200 -0
  226. package/src/server/lib/updates/shell-cache.ts +105 -0
  227. package/src/server/lib/updates/shell-pages.test.ts +187 -0
  228. package/src/server/lib/updates/shell-pages.ts +157 -0
  229. package/src/server/lib/updates/shell-release-desktop.test.ts +166 -0
  230. package/src/server/lib/updates/shell-release.test.ts +59 -71
  231. package/src/server/lib/updates/shell-release.ts +178 -198
  232. package/src/server/routes/fs/action-create.test.ts +18 -13
  233. package/src/server/routes/fs/action-nested.test.ts +184 -0
  234. package/src/server/routes/fs/action.ts +47 -16
  235. package/src/server/routes/fs/git-merge.test.ts +11 -1
  236. package/src/server/routes/fs/read.test.ts +24 -7
  237. package/src/server/routes/fs/scope.test.ts +15 -10
  238. package/src/server/routes/fs/scope.ts +3 -14
  239. package/src/server/routes/index.ts +2 -0
  240. package/src/server/routes/updates/android.ts +6 -1
  241. package/src/server/routes/updates/desktop.ts +59 -0
  242. package/src/server/routes/updates/index.ts +2 -0
  243. package/src/server/routes/updates/ios.ts +5 -1
  244. package/src/server/routes/worktrees/create.ts +138 -0
  245. package/src/server/routes/worktrees/index.ts +25 -0
  246. package/src/server/routes/worktrees/list.ts +134 -0
  247. package/src/server/routes/worktrees/remove.ts +102 -0
  248. package/src/server/routes/worktrees/routes.test.ts +326 -0
  249. package/src/server/routes/worktrees/shared.ts +90 -0
  250. package/src/shared/lib/chat/timeline/omp-callbacks.test.ts +36 -0
  251. package/src/shared/lib/chat/timeline/omp-callbacks.ts +18 -0
  252. package/src/shared/lib/chat/timeline/run-signals.test.ts +2 -0
  253. package/src/shared/lib/code/editor/comment-syntax.test.ts +37 -0
  254. package/src/shared/lib/code/editor/comment-syntax.ts +55 -0
  255. package/src/shared/lib/code/editor/find-replace.test.ts +134 -0
  256. package/src/shared/lib/code/editor/{find.ts → find-replace.ts} +23 -107
  257. package/src/shared/lib/code/editor/{find.test.ts → find-scan.test.ts} +19 -73
  258. package/src/shared/lib/code/editor/find-scan.ts +177 -0
  259. package/src/shared/lib/code/editor/keymap.ts +3 -3
  260. package/src/shared/lib/code/editor/lines.test.ts +7 -55
  261. package/src/shared/lib/code/editor/lines.ts +8 -48
  262. package/src/shared/lib/code/editor/marks.test.ts +4 -5
  263. package/src/shared/lib/code/editor/marks.ts +13 -25
  264. package/src/shared/lib/code/editor/theme-loader.ts +97 -0
  265. package/src/shared/lib/code/editor/themes.generated.ts +156 -0
  266. package/src/shared/lib/code/editor/themes.test.ts +165 -0
  267. package/src/shared/lib/code/editor/themes.ts +165 -0
  268. package/src/shared/lib/code/highlight-memo.test.ts +103 -0
  269. package/src/shared/lib/code/highlight-memo.ts +149 -0
  270. package/src/shared/lib/code/highlighter-warmup.ts +112 -0
  271. package/src/shared/lib/code/highlighter.ts +3 -1
  272. package/src/shared/lib/code/idle.ts +37 -0
  273. package/src/shared/lib/code/outline/rows.test.ts +96 -0
  274. package/src/shared/lib/code/outline/rows.ts +94 -0
  275. package/src/shared/lib/code/syntax-highlight.test.ts +1 -79
  276. package/src/shared/lib/code/syntax-highlight.ts +2 -3
  277. package/src/shared/lib/fs/file-kind-classify.test.ts +25 -11
  278. package/src/shared/lib/fs/file-kind.ts +47 -13
  279. package/src/shared/lib/fs/file-tree.test.ts +183 -0
  280. package/src/shared/lib/fs/file-tree.ts +159 -0
  281. package/src/shared/lib/fs/file-url.test.ts +52 -0
  282. package/src/shared/lib/fs/file-url.ts +37 -0
  283. package/src/shared/lib/fs/patch-sides.test.ts +114 -0
  284. package/src/shared/lib/fs/patch-sides.ts +139 -0
  285. package/src/shared/lib/fs/paths.test.ts +24 -1
  286. package/src/shared/lib/fs/paths.ts +17 -0
  287. package/src/shared/lib/fs/worktree/settings-keys.ts +18 -0
  288. package/src/shared/lib/omp/session/prewarm.ts +7 -0
  289. package/src/shared/lib/realtime/protocol.ts +16 -0
  290. package/src/shared/lib/ui/toolbar-overflow.test.ts +83 -0
  291. package/src/shared/lib/ui/toolbar-overflow.ts +86 -0
  292. package/src/shared/lib/updates/shell-arch.ts +54 -0
  293. package/src/shared/lib/updates/shell-ua.ts +24 -4
  294. package/src/shared/lib/workspace/active-project.ts +23 -0
  295. package/src/shared/lib/workspace/editor-view-mode.test.ts +70 -0
  296. package/src/shared/lib/workspace/editor-view-mode.ts +48 -0
  297. package/src/shared/lib/workspace/panel-widths.test.ts +30 -0
  298. package/src/shared/lib/workspace/panel-widths.ts +25 -5
  299. package/src/shared/lib/workspace/session-state/cache.ts +159 -0
  300. package/src/shared/lib/workspace/session-state/harness.test-util.ts +55 -0
  301. package/src/shared/lib/workspace/session-state/merge.ts +100 -0
  302. package/src/shared/lib/workspace/session-state/store-load.test.ts +206 -0
  303. package/src/shared/lib/workspace/session-state/store.test.ts +17 -139
  304. package/src/shared/lib/workspace/session-state/store.ts +92 -130
  305. package/src/shared/lib/workspace/workspace-modules.test.ts +49 -1
  306. package/src/shared/lib/workspace/workspace-panel-widths.ts +124 -0
  307. package/src/shared/lib/workspace/worktree/branch-name.test.ts +95 -0
  308. package/src/shared/lib/workspace/worktree/branch-name.ts +115 -0
  309. package/src/shared/lib/workspace/worktree/types.ts +104 -0
  310. package/src/shared/types/fs.ts +10 -0
  311. package/src/shared/types/realtime.ts +29 -0
  312. package/src/shared/types/settings/state.ts +10 -0
  313. package/src/shared/types/workspace.ts +14 -0
  314. package/src/test-support/file-explorer-disk.test-util.ts +76 -0
  315. package/src/test-support/file-explorer.test-util.ts +71 -40
  316. package/src/test-support/loopback.ts +183 -0
  317. package/dist/client/static/js/index-nzyjf92m.css +0 -1
  318. package/src/client/components/common/code-editor/handler.test.ts +0 -274
  319. package/src/client/components/common/code-editor/handler.ts +0 -175
  320. package/src/client/components/common/code-editor/index.tsx +0 -314
  321. package/src/client/components/common/code-surface/measure.ts +0 -148
  322. package/src/client/components/workspace/diff-panel/SplitView.tsx +0 -209
  323. package/src/client/components/workspace/diff-panel/UnifiedView.tsx +0 -150
  324. package/src/client/hooks/editor/scroll-container.test.ts +0 -102
  325. package/src/client/hooks/editor/scroll-container.ts +0 -26
  326. package/src/client/hooks/editor/use-code-window.ts +0 -100
  327. package/src/client/hooks/editor/use-editor-history.test.ts +0 -216
  328. package/src/client/hooks/editor/use-editor-history.ts +0 -134
  329. package/src/client/hooks/editor/use-occurrences.test.ts +0 -236
  330. package/src/client/hooks/editor/use-occurrences.ts +0 -98
  331. package/src/shared/lib/code/editor/commands.test.ts +0 -155
  332. package/src/shared/lib/code/editor/commands.ts +0 -260
  333. package/src/shared/lib/code/editor/edits.test.ts +0 -100
  334. package/src/shared/lib/code/editor/edits.ts +0 -122
  335. package/src/shared/lib/code/editor/history.test.ts +0 -118
  336. package/src/shared/lib/code/editor/history.ts +0 -101
  337. package/src/shared/lib/code/editor/layers.test.ts +0 -77
  338. package/src/shared/lib/code/editor/layers.ts +0 -67
  339. package/src/shared/lib/code/editor/line-edits.test.ts +0 -131
  340. package/src/shared/lib/code/editor/line-edits.ts +0 -159
  341. package/src/shared/lib/code/editor/occurrences.test.ts +0 -117
  342. package/src/shared/lib/code/editor/occurrences.ts +0 -111
  343. package/src/shared/lib/code/editor/reset-css.test.ts +0 -35
  344. package/src/shared/lib/code/editor/reset-css.ts +0 -32
  345. package/src/shared/lib/code/editor/text-commands.test.ts +0 -178
  346. package/src/shared/lib/code/editor/text-commands.ts +0 -154
  347. package/src/shared/lib/code/lazy-window.test.ts +0 -161
  348. package/src/shared/lib/code/lazy-window.ts +0 -156
  349. package/src/shared/lib/code/windowed-highlight.ts +0 -114
  350. package/src/shared/lib/fs/diff-parser.test.ts +0 -116
  351. package/src/shared/lib/fs/diff-parser.ts +0 -213
@@ -0,0 +1,125 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * The native shell's update/install control, shared by both sidebar layouts.
8
+ *
9
+ * ## Two controls, because there are two different jobs
10
+ *
11
+ * - **A link** when the console cannot name a file: a desktop shell that did not
12
+ * report its architecture, or any offer whose artifact is not installable from
13
+ * inside. Following it is the browser's (or the WebView's) job.
14
+ * - **A button** when the shell CAN install it: the click opens a CONFIRMATION, and
15
+ * the confirmation asks the shell to download, verify and replace itself. That is
16
+ * the desktop self-update, and it is the whole reason this component exists.
17
+ *
18
+ * The decision is `canSelfUpdate`, never a check on the platform at the call site:
19
+ * Android installs through `PackageInstaller` (its own path, unchanged), iOS cannot
20
+ * install at all, and a browser has no shell to ask.
21
+ *
22
+ * ## The control is ONLY a control
23
+ *
24
+ * Every word a run has to say — the confirmation, the progress, the shell's own
25
+ * refusal — is in `Modal.tsx`. The footer is a 24px row of glyphs, and a status
26
+ * sentence pushed into it moved the neighbouring controls and read as a status
27
+ * line for the SIDEBAR rather than for the update.
28
+ *
29
+ * ## The state lives here, not in either footer
30
+ *
31
+ * Both layouts draw this control and each unmounts at a different moment (a phone
32
+ * closes its drawer, a desktop swaps sessions). One hook, one vocabulary of
33
+ * phases, so the two cannot report the same run differently.
34
+ */
35
+
36
+ import { useState } from 'preact/hooks';
37
+ import { createPortal } from 'preact/compat';
38
+ import { Download, Loader2 } from 'lucide-preact';
39
+ import {
40
+ shellDownloadLabel,
41
+ shellDownloadShortLabel,
42
+ type ShellDownload,
43
+ } from '@/client/hooks/ui/updates/shell-download';
44
+ import { canSelfUpdate, useShellUpdate } from '@/client/hooks/ui/updates/shell-update';
45
+ import { ShellUpdateModal } from '@/client/components/common/shell-update-button/Modal';
46
+
47
+ interface ShellUpdateButtonProps {
48
+ download: ShellDownload;
49
+ /** `icon` for the desktop footer's glyph row, `label` for the phone's. */
50
+ variant: 'icon' | 'label';
51
+ }
52
+
53
+ const ICON_CLASS = 'p-1 rounded hover:text-ink hover:bg-ink/5 cursor-pointer transition-colors';
54
+
55
+ export function ShellUpdateButton({ download, variant }: ShellUpdateButtonProps) {
56
+ const { phase, message, apply, reset, working } = useShellUpdate();
57
+ // The dialog is open from the CLICK until it is dismissed, and the run's phases
58
+ // are drawn inside it. A closed dialog whose run is still `working` is not a
59
+ // state this component can be in: `confirm` is what opens it.
60
+ const [open, setOpen] = useState(false);
61
+ const label = shellDownloadLabel(download);
62
+
63
+ if (!canSelfUpdate(download)) {
64
+ // A link, and `target="_blank"` hands it to the system browser so the console
65
+ // is not replaced by a file download.
66
+ return (
67
+ <a
68
+ href={download.url}
69
+ target="_blank"
70
+ rel="noopener noreferrer"
71
+ title={label}
72
+ aria-label={label}
73
+ className={ICON_CLASS}
74
+ >
75
+ {variant === 'icon' ? <Download size={16} /> : <span>{shellDownloadShortLabel(download)}</span>}
76
+ </a>
77
+ );
78
+ }
79
+
80
+ return (
81
+ <>
82
+ <button
83
+ type="button"
84
+ // A dismissed run is not a run: reopening starts from the confirmation, not
85
+ // from the previous attempt's refusal.
86
+ onClick={() => {
87
+ reset();
88
+ setOpen(true);
89
+ }}
90
+ disabled={working}
91
+ title={label}
92
+ aria-label={label}
93
+ className={`${ICON_CLASS} disabled:cursor-not-allowed disabled:opacity-60 inline-flex items-center gap-1`}
94
+ >
95
+ {working ? (
96
+ <Loader2 size={variant === 'icon' ? 16 : 13} className="animate-spin motion-reduce:animate-none" />
97
+ ) : (
98
+ <Download size={variant === 'icon' ? 16 : 13} />
99
+ )}
100
+ {variant === 'label' && <span>{shellDownloadShortLabel(download)}</span>}
101
+ </button>
102
+
103
+ {open &&
104
+ // A PORTAL, not a child of this row. The footer is a 24px flex row of
105
+ // glyphs, so a dialog rendered inside it is nested in a control strip: its
106
+ // text joins the row's own text content (measured in the browser — the row
107
+ // read as the dialog's body), and any `overflow`/`transform` added to that
108
+ // row later would clip a `fixed` overlay. The portal is what makes "the
109
+ // footer says nothing about a run" true rather than merely visually true.
110
+ createPortal(
111
+ <ShellUpdateModal
112
+ download={download}
113
+ phase={phase === 'idle' ? 'confirm' : phase}
114
+ message={message}
115
+ onConfirm={() => void apply(download)}
116
+ // Closing mid-run hides the dialog but does not cancel the shell: it is
117
+ // already replacing itself, and a Cancel that could not stop it would be
118
+ // a button that lies. The control stays disabled until the run answers.
119
+ onClose={() => setOpen(false)}
120
+ />,
121
+ document.body,
122
+ )}
123
+ </>
124
+ );
125
+ }
@@ -0,0 +1,166 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * The toolbar stays ONE row, and what does not fit is one click away.
8
+ *
9
+ * The failure this pins is a row that becomes two: the editor panel is 320px
10
+ * wide on a 1440px screen, and the width-mode buttons pushed ten controls past
11
+ * it. Wrapping moved the document down by the toolbar's own height and read as
12
+ * a second toolbar; a horizontal scroll clipped the tail invisibly. Neither is
13
+ * visible to a state-only assertion, so the cases below are about what is
14
+ * RENDERED and what the `⋯` menu offers.
15
+ *
16
+ * happy-dom lays nothing out, so `getBoundingClientRect` and `clientWidth` are
17
+ * stubbed at the PROTOTYPE level — the rule reads exactly those two values, and
18
+ * stubbing them before the first render is what lets the component's own layout
19
+ * effect measure a real row instead of a row of zeros.
20
+ */
21
+
22
+ import { afterAll, afterEach, beforeAll, describe, expect, test } from 'bun:test';
23
+ import { Window } from 'happy-dom';
24
+ import { h, render } from 'preact';
25
+ import { act } from 'preact/test-utils';
26
+ import { Save, Search, WrapText } from 'lucide-preact';
27
+ import { ToolbarRow, type ToolbarCluster } from '@/client/components/common/toolbar-row';
28
+ import { installDomGlobals, restoreDomGlobals } from '@/test-support/pristine-globals';
29
+
30
+ let container: HTMLElement;
31
+ /** What the stubbed layout reports for the row, and for each cluster in order. */
32
+ let rowWidth = 0;
33
+ let clusterWidths: number[] = [];
34
+
35
+ /** Captured inside `beforeAll`: the DOM globals do not exist at module load. */
36
+ let originalRect: typeof Element.prototype.getBoundingClientRect;
37
+ let originalClientWidth: PropertyDescriptor | undefined;
38
+
39
+ /** The row is the flex container the component renders; clusters carry a marker. */
40
+ function isRow(el: HTMLElement): boolean {
41
+ return el.className.includes('flex-nowrap');
42
+ }
43
+
44
+ function rectOf(width: number): DOMRect {
45
+ return { width, height: 0, top: 0, left: 0, right: width, bottom: 0, x: 0, y: 0, toJSON: () => ({}) } as DOMRect;
46
+ }
47
+
48
+ beforeAll(() => {
49
+ installDomGlobals(new Window({ url: 'http://localhost' }));
50
+ originalRect = Element.prototype.getBoundingClientRect;
51
+ originalClientWidth = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'clientWidth');
52
+ Element.prototype.getBoundingClientRect = function (this: Element) {
53
+ const el = this as HTMLElement;
54
+ if (el.hasAttribute?.('data-toolbar-cluster')) {
55
+ const siblings = [...(el.parentElement?.querySelectorAll('[data-toolbar-cluster]') ?? [])];
56
+ return rectOf(clusterWidths[siblings.indexOf(el)] ?? 0);
57
+ }
58
+ if (isRow(el)) return rectOf(rowWidth);
59
+ return originalRect.call(this);
60
+ };
61
+ Object.defineProperty(HTMLElement.prototype, 'clientWidth', {
62
+ configurable: true,
63
+ get(this: HTMLElement) {
64
+ if (isRow(this)) return rowWidth;
65
+ return originalClientWidth?.get?.call(this) ?? 0;
66
+ },
67
+ });
68
+ });
69
+
70
+ afterEach(() => {
71
+ if (container) {
72
+ render(null, container);
73
+ container.remove();
74
+ }
75
+ rowWidth = 0;
76
+ clusterWidths = [];
77
+ });
78
+
79
+ afterAll(() => {
80
+ Element.prototype.getBoundingClientRect = originalRect;
81
+ if (originalClientWidth) Object.defineProperty(HTMLElement.prototype, 'clientWidth', originalClientWidth);
82
+ restoreDomGlobals();
83
+ });
84
+
85
+ /** Three clusters, the first pinned — the editor toolbar in miniature. */
86
+ function clusters(onSelect: string[] = []): ToolbarCluster[] {
87
+ const make = (id: string, label: string, icon: typeof Save, pinned?: boolean): ToolbarCluster => ({
88
+ id,
89
+ pinned,
90
+ actions: [{ label, icon, onSelect: () => onSelect.push(label) }],
91
+ children: h('button', { type: 'button', 'aria-label': label }, label),
92
+ });
93
+ return [
94
+ make('save', 'Save file', Save, true),
95
+ make('find', 'Find and replace', Search),
96
+ make('wrap', 'Word wrap on', WrapText),
97
+ ];
98
+ }
99
+
100
+ async function mount(width: number, widths: number[], onSelect: string[] = []): Promise<void> {
101
+ rowWidth = width;
102
+ clusterWidths = widths;
103
+ container = document.createElement('div');
104
+ document.body.appendChild(container);
105
+ await act(async () => {
106
+ render(h(ToolbarRow, { clusters: clusters(onSelect) }), container);
107
+ });
108
+ // The layout effect commits its measurement in a second pass, the way the
109
+ // browser gives one before paint.
110
+ await act(async () => {});
111
+ }
112
+
113
+ function rowButtons(): HTMLButtonElement[] {
114
+ const host = container.firstElementChild as HTMLElement;
115
+ return [...host.querySelectorAll<HTMLButtonElement>(':scope > div > button')];
116
+ }
117
+
118
+ async function openMenu(): Promise<void> {
119
+ const trigger = rowButtons().find((button) => button.getAttribute('aria-label') === 'More actions');
120
+ if (!trigger) throw new Error('no overflow trigger rendered');
121
+ await act(async () => {
122
+ trigger.dispatchEvent(new window.MouseEvent('click', { bubbles: true }) as never);
123
+ });
124
+ }
125
+
126
+ describe('ToolbarRow overflow', () => {
127
+ test('a wide row draws every cluster and no trigger', async () => {
128
+ await mount(400, [60, 60, 60]);
129
+ expect(rowButtons().map((button) => button.getAttribute('aria-label'))).toEqual([
130
+ 'Save file',
131
+ 'Find and replace',
132
+ 'Word wrap on',
133
+ ]);
134
+ });
135
+
136
+ test('a narrow row keeps the pinned cluster and moves the rest into `⋯`', async () => {
137
+ await mount(120, [60, 60, 60]);
138
+ const labels = rowButtons().map((button) => button.getAttribute('aria-label'));
139
+ expect(labels).toContain('Save file');
140
+ expect(labels).toContain('More actions');
141
+ expect(labels).not.toContain('Word wrap on');
142
+ });
143
+
144
+ test('the menu offers the hidden controls, named in words', async () => {
145
+ await mount(120, [60, 60, 60]);
146
+ await openMenu();
147
+ const items = [...container.querySelectorAll('[role="menuitem"]')].map((item) => item.textContent?.trim());
148
+ expect(items).toContain('Word wrap on');
149
+ // The pinned control is on the row, so it is NOT in the menu.
150
+ expect(items).not.toContain('Save file');
151
+ });
152
+
153
+ test('a menu row runs the same action the button would', async () => {
154
+ const ran: string[] = [];
155
+ await mount(120, [60, 60, 60], ran);
156
+ await openMenu();
157
+ const item = [...container.querySelectorAll<HTMLButtonElement>('[role="menuitem"]')].find(
158
+ (row) => row.textContent?.trim() === 'Word wrap on',
159
+ );
160
+ if (!item) throw new Error('the wrap row is missing from the menu');
161
+ await act(async () => {
162
+ item.dispatchEvent(new window.MouseEvent('click', { bubbles: true }) as never);
163
+ });
164
+ expect(ran).toEqual(['Word wrap on']);
165
+ });
166
+ });
@@ -0,0 +1,189 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * One toolbar row that never becomes two.
8
+ *
9
+ * The editor and diff toolbars carry ten-plus controls and the editor panel is
10
+ * 320px wide on a 1440px screen. WRAPPING was the previous answer and it is the
11
+ * wrong one: a second row moves the document down by the toolbar's own height
12
+ * and reads as a second toolbar. A horizontal scroll is worse — a clipped
13
+ * control is invisible. So the clusters that do not fit move into a `⋯` menu and
14
+ * every control stays one click away.
15
+ *
16
+ * Widths are MEASURED, never tabulated: the row lays every cluster out once and
17
+ * reads each one's rendered width, then hides what does not fit. A table of
18
+ * guessed pixel widths drifts the moment a label, an icon or a font changes, and
19
+ * a wrong guess here is a control that is either clipped or needlessly buried.
20
+ */
21
+
22
+ import { useEffect, useLayoutEffect, useRef, useState } from 'preact/hooks';
23
+ import type { ComponentChildren } from 'preact';
24
+ import { Ellipsis, type LucideIcon } from 'lucide-preact';
25
+ import { useOnClickOutside } from '@/client/hooks/ui/on-click-outside';
26
+ import { toolbarHasOverflow, toolbarVisibleClusters } from '@/shared/lib/ui/toolbar-overflow';
27
+
28
+ /** One row the `⋯` menu offers when its cluster is not on the row. */
29
+ export interface ToolbarOverflowAction {
30
+ label: string;
31
+ icon: LucideIcon;
32
+ onSelect: () => void;
33
+ /** Rendered as a pressed row (word wrap on, a mode already active). */
34
+ active?: boolean;
35
+ disabled?: boolean;
36
+ }
37
+
38
+ /**
39
+ * A logical group of controls that hides as a unit.
40
+ *
41
+ * A group rather than a single button because the clusters are already grouped
42
+ * in the toolbars (zoom is two buttons, copy/download is a pair, the width modes
43
+ * are a pair) and splitting them would let half of a pair stay on the row.
44
+ */
45
+ export interface ToolbarCluster {
46
+ id: string;
47
+ /**
48
+ * Never leave the row, whatever its width.
49
+ *
50
+ * The visual order is not the priority order: Save is drawn first because that
51
+ * is where a reader looks for it, while the width modes sit last and are the
52
+ * reason the panel exists. Pinning says which ones a menu row must not be the
53
+ * only way to reach.
54
+ */
55
+ pinned?: boolean;
56
+ /** What the `⋯` menu offers when this cluster is hidden. */
57
+ actions: ToolbarOverflowAction[];
58
+ children: ComponentChildren;
59
+ }
60
+
61
+ interface ToolbarRowProps {
62
+ clusters: ToolbarCluster[];
63
+ className?: string;
64
+ }
65
+
66
+ export function ToolbarRow({ clusters, className = '' }: ToolbarRowProps) {
67
+ const rowRef = useRef<HTMLDivElement>(null);
68
+ /** Measured widths, tagged with the cluster set they describe. */
69
+ const [metrics, setMetrics] = useState<{ signature: string; widths: number[] } | null>(null);
70
+ const [available, setAvailable] = useState(0);
71
+
72
+ // The set changes when a conditional cluster appears or disappears (a
73
+ // previewable file opened, a byte viewer took over), and stale widths would
74
+ // then hide the wrong cluster. Rendering every cluster until they are measured
75
+ // is what gives the ruler something to measure — and the layout effect runs
76
+ // BEFORE paint, so the unresolved frame is never seen.
77
+ const signature = clusters.map((cluster) => `${cluster.id}:${cluster.pinned ? 1 : 0}`).join('|');
78
+ const ready = metrics?.signature === signature;
79
+ const widths = ready ? metrics.widths : [];
80
+ const pinned = clusters.map((cluster) => cluster.pinned === true);
81
+ const visible = widths.length > 0
82
+ ? toolbarVisibleClusters({ widths, pinned, available })
83
+ : clusters.map(() => true);
84
+
85
+ useLayoutEffect(() => {
86
+ const row = rowRef.current;
87
+ if (!row) return;
88
+ if (!ready) {
89
+ const children = [...row.querySelectorAll<HTMLElement>('[data-toolbar-cluster]')];
90
+ if (children.length === clusters.length) {
91
+ setMetrics({ signature, widths: children.map((child) => child.getBoundingClientRect().width) });
92
+ }
93
+ }
94
+ setAvailable(row.clientWidth);
95
+ });
96
+
97
+ // The row's width changes on a window resize, a panel drag and a layout mode
98
+ // switch — none of which are `window` events.
99
+ useEffect(() => {
100
+ const row = rowRef.current;
101
+ if (!row || typeof ResizeObserver === 'undefined') return;
102
+ const observer = new ResizeObserver(() => setAvailable(row.clientWidth));
103
+ observer.observe(row);
104
+ return () => observer.disconnect();
105
+ }, []);
106
+
107
+ const hiddenActions = clusters.filter((_, index) => !visible[index]).flatMap((cluster) => cluster.actions);
108
+
109
+ return (
110
+ <div ref={rowRef} className={`flex flex-nowrap items-center justify-end gap-x-1 overflow-hidden ${className}`}>
111
+ {clusters.map((cluster, index) =>
112
+ visible[index] ? (
113
+ <div key={cluster.id} data-toolbar-cluster="" className="flex items-center flex-shrink-0">
114
+ {cluster.children}
115
+ </div>
116
+ ) : null,
117
+ )}
118
+ {toolbarHasOverflow(visible) && <OverflowMenu actions={hiddenActions} />}
119
+ </div>
120
+ );
121
+ }
122
+
123
+ /**
124
+ * The `⋯` trigger and its menu.
125
+ *
126
+ * A menu rather than a horizontal scroll: a clipped action is invisible, and the
127
+ * diff toolbar already learned that a scroll strip with no visible scrollbar
128
+ * reads as "there is nothing more here" (measured: 361px of content in a 296px
129
+ * box, with `Stage` entirely outside it).
130
+ */
131
+ function OverflowMenu({ actions }: { actions: ToolbarOverflowAction[] }) {
132
+ const [open, setOpen] = useState(false);
133
+ const rootRef = useRef<HTMLDivElement>(null);
134
+ useOnClickOutside(rootRef, () => setOpen(false));
135
+
136
+ useEffect(() => {
137
+ if (!open) return;
138
+ const onKeyDown = (event: globalThis.KeyboardEvent) => {
139
+ if (event.key === 'Escape') setOpen(false);
140
+ };
141
+ document.addEventListener('keydown', onKeyDown);
142
+ return () => document.removeEventListener('keydown', onKeyDown);
143
+ }, [open]);
144
+
145
+ return (
146
+ <div className="relative flex-shrink-0" ref={rootRef}>
147
+ <button
148
+ type="button"
149
+ title="More actions"
150
+ aria-label="More actions"
151
+ aria-haspopup="menu"
152
+ aria-expanded={open}
153
+ onClick={() => setOpen(!open)}
154
+ className={`flex items-center justify-center p-1.5 rounded transition-colors cursor-pointer ${
155
+ open ? 'bg-ink/10 text-ink' : 'text-ink/50 hover:text-ink hover:bg-ink/5'
156
+ }`}
157
+ >
158
+ <Ellipsis size={14} />
159
+ </button>
160
+
161
+ {open && (
162
+ <div
163
+ role="menu"
164
+ className="absolute right-0 top-full mt-1 w-52 bg-paper border border-ink/15 rounded-lg shadow-lg z-50 py-1 text-xs"
165
+ >
166
+ {actions.map((action) => (
167
+ <button
168
+ key={action.label}
169
+ type="button"
170
+ role="menuitem"
171
+ disabled={action.disabled}
172
+ aria-pressed={action.active}
173
+ onClick={() => {
174
+ setOpen(false);
175
+ action.onSelect();
176
+ }}
177
+ className={`w-full text-left flex items-center gap-2 px-3 py-1.5 transition-colors disabled:cursor-default disabled:opacity-40 ${
178
+ action.active ? 'bg-ink/5 text-ink font-medium' : 'text-ink/75 hover:bg-ink/5 hover:text-ink'
179
+ }`}
180
+ >
181
+ <action.icon size={13} className="flex-shrink-0" />
182
+ <span className="truncate">{action.label}</span>
183
+ </button>
184
+ ))}
185
+ </div>
186
+ )}
187
+ </div>
188
+ );
189
+ }
@@ -13,7 +13,7 @@
13
13
  * source of truth.
14
14
  */
15
15
 
16
- import { FolderOpen, ListChecks, Pin, PinOff, Trash2 } from 'lucide-preact';
16
+ import { FolderOpen, GitBranch, ListChecks, Pin, PinOff, Trash2 } from 'lucide-preact';
17
17
 
18
18
  interface WorkspaceOptionsMenuProps {
19
19
  variant: 'desktop' | 'mobile';
@@ -23,6 +23,12 @@ interface WorkspaceOptionsMenuProps {
23
23
  onOpenFolder?: () => void;
24
24
  /** Enter the session batch-selection mode scoped to this workspace. */
25
25
  onSelectSessions?: () => void;
26
+ /**
27
+ * Open the New worktree dialog for this workspace. Offered only for a folder
28
+ * bound to a repository: a worktree needs one, and a row that opens a dialog
29
+ * which can only refuse is a row that teaches the user nothing.
30
+ */
31
+ onNewWorktree?: () => void;
26
32
  confirmDelete: boolean;
27
33
  onPin: () => void;
28
34
  onDelete: () => void;
@@ -35,6 +41,7 @@ export function WorkspaceOptionsMenu({
35
41
  isPinned = false,
36
42
  onOpenFolder,
37
43
  onSelectSessions,
44
+ onNewWorktree,
38
45
  confirmDelete,
39
46
  onPin,
40
47
  onDelete,
@@ -71,6 +78,12 @@ export function WorkspaceOptionsMenu({
71
78
  <span>Select sessions</span>
72
79
  </button>
73
80
  )}
81
+ {onNewWorktree && (
82
+ <button type="button" onClick={onNewWorktree} className="w-full text-left px-3 py-2 hover:bg-ink/5 text-ink/80 flex items-center space-x-2">
83
+ <GitBranch size={13} />
84
+ <span>New worktree…</span>
85
+ </button>
86
+ )}
74
87
  <button type="button" onClick={onRequestDelete} className="w-full text-left px-3 py-2 hover:bg-error/10 text-error flex items-center space-x-2">
75
88
  <Trash2 size={13} />
76
89
  <span>Delete Workspace</span>
@@ -105,6 +118,11 @@ export function WorkspaceOptionsMenu({
105
118
  <ListChecks size={12} /><span>Select sessions</span>
106
119
  </button>
107
120
  )}
121
+ {onNewWorktree && (
122
+ <button type="button" onClick={onNewWorktree} className="w-full text-left px-3 py-1.5 hover:bg-ink/5 cursor-pointer flex items-center space-x-2 text-ink/80">
123
+ <GitBranch size={12} /><span>New worktree…</span>
124
+ </button>
125
+ )}
108
126
  <button type="button" onClick={onRequestDelete} className="w-full text-left px-3 py-1.5 hover:bg-ink/5 cursor-pointer flex items-center space-x-2 text-error">
109
127
  <Trash2 size={12} /><span>Delete Workspace</span>
110
128
  </button>
@@ -12,8 +12,7 @@ import { titlebarPaddingLeft, titlebarPaddingRight } from '@/shared/lib/ui/title
12
12
  import { ServerSwitcher } from '@/client/components/common/server-switcher';
13
13
  import { useDesktopServers } from '@/client/hooks/desktop/servers';
14
14
  import { isDesktopShell } from '@/client/lib/desktop/bridge';
15
- import { dragRegionProps, drawsWindowControls, titlebarInsetLeft } from '@/client/lib/desktop/titlebar';
16
- import { WindowControls } from '@/client/components/common/window-controls';
15
+ import { dragRegionProps, titlebarInsetLeft } from '@/client/lib/desktop/titlebar';
17
16
  import { useOnClickOutside } from '@/client/hooks/ui/on-click-outside';
18
17
 
19
18
  interface TopNavbarProps {
@@ -203,10 +202,10 @@ export function TopNavbar({
203
202
  onToggleEditor={onToggleEditor}
204
203
  onSwitchToMobile={onSwitchToMobile}
205
204
  />
206
- {/* The Windows shell is borderless and its webview is an HWND, so the
207
- system's caption buttons cannot be drawn over it — the page owns
208
- them there. Absent on macOS, whose traffic lights are the system's. */}
209
- {drawsWindowControls() && <WindowControls />}
205
+ {/* The Windows/Linux caption buttons come from `HeaderControls` itself:
206
+ a desktop shell window can be narrower than the mobile breakpoint,
207
+ so the phone header has to draw them too — and a control that only
208
+ one layout renders is a control the other layout's window loses. */}
210
209
  </div>
211
210
 
212
211
  {menu.anchor && sessionActions && (