@kabmo-labs/kabmo 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (351) hide show
  1. package/README.md +7 -0
  2. package/dist/client/index.html +1 -1
  3. package/dist/client/index.js +403 -384
  4. package/dist/client/static/js/{index-0wvbca0b.js → index-bpptfkz2.js} +905 -875
  5. package/dist/client/static/js/index-k315p1e8.css +1 -0
  6. package/package.json +10 -1
  7. package/src/client/components/common/code-surface/documents.test.ts +120 -0
  8. package/src/client/components/common/code-surface/documents.ts +201 -0
  9. package/src/client/components/common/code-surface/extensions.test.ts +110 -0
  10. package/src/client/components/common/code-surface/extensions.ts +162 -0
  11. package/src/client/components/common/code-surface/handle.ts +102 -0
  12. package/src/client/components/common/code-surface/index.tsx +253 -314
  13. package/src/client/components/common/code-surface/keymap.test.ts +133 -0
  14. package/src/client/components/common/code-surface/keymap.ts +267 -0
  15. package/src/client/components/common/code-surface/marks.ts +74 -0
  16. package/src/client/components/common/code-surface/shiki.ts +234 -0
  17. package/src/client/components/common/code-surface/sync.test.ts +185 -0
  18. package/src/client/components/common/code-surface/sync.ts +58 -0
  19. package/src/client/components/common/code-surface/theme.test.ts +202 -0
  20. package/src/client/components/common/code-surface/theme.ts +111 -0
  21. package/src/client/components/common/editor-view-buttons/index.test.tsx +148 -0
  22. package/src/client/components/common/editor-view-buttons/index.tsx +109 -0
  23. package/src/client/components/common/header-controls/index.tsx +24 -1
  24. package/src/client/components/common/header-controls/shell-chrome.test.tsx +272 -0
  25. package/src/client/components/common/shell-update-button/Modal.tsx +163 -0
  26. package/src/client/components/common/shell-update-button/index.test.tsx +251 -0
  27. package/src/client/components/common/shell-update-button/index.tsx +125 -0
  28. package/src/client/components/common/toolbar-row/index.test.tsx +166 -0
  29. package/src/client/components/common/toolbar-row/index.tsx +189 -0
  30. package/src/client/components/common/workspace-options-menu/index.tsx +19 -1
  31. package/src/client/components/layout/desktop-layout/TopNavbar.tsx +5 -6
  32. package/src/client/components/layout/desktop-layout/WorkspacePanels.tsx +41 -78
  33. package/src/client/components/layout/desktop-layout/index.tsx +55 -69
  34. package/src/client/components/layout/desktop-layout/resizer/index.tsx +6 -3
  35. package/src/client/components/layout/session-sidebar/CategoryHeader.tsx +24 -0
  36. package/src/client/components/layout/session-sidebar/CategoryHeader.worktree.test.tsx +126 -0
  37. package/src/client/components/layout/session-sidebar/CategoryItem.tsx +4 -0
  38. package/src/client/components/layout/session-sidebar/Footer.tsx +8 -15
  39. package/src/client/components/layout/session-sidebar/SessionItem.tsx +12 -0
  40. package/src/client/components/layout/session-sidebar/SessionList.tsx +4 -0
  41. package/src/client/components/layout/session-sidebar/SessionsView.tsx +24 -0
  42. package/src/client/components/layout/session-sidebar/WorktreeBadge.test.ts +48 -0
  43. package/src/client/components/layout/session-sidebar/WorktreeBadge.tsx +55 -0
  44. package/src/client/components/mobile/LayoutWrapper.tsx +23 -16
  45. package/src/client/components/mobile/RightSidebar.tsx +21 -10
  46. package/src/client/components/mobile/SessionSidebar.tsx +8 -0
  47. package/src/client/components/mobile/header-titlebar.test.tsx +172 -0
  48. package/src/client/components/mobile/mobile-main-view/Header.tsx +10 -1
  49. package/src/client/components/mobile/mobile-right-sidebar/FullDiff.tsx +3 -0
  50. package/src/client/components/mobile/mobile-right-sidebar/FullEditor.tsx +63 -38
  51. package/src/client/components/mobile/mobile-right-sidebar/OutlineButton.test.tsx +137 -0
  52. package/src/client/components/mobile/mobile-right-sidebar/OutlineButton.tsx +14 -3
  53. package/src/client/components/mobile/mobile-session-sidebar/Footer.tsx +9 -21
  54. package/src/client/components/mobile/mobile-session-sidebar/Header.tsx +6 -2
  55. package/src/client/components/mobile/mobile-session-sidebar/Item.tsx +16 -0
  56. package/src/client/components/mobile/mobile-session-sidebar/List.tsx +4 -0
  57. package/src/client/components/mobile/mobile-session-sidebar/SessionRow.tsx +5 -0
  58. package/src/client/components/mobile/mobile-session-sidebar/WorktreeDialog.tsx +49 -0
  59. package/src/client/components/mobile/worktree-session.ts +51 -0
  60. package/src/client/components/settings/Modal.tsx +8 -0
  61. package/src/client/components/settings/categories/AppearanceSettings.tsx +3 -0
  62. package/src/client/components/settings/categories/ProjectSettings.tsx +22 -8
  63. package/src/client/components/settings/categories/appearance-settings/EditorThemeSection.test.tsx +167 -0
  64. package/src/client/components/settings/categories/appearance-settings/EditorThemeSection.tsx +184 -0
  65. package/src/client/components/settings/categories/project-settings/WorktreeSettings.tsx +146 -0
  66. package/src/client/components/settings/categories/project-settings/worktree-row.test.ts +83 -0
  67. package/src/client/components/settings/categories/project-settings/worktree-row.ts +47 -0
  68. package/src/client/components/workspace/chat-timeline/EmptyWorkspacePrompt.tsx +36 -55
  69. package/src/client/components/workspace/chat-timeline/PendingChatView.tsx +8 -0
  70. package/src/client/components/workspace/chat-timeline/SystemNotice.tsx +9 -4
  71. package/src/client/components/workspace/chat-timeline/TaskResultContent.tsx +9 -7
  72. package/src/client/components/workspace/chat-timeline/index.tsx +5 -0
  73. package/src/client/components/workspace/chat-timeline/target-picker/WorkspacePicker.test.tsx +132 -0
  74. package/src/client/components/workspace/chat-timeline/target-picker/WorkspacePicker.tsx +120 -0
  75. package/src/client/components/workspace/chat-timeline/target-picker/WorktreePicker.test.tsx +319 -0
  76. package/src/client/components/workspace/chat-timeline/target-picker/WorktreePicker.tsx +285 -0
  77. package/src/client/components/workspace/chat-timeline/tool-renderers/ask-panel/index.test.ts +2 -0
  78. package/src/client/components/workspace/chat-timeline/tool-renderers/hashline-patch/index.tsx +34 -29
  79. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/AstEdit.tsx +11 -9
  80. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Bash.tsx +10 -7
  81. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Edit.tsx +25 -8
  82. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Eval.test.tsx +146 -0
  83. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Eval.tsx +32 -17
  84. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/FileMatches.test.ts +114 -0
  85. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/FileMatches.tsx +111 -0
  86. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Read.tsx +7 -7
  87. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/SearchTool.tsx +9 -59
  88. package/src/client/components/workspace/chat-timeline/tool-renderers/shared/ArtifactBody.tsx +2 -7
  89. package/src/client/components/workspace/chat-timeline/tool-renderers/shared/DiffView.test.ts +27 -28
  90. package/src/client/components/workspace/chat-timeline/tool-renderers/shared/DiffView.tsx +39 -26
  91. package/src/client/components/workspace/chat-timeline/tool-renderers/shared/ExcerptCode.tsx +8 -8
  92. package/src/client/components/workspace/chat-timeline/tool-renderers/shared/HighlightedCode.tsx +35 -0
  93. package/src/client/components/workspace/chat-timeline/tool-renderers/shared/JsonCodeBlock.tsx +4 -4
  94. package/src/client/components/workspace/diff-panel/MergeDiffView.tsx +179 -0
  95. package/src/client/components/workspace/diff-panel/ThemedMergeDiffView.tsx +24 -0
  96. package/src/client/components/workspace/diff-panel/Toolbar.tsx +224 -186
  97. package/src/client/components/workspace/diff-panel/ToolbarSegments.tsx +98 -0
  98. package/src/client/components/workspace/diff-panel/index.tsx +76 -145
  99. package/src/client/components/workspace/diff-panel/merge.ts +315 -0
  100. package/src/client/components/workspace/editor/Body.tsx +4 -7
  101. package/src/client/components/workspace/editor/CommandPalette.tsx +2 -2
  102. package/src/client/components/workspace/editor/FileDocument.tsx +58 -63
  103. package/src/client/components/workspace/editor/FilePane.tsx +4 -7
  104. package/src/client/components/workspace/editor/FindWidget.tsx +7 -2
  105. package/src/client/components/workspace/editor/Toolbar.tsx +220 -123
  106. package/src/client/components/workspace/editor/index.tsx +7 -9
  107. package/src/client/components/workspace/file-explorer/ContextMenu.tsx +2 -1
  108. package/src/client/components/workspace/file-explorer/Modals.tsx +40 -12
  109. package/src/client/components/workspace/file-explorer/OutlineSection.test.tsx +18 -0
  110. package/src/client/components/workspace/file-explorer/OutlineSection.tsx +10 -3
  111. package/src/client/components/workspace/file-explorer/RootMenu.tsx +14 -5
  112. package/src/client/components/workspace/file-explorer/Toolbar.tsx +74 -0
  113. package/src/client/components/workspace/file-explorer/TreeItem.tsx +38 -3
  114. package/src/client/components/workspace/file-explorer/filter.ts +47 -0
  115. package/src/client/components/workspace/file-explorer/index.tsx +77 -64
  116. package/src/client/components/workspace/file-explorer/local-edit.test.ts +135 -0
  117. package/src/client/components/workspace/search-panel/Body.tsx +1 -1
  118. package/src/client/components/workspace/worktree-modal/index.test.tsx +118 -0
  119. package/src/client/components/workspace/worktree-modal/index.tsx +261 -0
  120. package/src/client/hooks/chat/omp/reconnect.test.ts +338 -0
  121. package/src/client/hooks/chat/omp/reconnect.ts +187 -0
  122. package/src/client/hooks/chat/omp/run-liveness.test.ts +71 -0
  123. package/src/client/hooks/chat/omp/run-liveness.ts +65 -0
  124. package/src/client/hooks/chat/omp/session-list.activity.test.ts +114 -0
  125. package/src/client/hooks/chat/omp/session-list.test-util.ts +164 -0
  126. package/src/client/hooks/chat/omp/session-list.test.ts +30 -150
  127. package/src/client/hooks/chat/omp/session-list.tsx +69 -9
  128. package/src/client/hooks/chat/timeline/generating.ts +19 -2
  129. package/src/client/hooks/chat/timeline/index.ts +27 -28
  130. package/src/client/hooks/chat/timeline/lost-run.test.ts +329 -0
  131. package/src/client/hooks/chat/timeline/lost-run.ts +177 -0
  132. package/src/client/hooks/chat/timeline/send-execute.test.ts +2 -0
  133. package/src/client/hooks/chat/timeline/send-steer.test.ts +2 -0
  134. package/src/client/hooks/chat/timeline/send.test.ts +2 -0
  135. package/src/client/hooks/chat/timeline/send.ts +11 -1
  136. package/src/client/hooks/chat/timeline/session-load.test-util.ts +205 -0
  137. package/src/client/hooks/chat/timeline/session-load.test.ts +61 -162
  138. package/src/client/hooks/chat/timeline/session-load.ts +69 -37
  139. package/src/client/hooks/chat/timeline/session-switch.test.ts +65 -0
  140. package/src/client/hooks/chat/timeline/session-switch.ts +63 -0
  141. package/src/client/hooks/chat/timeline/spawn-target.ts +52 -0
  142. package/src/client/hooks/chat/timeline/tool-open.test.ts +74 -0
  143. package/src/client/hooks/chat/timeline/tool-open.ts +52 -8
  144. package/src/client/hooks/code/deferred-highlight.test.ts +182 -0
  145. package/src/client/hooks/code/deferred-highlight.ts +129 -0
  146. package/src/client/hooks/editor/find-state.ts +1 -1
  147. package/src/client/hooks/editor/theme.test.ts +206 -0
  148. package/src/client/hooks/editor/theme.ts +82 -0
  149. package/src/client/hooks/editor/use-editor-drop.test.ts +101 -0
  150. package/src/client/hooks/editor/use-editor-drop.ts +67 -14
  151. package/src/client/hooks/editor/use-editor-find.ts +7 -7
  152. package/src/client/hooks/editor/use-outline-view.test.ts +182 -0
  153. package/src/client/hooks/editor/use-outline-view.ts +94 -7
  154. package/src/client/hooks/editor/use-scroll-restore.test.ts +55 -2
  155. package/src/client/hooks/editor/use-scroll-restore.ts +124 -4
  156. package/src/client/hooks/editor/use-search-reveal.ts +1 -1
  157. package/src/client/hooks/ui/long-press.test.ts +174 -0
  158. package/src/client/hooks/ui/long-press.ts +112 -0
  159. package/src/client/hooks/ui/settings-modal.ts +56 -0
  160. package/src/client/hooks/ui/updates/shell-download.test.ts +75 -0
  161. package/src/client/hooks/ui/updates/shell-download.ts +153 -28
  162. package/src/client/hooks/ui/updates/shell-offer.test.ts +213 -0
  163. package/src/client/hooks/ui/updates/shell-update.test.ts +140 -0
  164. package/src/client/hooks/ui/updates/shell-update.ts +119 -0
  165. package/src/client/hooks/workspace/diff-file.ts +29 -10
  166. package/src/client/hooks/workspace/diff-git-actions.ts +147 -0
  167. package/src/client/hooks/workspace/editor-view.ts +61 -0
  168. package/src/client/hooks/workspace/file-create.ts +41 -11
  169. package/src/client/hooks/workspace/file-listing.ts +56 -1
  170. package/src/client/hooks/workspace/file-tabs.test.ts +62 -5
  171. package/src/client/hooks/workspace/file-tabs.ts +24 -11
  172. package/src/client/hooks/workspace/file-tree-actions.ts +37 -5
  173. package/src/client/hooks/workspace/navbar-session-actions.ts +72 -0
  174. package/src/client/hooks/workspace/worktree/index.ts +165 -0
  175. package/src/client/lib/desktop/bridge.ts +45 -0
  176. package/src/client/lib/search-reveal.ts +1 -1
  177. package/src/client/main.tsx +5 -1
  178. package/src/client/tailwind.css +56 -8
  179. package/src/server/index.ts +7 -2
  180. package/src/server/lib/auth/guard.test.ts +28 -0
  181. package/src/server/lib/auth/guard.ts +10 -0
  182. package/src/server/lib/bundler/titlebar-css.test.ts +42 -1
  183. package/src/server/lib/db/schema.ts +4 -0
  184. package/src/server/lib/db/worktree-registry.server.ts +91 -0
  185. package/src/server/lib/fs/canonical.ts +52 -0
  186. package/src/server/lib/fs/relative-path.ts +48 -0
  187. package/src/server/lib/fs/root-worktree.test.ts +116 -0
  188. package/src/server/lib/fs/root.ts +39 -10
  189. package/src/server/lib/fs/worktree/carry.ts +134 -0
  190. package/src/server/lib/fs/worktree/create.test.ts +249 -0
  191. package/src/server/lib/fs/worktree/create.ts +262 -0
  192. package/src/server/lib/fs/worktree/errors.ts +43 -0
  193. package/src/server/lib/fs/worktree/list.test.ts +207 -0
  194. package/src/server/lib/fs/worktree/list.ts +142 -0
  195. package/src/server/lib/fs/worktree/location.test.ts +206 -0
  196. package/src/server/lib/fs/worktree/location.ts +114 -0
  197. package/src/server/lib/fs/worktree/project-id.ts +53 -0
  198. package/src/server/lib/fs/worktree/remove.test.ts +242 -0
  199. package/src/server/lib/fs/worktree/remove.ts +152 -0
  200. package/src/server/lib/fs/worktree/settings.server.ts +56 -0
  201. package/src/server/lib/fs/worktree/topology.ts +142 -0
  202. package/src/server/lib/lifecycle/claim-port.test.ts +156 -0
  203. package/src/server/lib/lifecycle/port-guard.test.ts +20 -164
  204. package/src/server/lib/lifecycle/port-guard.ts +39 -7
  205. package/src/server/lib/lifecycle/probe-health.test.ts +166 -0
  206. package/src/server/lib/lifecycle/probe.test.ts +77 -164
  207. package/src/server/lib/lifecycle/probe.ts +121 -13
  208. package/src/server/lib/omp/rpc/frame-fold-signals.test.ts +112 -0
  209. package/src/server/lib/omp/rpc/frame-fold.ts +10 -5
  210. package/src/server/lib/omp/session/sidebar-activity.server.test.ts +162 -0
  211. package/src/server/lib/omp/session/sidebar-activity.server.ts +70 -0
  212. package/src/server/lib/omp/session/sidebar-data.canonical.test.ts +155 -0
  213. package/src/server/lib/omp/session/sidebar-data.server.ts +28 -1
  214. package/src/server/lib/omp/session/telemetry/compute.ts +13 -1
  215. package/src/server/lib/omp/session/telemetry.test.ts +33 -0
  216. package/src/server/lib/panels/local-edits.server.ts +33 -0
  217. package/src/server/lib/panels/sync.fixture.test-util.ts +92 -0
  218. package/src/server/lib/panels/sync.local-edits.test.ts +150 -0
  219. package/src/server/lib/panels/sync.server.test.ts +125 -147
  220. package/src/server/lib/panels/sync.server.ts +153 -48
  221. package/src/server/lib/realtime/signals.server.ts +10 -0
  222. package/src/server/lib/realtime/topics/catalog.server.ts +4 -1
  223. package/src/server/lib/realtime/topics.server.ts +16 -0
  224. package/src/server/lib/updates/shell-assets.test.ts +114 -0
  225. package/src/server/lib/updates/shell-assets.ts +200 -0
  226. package/src/server/lib/updates/shell-cache.ts +105 -0
  227. package/src/server/lib/updates/shell-pages.test.ts +187 -0
  228. package/src/server/lib/updates/shell-pages.ts +157 -0
  229. package/src/server/lib/updates/shell-release-desktop.test.ts +166 -0
  230. package/src/server/lib/updates/shell-release.test.ts +59 -71
  231. package/src/server/lib/updates/shell-release.ts +178 -198
  232. package/src/server/routes/fs/action-create.test.ts +18 -13
  233. package/src/server/routes/fs/action-nested.test.ts +184 -0
  234. package/src/server/routes/fs/action.ts +47 -16
  235. package/src/server/routes/fs/git-merge.test.ts +11 -1
  236. package/src/server/routes/fs/read.test.ts +24 -7
  237. package/src/server/routes/fs/scope.test.ts +15 -10
  238. package/src/server/routes/fs/scope.ts +3 -14
  239. package/src/server/routes/index.ts +2 -0
  240. package/src/server/routes/updates/android.ts +6 -1
  241. package/src/server/routes/updates/desktop.ts +59 -0
  242. package/src/server/routes/updates/index.ts +2 -0
  243. package/src/server/routes/updates/ios.ts +5 -1
  244. package/src/server/routes/worktrees/create.ts +138 -0
  245. package/src/server/routes/worktrees/index.ts +25 -0
  246. package/src/server/routes/worktrees/list.ts +134 -0
  247. package/src/server/routes/worktrees/remove.ts +102 -0
  248. package/src/server/routes/worktrees/routes.test.ts +326 -0
  249. package/src/server/routes/worktrees/shared.ts +90 -0
  250. package/src/shared/lib/chat/timeline/omp-callbacks.test.ts +36 -0
  251. package/src/shared/lib/chat/timeline/omp-callbacks.ts +18 -0
  252. package/src/shared/lib/chat/timeline/run-signals.test.ts +2 -0
  253. package/src/shared/lib/code/editor/comment-syntax.test.ts +37 -0
  254. package/src/shared/lib/code/editor/comment-syntax.ts +55 -0
  255. package/src/shared/lib/code/editor/find-replace.test.ts +134 -0
  256. package/src/shared/lib/code/editor/{find.ts → find-replace.ts} +23 -107
  257. package/src/shared/lib/code/editor/{find.test.ts → find-scan.test.ts} +19 -73
  258. package/src/shared/lib/code/editor/find-scan.ts +177 -0
  259. package/src/shared/lib/code/editor/keymap.ts +3 -3
  260. package/src/shared/lib/code/editor/lines.test.ts +7 -55
  261. package/src/shared/lib/code/editor/lines.ts +8 -48
  262. package/src/shared/lib/code/editor/marks.test.ts +4 -5
  263. package/src/shared/lib/code/editor/marks.ts +13 -25
  264. package/src/shared/lib/code/editor/theme-loader.ts +97 -0
  265. package/src/shared/lib/code/editor/themes.generated.ts +156 -0
  266. package/src/shared/lib/code/editor/themes.test.ts +165 -0
  267. package/src/shared/lib/code/editor/themes.ts +165 -0
  268. package/src/shared/lib/code/highlight-memo.test.ts +103 -0
  269. package/src/shared/lib/code/highlight-memo.ts +149 -0
  270. package/src/shared/lib/code/highlighter-warmup.ts +112 -0
  271. package/src/shared/lib/code/highlighter.ts +3 -1
  272. package/src/shared/lib/code/idle.ts +37 -0
  273. package/src/shared/lib/code/outline/rows.test.ts +96 -0
  274. package/src/shared/lib/code/outline/rows.ts +94 -0
  275. package/src/shared/lib/code/syntax-highlight.test.ts +1 -79
  276. package/src/shared/lib/code/syntax-highlight.ts +2 -3
  277. package/src/shared/lib/fs/file-kind-classify.test.ts +25 -11
  278. package/src/shared/lib/fs/file-kind.ts +47 -13
  279. package/src/shared/lib/fs/file-tree.test.ts +183 -0
  280. package/src/shared/lib/fs/file-tree.ts +159 -0
  281. package/src/shared/lib/fs/file-url.test.ts +52 -0
  282. package/src/shared/lib/fs/file-url.ts +37 -0
  283. package/src/shared/lib/fs/patch-sides.test.ts +114 -0
  284. package/src/shared/lib/fs/patch-sides.ts +139 -0
  285. package/src/shared/lib/fs/paths.test.ts +24 -1
  286. package/src/shared/lib/fs/paths.ts +17 -0
  287. package/src/shared/lib/fs/worktree/settings-keys.ts +18 -0
  288. package/src/shared/lib/omp/session/prewarm.ts +7 -0
  289. package/src/shared/lib/realtime/protocol.ts +16 -0
  290. package/src/shared/lib/ui/toolbar-overflow.test.ts +83 -0
  291. package/src/shared/lib/ui/toolbar-overflow.ts +86 -0
  292. package/src/shared/lib/updates/shell-arch.ts +54 -0
  293. package/src/shared/lib/updates/shell-ua.ts +24 -4
  294. package/src/shared/lib/workspace/active-project.ts +23 -0
  295. package/src/shared/lib/workspace/editor-view-mode.test.ts +70 -0
  296. package/src/shared/lib/workspace/editor-view-mode.ts +48 -0
  297. package/src/shared/lib/workspace/panel-widths.test.ts +30 -0
  298. package/src/shared/lib/workspace/panel-widths.ts +25 -5
  299. package/src/shared/lib/workspace/session-state/cache.ts +159 -0
  300. package/src/shared/lib/workspace/session-state/harness.test-util.ts +55 -0
  301. package/src/shared/lib/workspace/session-state/merge.ts +100 -0
  302. package/src/shared/lib/workspace/session-state/store-load.test.ts +206 -0
  303. package/src/shared/lib/workspace/session-state/store.test.ts +17 -139
  304. package/src/shared/lib/workspace/session-state/store.ts +92 -130
  305. package/src/shared/lib/workspace/workspace-modules.test.ts +49 -1
  306. package/src/shared/lib/workspace/workspace-panel-widths.ts +124 -0
  307. package/src/shared/lib/workspace/worktree/branch-name.test.ts +95 -0
  308. package/src/shared/lib/workspace/worktree/branch-name.ts +115 -0
  309. package/src/shared/lib/workspace/worktree/types.ts +104 -0
  310. package/src/shared/types/fs.ts +10 -0
  311. package/src/shared/types/realtime.ts +29 -0
  312. package/src/shared/types/settings/state.ts +10 -0
  313. package/src/shared/types/workspace.ts +14 -0
  314. package/src/test-support/file-explorer-disk.test-util.ts +76 -0
  315. package/src/test-support/file-explorer.test-util.ts +71 -40
  316. package/src/test-support/loopback.ts +183 -0
  317. package/dist/client/static/js/index-nzyjf92m.css +0 -1
  318. package/src/client/components/common/code-editor/handler.test.ts +0 -274
  319. package/src/client/components/common/code-editor/handler.ts +0 -175
  320. package/src/client/components/common/code-editor/index.tsx +0 -314
  321. package/src/client/components/common/code-surface/measure.ts +0 -148
  322. package/src/client/components/workspace/diff-panel/SplitView.tsx +0 -209
  323. package/src/client/components/workspace/diff-panel/UnifiedView.tsx +0 -150
  324. package/src/client/hooks/editor/scroll-container.test.ts +0 -102
  325. package/src/client/hooks/editor/scroll-container.ts +0 -26
  326. package/src/client/hooks/editor/use-code-window.ts +0 -100
  327. package/src/client/hooks/editor/use-editor-history.test.ts +0 -216
  328. package/src/client/hooks/editor/use-editor-history.ts +0 -134
  329. package/src/client/hooks/editor/use-occurrences.test.ts +0 -236
  330. package/src/client/hooks/editor/use-occurrences.ts +0 -98
  331. package/src/shared/lib/code/editor/commands.test.ts +0 -155
  332. package/src/shared/lib/code/editor/commands.ts +0 -260
  333. package/src/shared/lib/code/editor/edits.test.ts +0 -100
  334. package/src/shared/lib/code/editor/edits.ts +0 -122
  335. package/src/shared/lib/code/editor/history.test.ts +0 -118
  336. package/src/shared/lib/code/editor/history.ts +0 -101
  337. package/src/shared/lib/code/editor/layers.test.ts +0 -77
  338. package/src/shared/lib/code/editor/layers.ts +0 -67
  339. package/src/shared/lib/code/editor/line-edits.test.ts +0 -131
  340. package/src/shared/lib/code/editor/line-edits.ts +0 -159
  341. package/src/shared/lib/code/editor/occurrences.test.ts +0 -117
  342. package/src/shared/lib/code/editor/occurrences.ts +0 -111
  343. package/src/shared/lib/code/editor/reset-css.test.ts +0 -35
  344. package/src/shared/lib/code/editor/reset-css.ts +0 -32
  345. package/src/shared/lib/code/editor/text-commands.test.ts +0 -178
  346. package/src/shared/lib/code/editor/text-commands.ts +0 -154
  347. package/src/shared/lib/code/lazy-window.test.ts +0 -161
  348. package/src/shared/lib/code/lazy-window.ts +0 -156
  349. package/src/shared/lib/code/windowed-highlight.ts +0 -114
  350. package/src/shared/lib/fs/diff-parser.test.ts +0 -116
  351. package/src/shared/lib/fs/diff-parser.ts +0 -213
@@ -1,6 +1,9 @@
1
1
  import { AlignJustify, Check, Code2, Columns, Copy, Diff, FileCode, Minus, Plus, RefreshCw, Space, Undo2, WrapText } from 'lucide-preact';
2
2
  import { FileIcon } from '@/client/components/common/file-icon';
3
+ import { EditorViewButtons, useEditorViewActions } from '@/client/components/common/editor-view-buttons';
3
4
  import { getGitStatusInfo } from '@/shared/lib/fs/git-status';
5
+ import { ToolbarRow, type ToolbarCluster } from '@/client/components/common/toolbar-row';
6
+ import { DiffContentModeSegments, DiffViewModeSegments } from '@/client/components/workspace/diff-panel/ToolbarSegments';
4
7
 
5
8
  export type DiffViewMode = 'unified' | 'split';
6
9
 
@@ -26,6 +29,12 @@ interface DiffToolbarProps {
26
29
  isCopied: boolean;
27
30
  /** A stage/unstage/discard write is in flight; the toolbar locks its actions. */
28
31
  isBusy?: boolean;
32
+ /**
33
+ * Offer the two WIDTH-mode buttons (compact / full view). Desktop-only: the
34
+ * phone renders this same panel in a full-screen overlay, where there is no
35
+ * chat column or sidebar beside it for a mode to widen into.
36
+ */
37
+ viewModeButtons?: boolean;
29
38
  onSetViewMode: (mode: DiffViewMode) => void;
30
39
  onSetContentMode: (mode: DiffContentMode) => void;
31
40
  onToggleWordWrap: () => void;
@@ -53,6 +62,7 @@ export function DiffToolbar({
53
62
  isLoading,
54
63
  isCopied,
55
64
  isBusy = false,
65
+ viewModeButtons = true,
56
66
  onSetViewMode,
57
67
  onSetContentMode,
58
68
  onToggleWordWrap,
@@ -63,219 +73,247 @@ export function DiffToolbar({
63
73
  onRefresh,
64
74
  onCopyDiff,
65
75
  }: DiffToolbarProps) {
76
+ const viewModeActions = useEditorViewActions();
66
77
  const fileName = filePath.split('/').pop() || filePath;
67
78
  const statusInfo = getGitStatusInfo(status, isStaged);
68
79
  // Only the actions that write to git lock while one is in flight. Copying the
69
80
  // diff is a local read of what is already on screen, so it stays available.
70
81
  const locked = isLoading || isBusy;
71
82
 
72
- return (
73
- // `@container` so the labels below can hide on a NARROW PANEL rather than a
74
- // narrow viewport: the editor panel is 320px wide on a 1440px screen, and
75
- // `sm:` (viewport-keyed) kept every label on, overflowing the toolbar.
76
- <div className="@container flex items-center justify-between px-3 py-1.5 bg-paper border-b border-ink/10 text-xs font-sans select-none flex-shrink-0 gap-2 min-w-0">
77
- {/* Left: File name and status badge. Hidden when the panel is narrow —
78
- the editor tab already carries the name and the status character, and
79
- the mobile diff header carries both. */}
80
- <div className="hidden @[560px]:flex items-center space-x-2 min-w-0 pr-2">
81
- <FileIcon name={fileName} size={14} className="flex-shrink-0" />
82
- <span className="font-mono text-ink font-medium truncate" title={filePath}>
83
- {fileName}
84
- </span>
83
+ const stageLabel = isStaged ? 'Unstage file' : 'Stage file';
84
+ const wrapLabel = wordWrap ? 'Word wrap on' : 'Word wrap off';
85
+ const whitespaceLabel = ignoreWhitespace ? 'Whitespace changes hidden' : 'Hide whitespace changes';
86
+ const copyLabel = isCopied ? 'Copied' : 'Copy diff text';
85
87
 
86
- <span className={`px-1.5 py-0.5 text-[10px] font-mono font-semibold rounded border ${statusInfo.badgeBgClass} flex-shrink-0`}>
87
- {statusInfo.charStatus} {statusInfo.label}
88
- </span>
89
- </div>
90
-
91
- {/* Right: Controls & actions. `ml-auto` keeps them right-aligned where the
92
- identity block is hidden. This strip WRAPS rather than scrolls: a
93
- 320px panel cannot hold ten actions in one row, and the previous
94
- `overflow-x-auto no-scrollbar` clipped the overflow invisibly —
95
- measured 361px of content in a 296px box, with `Stage` entirely
96
- outside it and no scrollbar to say so. Wrapping costs one extra row on
97
- a narrow panel and keeps every action reachable. Each segment sets its
98
- OWN mode — toggling meant clicking the already-active segment flipped
99
- the view to the other one. */}
100
- <div className="flex flex-wrap items-center justify-end gap-x-0.5 @[440px]:gap-x-1 gap-y-1 text-ink/70 ml-auto">
101
- {/* Change counts live here, not in the identity block: the editor tab
102
- carries the file name but never the counts, so this is the one piece
103
- of the header that must survive a narrow panel. */}
104
- {(additions > 0 || deletions > 0) && (
105
- <div className="flex items-center space-x-1.5 text-[11px] font-mono flex-shrink-0 pr-1.5 mr-0.5 @[440px]:mr-1 border-r border-ink/10">
106
- {additions > 0 && <span className="text-success font-semibold">+{additions}</span>}
107
- {deletions > 0 && <span className="text-error font-semibold">-{deletions}</span>}
108
- </div>
109
- )}
88
+ /**
89
+ * The controls, as clusters that hide as a unit.
90
+ *
91
+ * ORDER is the visual order a reader looks for. PINNING says what a narrow
92
+ * panel must not bury: the width modes (the reason these buttons exist), Stage
93
+ * (the panel's primary write) and Edit File (the way out of the diff). The
94
+ * reading options — Unified/Split, Full/Diff, wrap, whitespace — are what a
95
+ * `⋯` menu is for; they change how the diff READS, not what it does.
96
+ */
97
+ const clusters: ToolbarCluster[] = [];
110
98
 
111
- {/* View mode toggle (Unified vs Split) */}
112
- <div className="flex items-center bg-canvas rounded border border-ink/15 p-0.5 mr-0.5 @[440px]:mr-1 flex-shrink-0">
113
- <button
114
- type="button"
115
- onClick={() => onSetViewMode('split')}
116
- className={`px-2 py-0.5 rounded text-[11px] font-medium flex items-center space-x-1 transition-colors cursor-pointer flex-shrink-0 ${
117
- viewMode === 'split' ? 'bg-paper text-ink shadow-xs' : 'text-ink/60 hover:text-ink'
118
- }`}
119
- title="Split (side-by-side) diff"
120
- aria-label="Split (side-by-side) diff"
121
- aria-pressed={viewMode === 'split'}
122
- >
123
- <Columns size={12} />
124
- <span className="hidden @[420px]:inline">Split</span>
125
- </button>
126
- <button
127
- type="button"
128
- onClick={() => onSetViewMode('unified')}
129
- className={`px-2 py-0.5 rounded text-[11px] font-medium flex items-center space-x-1 transition-colors cursor-pointer flex-shrink-0 ${
130
- viewMode === 'unified' ? 'bg-paper text-ink shadow-xs' : 'text-ink/60 hover:text-ink'
131
- }`}
132
- title="Unified diff"
133
- aria-label="Unified diff"
134
- aria-pressed={viewMode === 'unified'}
135
- >
136
- <AlignJustify size={12} />
137
- <span className="hidden @[420px]:inline">Unified</span>
138
- </button>
99
+ if (additions > 0 || deletions > 0) {
100
+ clusters.push({
101
+ id: 'counts',
102
+ // The only piece of the header that survives everywhere: the editor tab
103
+ // carries the file name but never the counts.
104
+ pinned: true,
105
+ actions: [],
106
+ children: (
107
+ <div className="flex items-center space-x-1.5 text-[11px] font-mono pr-1.5 mr-0.5 @[440px]:mr-1 border-r border-ink/10">
108
+ {additions > 0 && <span className="text-success font-semibold">+{additions}</span>}
109
+ {deletions > 0 && <span className="text-error font-semibold">-{deletions}</span>}
139
110
  </div>
111
+ ),
112
+ });
113
+ }
140
114
 
141
- {/* Content scope toggle (Full Code vs Diff Only). A separate group from
142
- the view-mode toggle because the two are orthogonal: Unified/Split is
143
- the shape of the rows, Full/Diff is how many of them there are. Each
144
- segment SETS its mode — clicking the active one must not flip the
145
- other, which is what a toggle would do. */}
146
- <div className="flex items-center bg-canvas rounded border border-ink/15 p-0.5 mr-0.5 @[440px]:mr-1 flex-shrink-0">
147
- <button
148
- type="button"
149
- onClick={() => onSetContentMode('full')}
150
- className={`px-2 py-0.5 rounded text-[11px] font-medium flex items-center space-x-1 transition-colors cursor-pointer flex-shrink-0 ${
151
- contentMode === 'full' ? 'bg-paper text-ink shadow-xs' : 'text-ink/60 hover:text-ink'
152
- }`}
153
- title="Full code (whole file, changes marked)"
154
- aria-label="Full code (whole file, changes marked)"
155
- aria-pressed={contentMode === 'full'}
156
- >
157
- <FileCode size={12} />
158
- <span className="hidden @[420px]:inline">Full Code</span>
159
- </button>
160
- <button
161
- type="button"
162
- onClick={() => onSetContentMode('diff')}
163
- className={`px-2 py-0.5 rounded text-[11px] font-medium flex items-center space-x-1 transition-colors cursor-pointer flex-shrink-0 ${
164
- contentMode === 'diff' ? 'bg-paper text-ink shadow-xs' : 'text-ink/60 hover:text-ink'
165
- }`}
166
- title="Diff only (changed hunks)"
167
- aria-label="Diff only (changed hunks)"
168
- aria-pressed={contentMode === 'diff'}
169
- >
170
- <Diff size={12} />
171
- <span className="hidden @[420px]:inline">Diff Only</span>
172
- </button>
173
- </div>
115
+ clusters.push({
116
+ id: 'view-mode',
117
+ actions: [
118
+ { label: 'Split (side-by-side) diff', icon: Columns, onSelect: () => onSetViewMode('split'), active: viewMode === 'split' },
119
+ { label: 'Unified diff', icon: AlignJustify, onSelect: () => onSetViewMode('unified'), active: viewMode === 'unified' },
120
+ ],
121
+ children: <DiffViewModeSegments viewMode={viewMode} onSetViewMode={onSetViewMode} />,
122
+ });
174
123
 
175
- {/* Word wrap. On by default: a diff of a minified file or a long line
176
- is unreadable when it runs off the panel, and horizontal scrolling
177
- hides the change the reader opened the diff to see. Off keeps one
178
- line = one row, which is what a column-aligned comparison needs. */}
179
- <button
180
- type="button"
181
- onClick={onToggleWordWrap}
182
- className={`p-1.5 rounded transition-colors cursor-pointer flex-shrink-0 ${
183
- wordWrap ? 'bg-ink/10 text-ink' : 'text-ink/50 hover:text-ink hover:bg-ink/5'
184
- }`}
185
- title={wordWrap ? 'Word wrap on' : 'Word wrap off'}
186
- aria-label={wordWrap ? 'Word wrap on' : 'Word wrap off'}
187
- aria-pressed={wordWrap}
188
- >
189
- <WrapText size={13} />
190
- </button>
124
+ clusters.push({
125
+ id: 'content-mode',
126
+ actions: [
127
+ {
128
+ label: 'Full code (whole file, changes marked)',
129
+ icon: FileCode,
130
+ onSelect: () => onSetContentMode('full'),
131
+ active: contentMode === 'full',
132
+ },
133
+ { label: 'Diff only (changed hunks)', icon: Diff, onSelect: () => onSetContentMode('diff'), active: contentMode === 'diff' },
134
+ ],
135
+ children: <DiffContentModeSegments contentMode={contentMode} onSetContentMode={onSetContentMode} />,
136
+ });
191
137
 
192
- {/* Ignore whitespace toggle */}
193
- <button
194
- type="button"
195
- onClick={onToggleWhitespace}
196
- className={`p-1.5 rounded transition-colors cursor-pointer flex-shrink-0 ${
197
- ignoreWhitespace ? 'bg-ink/10 text-ink' : 'text-ink/50 hover:text-ink hover:bg-ink/5'
198
- }`}
199
- title={ignoreWhitespace ? 'Whitespace changes hidden' : 'Hide whitespace changes'}
200
- aria-label={ignoreWhitespace ? 'Whitespace changes hidden' : 'Hide whitespace changes'}
201
- aria-pressed={ignoreWhitespace}
202
- >
203
- <Space size={13} />
204
- </button>
138
+ clusters.push({
139
+ id: 'wrap',
140
+ actions: [{ label: wrapLabel, icon: WrapText, onSelect: onToggleWordWrap, active: wordWrap }],
141
+ children: (
142
+ <button
143
+ type="button"
144
+ onClick={onToggleWordWrap}
145
+ className={`p-1.5 rounded transition-colors cursor-pointer flex-shrink-0 ${
146
+ wordWrap ? 'bg-ink/10 text-ink' : 'text-ink/50 hover:text-ink hover:bg-ink/5'
147
+ }`}
148
+ title={wrapLabel}
149
+ aria-label={wrapLabel}
150
+ aria-pressed={wordWrap}
151
+ >
152
+ <WrapText size={13} />
153
+ </button>
154
+ ),
155
+ });
205
156
 
206
- {/* Refresh */}
207
- <button
208
- type="button"
209
- onClick={onRefresh}
210
- className={`${ACTION_CLASS} text-ink/50 hover:text-ink hover:bg-ink/5 flex-shrink-0`}
211
- title="Reload this diff"
212
- aria-label="Reload this diff"
213
- disabled={locked}
214
- >
215
- <RefreshCw size={13} className={isLoading ? 'animate-spin' : ''} />
216
- </button>
157
+ clusters.push({
158
+ id: 'whitespace',
159
+ actions: [{ label: whitespaceLabel, icon: Space, onSelect: onToggleWhitespace, active: ignoreWhitespace }],
160
+ children: (
161
+ <button
162
+ type="button"
163
+ onClick={onToggleWhitespace}
164
+ className={`p-1.5 rounded transition-colors cursor-pointer flex-shrink-0 ${
165
+ ignoreWhitespace ? 'bg-ink/10 text-ink' : 'text-ink/50 hover:text-ink hover:bg-ink/5'
166
+ }`}
167
+ title={whitespaceLabel}
168
+ aria-label={whitespaceLabel}
169
+ aria-pressed={ignoreWhitespace}
170
+ >
171
+ <Space size={13} />
172
+ </button>
173
+ ),
174
+ });
175
+
176
+ if (viewModeButtons) {
177
+ clusters.push({
178
+ id: 'view-modes',
179
+ // The same two buttons the editor toolbar draws, in the same order, so a
180
+ // mode entered from either tab kind is one mode.
181
+ pinned: true,
182
+ actions: viewModeActions,
183
+ children: <EditorViewButtons size={13} />,
184
+ });
185
+ }
186
+
187
+ clusters.push({
188
+ id: 'refresh',
189
+ actions: [{ label: 'Reload this diff', icon: RefreshCw, onSelect: onRefresh, disabled: locked }],
190
+ children: (
191
+ <button
192
+ type="button"
193
+ onClick={onRefresh}
194
+ className={`${ACTION_CLASS} text-ink/50 hover:text-ink hover:bg-ink/5`}
195
+ title="Reload this diff"
196
+ aria-label="Reload this diff"
197
+ disabled={locked}
198
+ >
199
+ <RefreshCw size={13} className={isLoading ? 'animate-spin' : ''} />
200
+ </button>
201
+ ),
202
+ });
203
+
204
+ clusters.push({
205
+ id: 'copy',
206
+ // Never disabled: it reads the buffer already on screen.
207
+ actions: [{ label: copyLabel, icon: Copy, onSelect: onCopyDiff }],
208
+ children: (
209
+ <button
210
+ type="button"
211
+ onClick={onCopyDiff}
212
+ className={`${ACTION_CLASS} text-ink/50 hover:text-ink hover:bg-ink/5`}
213
+ title={copyLabel}
214
+ aria-label={copyLabel}
215
+ >
216
+ {isCopied ? <Check size={13} className="text-success" /> : <Copy size={13} />}
217
+ </button>
218
+ ),
219
+ });
217
220
 
218
- {/* Copy diff — never disabled: it reads the buffer already on screen. */}
221
+ if (!isStaged) {
222
+ clusters.push({
223
+ id: 'discard',
224
+ actions: [{ label: 'Discard changes in this file', icon: Undo2, onSelect: onDiscard, disabled: locked }],
225
+ children: (
219
226
  <button
220
227
  type="button"
221
- onClick={onCopyDiff}
222
- className={`${ACTION_CLASS} text-ink/50 hover:text-ink hover:bg-ink/5 flex-shrink-0`}
223
- title={isCopied ? 'Copied' : 'Copy diff text'}
224
- aria-label={isCopied ? 'Copied' : 'Copy diff text'}
228
+ onClick={onDiscard}
229
+ className={`${ACTION_CLASS} text-ink/50 hover:text-error hover:bg-error/10`}
230
+ title="Discard changes in this file"
231
+ aria-label="Discard changes in this file"
232
+ disabled={locked}
225
233
  >
226
- {isCopied ? <Check size={13} className="text-success" /> : <Copy size={13} />}
234
+ <Undo2 size={13} />
227
235
  </button>
236
+ ),
237
+ });
238
+ }
228
239
 
229
- {/* Discard changes */}
230
- {!isStaged && (
231
- <button
232
- type="button"
233
- onClick={onDiscard}
234
- className={`${ACTION_CLASS} text-ink/50 hover:text-error hover:bg-error/10 flex-shrink-0`}
235
- title="Discard changes in this file"
236
- aria-label="Discard changes in this file"
237
- disabled={locked}
238
- >
239
- <Undo2 size={13} />
240
- </button>
240
+ clusters.push({
241
+ id: 'stage',
242
+ // The panel's primary write: a menu row must not be the only way to stage.
243
+ pinned: true,
244
+ actions: [{ label: stageLabel, icon: isStaged ? Minus : Plus, onSelect: onStageUnstage, disabled: locked }],
245
+ children: (
246
+ <button
247
+ type="button"
248
+ onClick={onStageUnstage}
249
+ className="px-2 py-1 text-[11px] font-medium rounded border border-ink/15 hover:bg-ink/5 flex items-center space-x-1 transition-colors text-ink cursor-pointer disabled:cursor-default disabled:opacity-40 disabled:hover:bg-transparent"
250
+ title={stageLabel}
251
+ aria-label={stageLabel}
252
+ disabled={locked}
253
+ >
254
+ {isStaged ? (
255
+ <>
256
+ <Minus size={12} />
257
+ <span className="hidden @[360px]:inline">Unstage</span>
258
+ </>
259
+ ) : (
260
+ <>
261
+ <Plus size={12} />
262
+ <span className="hidden @[360px]:inline">Stage</span>
263
+ </>
241
264
  )}
265
+ </button>
266
+ ),
267
+ });
242
268
 
243
- {/* Stage / Unstage button */}
269
+ if (onOpenInEditor) {
270
+ clusters.push({
271
+ id: 'edit-file',
272
+ // The way out of the diff, and the only control that changes the SURFACE
273
+ // rather than the view: kept on the row.
274
+ pinned: true,
275
+ actions: [{ label: 'Open in code editor', icon: Code2, onSelect: onOpenInEditor }],
276
+ children: (
244
277
  <button
245
278
  type="button"
246
- onClick={onStageUnstage}
247
- className="px-2 py-1 text-[11px] font-medium rounded border border-ink/15 hover:bg-ink/5 flex items-center space-x-1 transition-colors text-ink cursor-pointer disabled:cursor-default disabled:opacity-40 disabled:hover:bg-transparent flex-shrink-0"
248
- title={isStaged ? 'Unstage file' : 'Stage file'}
249
- aria-label={isStaged ? 'Unstage file' : 'Stage file'}
250
- disabled={locked}
279
+ onClick={onOpenInEditor}
280
+ className="px-2 py-1 text-[11px] font-medium rounded bg-ink text-canvas hover:bg-ink/90 flex items-center space-x-1 transition-colors shadow-xs ml-1 cursor-pointer"
281
+ title="Open in code editor"
282
+ aria-label="Open in code editor"
251
283
  >
252
- {isStaged ? (
253
- <>
254
- <Minus size={12} />
255
- <span className="hidden @[360px]:inline">Unstage</span>
256
- </>
257
- ) : (
258
- <>
259
- <Plus size={12} />
260
- <span className="hidden @[360px]:inline">Stage</span>
261
- </>
262
- )}
284
+ <Code2 size={12} />
285
+ <span className="hidden @[360px]:inline">Edit File</span>
263
286
  </button>
287
+ ),
288
+ });
289
+ }
264
290
 
265
- {/* Switch to normal code editor */}
266
- {onOpenInEditor && (
267
- <button
268
- type="button"
269
- onClick={onOpenInEditor}
270
- className="px-2 py-1 text-[11px] font-medium rounded bg-ink text-canvas hover:bg-ink/90 flex items-center space-x-1 transition-colors shadow-xs ml-1 cursor-pointer flex-shrink-0"
271
- title="Open in code editor"
272
- aria-label="Open in code editor"
273
- >
274
- <Code2 size={12} />
275
- <span className="hidden @[360px]:inline">Edit File</span>
276
- </button>
277
- )}
291
+ return (
292
+ // `@container` so the labels below can hide on a NARROW PANEL rather than a
293
+ // narrow viewport: the editor panel is 320px wide on a 1440px screen, and
294
+ // `sm:` (viewport-keyed) kept every label on, overflowing the toolbar.
295
+ <div className="@container flex items-center justify-between px-3 py-1.5 bg-paper border-b border-ink/10 text-xs font-sans select-none flex-shrink-0 gap-2 min-w-0">
296
+ {/* Left: File name and status badge. Hidden when the panel is narrow —
297
+ the editor tab already carries the name and the status character, and
298
+ the mobile diff header carries both. */}
299
+ <div className="hidden @[560px]:flex items-center space-x-2 min-w-0 pr-2">
300
+ <FileIcon name={fileName} size={14} className="flex-shrink-0" />
301
+ <span className="font-mono text-ink font-medium truncate" title={filePath}>
302
+ {fileName}
303
+ </span>
304
+
305
+ <span className={`px-1.5 py-0.5 text-[10px] font-mono font-semibold rounded border ${statusInfo.badgeBgClass} flex-shrink-0`}>
306
+ {statusInfo.charStatus} {statusInfo.label}
307
+ </span>
278
308
  </div>
309
+
310
+ {/* Right: Controls & actions. ONE row, never two: what does not fit moves
311
+ into `⋯` (see `ToolbarRow`), because a wrapped strip moved the diff
312
+ down by its own height and read as a second toolbar, while the
313
+ `overflow-x-auto` before that clipped the tail invisibly (measured:
314
+ 361px of content in a 296px box, `Stage` entirely outside it).
315
+ Each segment SETS its own mode — a toggle would flip the other one. */}
316
+ <ToolbarRow clusters={clusters} className="text-ink/70 ml-auto" />
279
317
  </div>
280
318
  );
281
319
  }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * The diff toolbar's two segmented controls.
8
+ *
9
+ * Split out because they are the widest part of the toolbar (a segment pair is
10
+ * 130-190px, and the panel's floor is 320px) and because the pair carries a rule
11
+ * of its own: each segment SETS its mode rather than toggling it. Clicking the
12
+ * already-active segment used to flip the view to the other one, which made the
13
+ * control mean two things depending on which half the reader aimed at.
14
+ *
15
+ * A separate group from each other because the two axes are orthogonal:
16
+ * Unified/Split is the SHAPE of the rows, Full/Diff is how MANY of them there
17
+ * are.
18
+ */
19
+
20
+ import { AlignJustify, Columns, Diff, FileCode } from 'lucide-preact';
21
+ import type { DiffContentMode, DiffViewMode } from '@/client/components/workspace/diff-panel/Toolbar';
22
+
23
+ const SEGMENT_CLASS =
24
+ 'px-2 py-0.5 rounded text-[11px] font-medium flex items-center space-x-1 transition-colors cursor-pointer flex-shrink-0';
25
+
26
+ const GROUP_CLASS = 'flex items-center bg-canvas rounded border border-ink/15 p-0.5 mr-0.5 @[440px]:mr-1';
27
+
28
+ /** Row shape: side-by-side or unified. */
29
+ export function DiffViewModeSegments({
30
+ viewMode,
31
+ onSetViewMode,
32
+ }: {
33
+ viewMode: DiffViewMode;
34
+ onSetViewMode: (mode: DiffViewMode) => void;
35
+ }) {
36
+ return (
37
+ <div className={GROUP_CLASS}>
38
+ <button
39
+ type="button"
40
+ onClick={() => onSetViewMode('split')}
41
+ className={`${SEGMENT_CLASS} ${viewMode === 'split' ? 'bg-paper text-ink shadow-xs' : 'text-ink/60 hover:text-ink'}`}
42
+ title="Split (side-by-side) diff"
43
+ aria-label="Split (side-by-side) diff"
44
+ aria-pressed={viewMode === 'split'}
45
+ >
46
+ <Columns size={12} />
47
+ <span className="hidden @[420px]:inline">Split</span>
48
+ </button>
49
+ <button
50
+ type="button"
51
+ onClick={() => onSetViewMode('unified')}
52
+ className={`${SEGMENT_CLASS} ${viewMode === 'unified' ? 'bg-paper text-ink shadow-xs' : 'text-ink/60 hover:text-ink'}`}
53
+ title="Unified diff"
54
+ aria-label="Unified diff"
55
+ aria-pressed={viewMode === 'unified'}
56
+ >
57
+ <AlignJustify size={12} />
58
+ <span className="hidden @[420px]:inline">Unified</span>
59
+ </button>
60
+ </div>
61
+ );
62
+ }
63
+
64
+ /** How much of the file: the whole thing with changes marked, or the hunks. */
65
+ export function DiffContentModeSegments({
66
+ contentMode,
67
+ onSetContentMode,
68
+ }: {
69
+ contentMode: DiffContentMode;
70
+ onSetContentMode: (mode: DiffContentMode) => void;
71
+ }) {
72
+ return (
73
+ <div className={GROUP_CLASS}>
74
+ <button
75
+ type="button"
76
+ onClick={() => onSetContentMode('full')}
77
+ className={`${SEGMENT_CLASS} ${contentMode === 'full' ? 'bg-paper text-ink shadow-xs' : 'text-ink/60 hover:text-ink'}`}
78
+ title="Full code (whole file, changes marked)"
79
+ aria-label="Full code (whole file, changes marked)"
80
+ aria-pressed={contentMode === 'full'}
81
+ >
82
+ <FileCode size={12} />
83
+ <span className="hidden @[420px]:inline">Full Code</span>
84
+ </button>
85
+ <button
86
+ type="button"
87
+ onClick={() => onSetContentMode('diff')}
88
+ className={`${SEGMENT_CLASS} ${contentMode === 'diff' ? 'bg-paper text-ink shadow-xs' : 'text-ink/60 hover:text-ink'}`}
89
+ title="Diff only (changed hunks)"
90
+ aria-label="Diff only (changed hunks)"
91
+ aria-pressed={contentMode === 'diff'}
92
+ >
93
+ <Diff size={12} />
94
+ <span className="hidden @[420px]:inline">Diff Only</span>
95
+ </button>
96
+ </div>
97
+ );
98
+ }