@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
@@ -9,9 +9,9 @@
9
9
  *
10
10
  * A row's menu (`FileContextMenu`) offers View, Diff, Rename, Delete — all
11
11
  * things a FILE has. The root has none of them: it is the listing itself, and
12
- * the only things you can ask of it are a new file at the top of the tree and a
13
- * paste into it. Reusing the row menu here would have offered six actions that
14
- * name the wrong target.
12
+ * the only things you can ask of it are a new file or folder at the top of the
13
+ * tree and a paste into it. Reusing the row menu here would have offered six
14
+ * actions that name the wrong target.
15
15
  *
16
16
  * Paste is the one row-menu action the root DOES own: pasting into the
17
17
  * background means pasting into the root folder, which is a real destination —
@@ -21,7 +21,7 @@
21
21
 
22
22
  import { createPortal } from 'preact/compat';
23
23
  import type { RefObject } from 'preact/compat';
24
- import { ClipboardPaste, FilePlus } from 'lucide-preact';
24
+ import { ClipboardPaste, FilePlus, FolderPlus } from 'lucide-preact';
25
25
 
26
26
  interface RootContextMenuProps {
27
27
  x: number;
@@ -36,7 +36,8 @@ interface RootContextMenuProps {
36
36
 
37
37
  export function RootContextMenu({ x, y, pasteLabel, busy, onAction, menuRef }: RootContextMenuProps) {
38
38
  const menuWidth = 190;
39
- const menuHeight = pasteLabel ? 76 : 48;
39
+ // Two create rows, plus paste when the clipboard can land here.
40
+ const menuHeight = pasteLabel ? 104 : 76;
40
41
 
41
42
  const posX = typeof window !== 'undefined' ? Math.max(10, Math.min(x, window.innerWidth - menuWidth - 12)) : x;
42
43
  const posY = typeof window !== 'undefined' ? Math.max(10, Math.min(y, window.innerHeight - menuHeight - 12)) : y;
@@ -61,6 +62,14 @@ export function RootContextMenu({ x, y, pasteLabel, busy, onAction, menuRef }: R
61
62
  <FilePlus size={13} className="text-ink/60" />
62
63
  <span>New File</span>
63
64
  </button>
65
+ <button
66
+ type="button"
67
+ onClick={() => onAction('new_folder')}
68
+ className="w-full text-left px-3 py-1.5 hover:bg-ink/5 flex items-center space-x-2 transition-colors"
69
+ >
70
+ <FolderPlus size={13} className="text-ink/60" />
71
+ <span>New Folder</span>
72
+ </button>
64
73
  {pasteLabel && (
65
74
  <button
66
75
  type="button"
@@ -0,0 +1,74 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * The Files panel's header: the repo picker, Refresh and the filter field.
8
+ *
9
+ * Split out of the panel because it is a self-contained strip of markup with no
10
+ * state of its own — the panel's `index.tsx` sits at the repository's line
11
+ * ceiling, and the tree below it is what the file is for.
12
+ */
13
+
14
+ import { RefreshCw, Search } from 'lucide-preact';
15
+ import { GitRepoPicker } from '@/client/components/workspace/repo-picker';
16
+
17
+ export interface FileExplorerToolbarProps {
18
+ rootPath?: string;
19
+ activeRepo: string;
20
+ repos: string[];
21
+ reposScanning: boolean;
22
+ onRefreshRepos: () => void;
23
+ onSelectRepo: (repo: string) => void;
24
+ searchQuery: string;
25
+ onSearchChange: (value: string) => void;
26
+ onRefresh: () => void;
27
+ /** True while a read is in flight or a topic push just landed. */
28
+ refreshing: boolean;
29
+ }
30
+
31
+ export function FileExplorerToolbar({
32
+ rootPath,
33
+ activeRepo,
34
+ repos,
35
+ reposScanning,
36
+ onRefreshRepos,
37
+ onSelectRepo,
38
+ searchQuery,
39
+ onSearchChange,
40
+ onRefresh,
41
+ refreshing,
42
+ }: FileExplorerToolbarProps) {
43
+ return (
44
+ <div className="p-3 border-b border-ink/10 flex flex-col space-y-2">
45
+ <div className="flex items-center justify-between">
46
+ <GitRepoPicker
47
+ rootPath={rootPath}
48
+ activeRepo={activeRepo}
49
+ onSelectRepo={onSelectRepo}
50
+ repos={repos}
51
+ scanning={reposScanning}
52
+ onRefreshRepos={onRefreshRepos}
53
+ />
54
+ <button
55
+ onClick={onRefresh}
56
+ className="p-1.5 text-ink/40 hover:text-ink hover:bg-ink/5 rounded transition-colors"
57
+ title="Refresh"
58
+ >
59
+ <RefreshCw size={14} className={refreshing ? 'animate-spin' : ''} />
60
+ </button>
61
+ </div>
62
+ <div className="relative">
63
+ <Search size={14} className="absolute left-2.5 top-1/2 -translate-y-1/2 text-ink/40" />
64
+ <input
65
+ type="text"
66
+ placeholder="Search files..."
67
+ value={searchQuery}
68
+ onChange={(e) => onSearchChange(e.currentTarget.value)}
69
+ className="w-full bg-canvas border border-ink/20 rounded pl-8 pr-3 py-1.5 text-xs focus:outline-none focus:border-ink transition-colors text-ink placeholder-ink/40"
70
+ />
71
+ </div>
72
+ </div>
73
+ );
74
+ }
@@ -7,6 +7,7 @@ import { getGitStatusInfo, type FolderGitStatusInfo } from '@/shared/lib/fs/git-
7
7
  import { useFileActions } from '@/client/hooks/workspace/file-tree-actions';
8
8
  import { useFileRowDrag } from '@/client/hooks/workspace/file-row-drag';
9
9
  import { useOnClickOutside } from '@/client/hooks/ui/on-click-outside';
10
+ import { useLongPress } from '@/client/hooks/ui/long-press';
10
11
  import type { FileClipboard, ClipboardTarget } from '@/client/hooks/workspace/file-clipboard';
11
12
  import type { GitChange } from '@/shared/types/git';
12
13
 
@@ -18,7 +19,12 @@ interface FileTreeItemProps {
18
19
  repo?: string;
19
20
  onLoadChildren?: (path: string) => Promise<void>;
20
21
  onOpenFile?: (file: any) => void;
22
+ /** Re-read the tree — a DELETE has no local answer. */
21
23
  onActionComplete: () => void;
24
+ /** Place a row this row's menu just created, without a reload. */
25
+ onCreated?: (entry: { path: string; kind: 'file' | 'folder' }) => void;
26
+ /** Move a row this row's menu just renamed, without a reload. */
27
+ onRenamed?: (fromPath: string, toPath: string) => void;
22
28
  expandedPaths?: Set<string>;
23
29
  onToggleFolder?: (path: string, open: boolean) => void;
24
30
  gitFileMap?: Map<string, GitChange>;
@@ -31,6 +37,13 @@ interface FileTreeItemProps {
31
37
  onDragStart: (target: ClipboardTarget) => void;
32
38
  onDragEnd: () => void;
33
39
  onDropOn: (target: ClipboardTarget) => void;
40
+ /**
41
+ * True on a device typed into with a finger. A row is a drag SOURCE, and on
42
+ * touch a long press starts a native drag of the whole panel — the reported
43
+ * "it selects all of the UI" — while no `contextmenu` reaches this row. The
44
+ * native drag is dropped there and the press opens the menu instead.
45
+ */
46
+ touch?: boolean;
34
47
  }
35
48
 
36
49
  /**
@@ -63,6 +76,8 @@ export function FileTreeItem({
63
76
  onLoadChildren,
64
77
  onOpenFile,
65
78
  onActionComplete,
79
+ onCreated,
80
+ onRenamed,
66
81
  expandedPaths,
67
82
  onToggleFolder,
68
83
  gitFileMap,
@@ -72,6 +87,7 @@ export function FileTreeItem({
72
87
  onDragStart,
73
88
  onDragEnd,
74
89
  onDropOn,
90
+ touch = false,
75
91
  }: FileTreeItemProps) {
76
92
  const isFolder = file.type === 'folder';
77
93
  const [isOpen, setIsOpen] = useState(false);
@@ -115,13 +131,23 @@ export function FileTreeItem({
115
131
  diff: { staged: gitChange?.staged || (gitChange?.status ? gitChange.status[0] !== ' ' && gitChange.status[0] !== '?' : false), status: gitStatusInfo?.charStatus },
116
132
  onOpenFile,
117
133
  onActionComplete,
134
+ onCreated,
135
+ onRenamed,
118
136
  clipboard,
119
137
  isFolder,
120
138
  });
121
139
  useOnClickOutside(actions.contextMenuRef, actions.closeContextMenu);
122
140
 
141
+ // A press-and-hold opens the same menu a right-click does; the click the same
142
+ // gesture produces is swallowed, or lifting the finger would toggle the row.
143
+ const longPress = useLongPress({
144
+ enabled: touch,
145
+ onLongPress: (x, y) => actions.openContextMenu(x, y),
146
+ });
147
+
123
148
  const handleToggle = (e: globalThis.MouseEvent) => {
124
149
  e.stopPropagation();
150
+ if (longPress.consume()) return;
125
151
  if (isFolder) {
126
152
  const next = !actualIsOpen;
127
153
  setIsOpen(next);
@@ -175,16 +201,20 @@ export function FileTreeItem({
175
201
  return (
176
202
  <div>
177
203
  <div
178
- className={`flex items-center space-x-1.5 py-1 px-2 hover:bg-ink/5 cursor-pointer rounded group ${
204
+ className={`flex items-center space-x-1.5 py-1 px-2 hover:bg-ink/5 cursor-pointer rounded group select-none ${
179
205
  drag.isDragging ? 'opacity-40' : ''
180
206
  } ${drag.acceptsDrop ? 'bg-info/10 ring-1 ring-info/40 ring-inset' : ''}`}
181
- draggable
207
+ draggable={!touch}
182
208
  onClick={handleToggle}
183
209
  onContextMenu={handleContextMenu}
184
210
  onDragStart={drag.onDragStart}
185
211
  onDragEnd={drag.onDragEnd}
186
212
  onDragOver={drag.onDragOver}
187
213
  onDrop={drag.onDrop}
214
+ onTouchStart={longPress.onTouchStart}
215
+ onTouchMove={longPress.onTouchMove}
216
+ onTouchEnd={longPress.onTouchEnd}
217
+ onTouchCancel={longPress.onTouchCancel}
188
218
  >
189
219
  {isFolder ? (
190
220
  actualIsOpen ? <ChevronDown size={12} className="flex-shrink-0 text-ink/40" /> : <ChevronRight size={12} className="flex-shrink-0 text-ink/40" />
@@ -228,7 +258,7 @@ export function FileTreeItem({
228
258
  <div className="ml-3 border-l border-ink/10 pl-1">
229
259
  {children.map((child: any) => (
230
260
  <FileTreeItem
231
- key={child.id}
261
+ key={child.path}
232
262
  file={child}
233
263
  rootPath={rootPath}
234
264
  basePath={basePath}
@@ -236,6 +266,8 @@ export function FileTreeItem({
236
266
  onLoadChildren={onLoadChildren}
237
267
  onOpenFile={onOpenFile}
238
268
  onActionComplete={onActionComplete}
269
+ onCreated={onCreated}
270
+ onRenamed={onRenamed}
239
271
  expandedPaths={expandedPaths}
240
272
  onToggleFolder={onToggleFolder}
241
273
  gitFileMap={gitFileMap}
@@ -245,6 +277,7 @@ export function FileTreeItem({
245
277
  onDragStart={onDragStart}
246
278
  onDragEnd={onDragEnd}
247
279
  onDropOn={onDropOn}
280
+ touch={touch}
248
281
  />
249
282
  ))}
250
283
  </div>
@@ -265,6 +298,7 @@ export function FileTreeItem({
265
298
  {mounted && actions.showCreateModal && (
266
299
  <FileCreateModal
267
300
  folderPath={file.path}
301
+ kind={actions.createKind}
268
302
  value={actions.createName}
269
303
  error={actions.createError}
270
304
  isLoading={actions.busy}
@@ -289,6 +323,7 @@ export function FileTreeItem({
289
323
  isFolder={isFolder}
290
324
  renameValue={actions.renameValue}
291
325
  originalPath={file.path}
326
+ error={actions.renameError}
292
327
  isLoading={actions.busy}
293
328
  onChange={actions.setRenameValue}
294
329
  onCancel={actions.closeRename}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * The Files panel's filter: keep a node when its own name matches, or when a
8
+ * descendant does.
9
+ *
10
+ * A matching FOLDER keeps its whole subtree (its name is the answer, and hiding
11
+ * its contents would make the match useless), while a folder that only matches
12
+ * through its children keeps just the children that do. Either way the row is
13
+ * forced open, because a match inside a collapsed folder is a match the user
14
+ * cannot see.
15
+ *
16
+ * Extracted from the panel so the render file stays under the line ceiling.
17
+ */
18
+
19
+ import type { FsNode } from '@/shared/types';
20
+
21
+ export function filterFileTree(tree: FsNode[], query: string): FsNode[] {
22
+ const lowerQuery = query.trim().toLowerCase();
23
+ if (!lowerQuery) return tree;
24
+
25
+ const filterNode = (node: FsNode): FsNode | null => {
26
+ const isMatch = node.name.toLowerCase().includes(lowerQuery);
27
+
28
+ if (node.type === 'folder') {
29
+ const filteredChildren = (node.children || [])
30
+ .map(filterNode)
31
+ .filter((child): child is FsNode => child !== null);
32
+
33
+ if (isMatch || filteredChildren.length > 0) {
34
+ return {
35
+ ...node,
36
+ children: isMatch && filteredChildren.length === 0 ? node.children : filteredChildren,
37
+ forceExpanded: true,
38
+ };
39
+ }
40
+ return null;
41
+ }
42
+
43
+ return isMatch ? node : null;
44
+ };
45
+
46
+ return tree.map(filterNode).filter((node): node is FsNode => node !== null);
47
+ }
@@ -1,7 +1,6 @@
1
1
  import { useEffect, useRef, useState } from 'preact/hooks';
2
- import { RefreshCw, Search } from 'lucide-preact';
3
2
  import type { FsNode } from '@/shared/types';
4
- import { GitRepoPicker } from '@/client/components/workspace/repo-picker';
3
+ import { FileExplorerToolbar } from '@/client/components/workspace/file-explorer/Toolbar';
5
4
  import { FileTreeItem } from '@/client/components/workspace/file-explorer/TreeItem';
6
5
  import { RootContextMenu } from '@/client/components/workspace/file-explorer/RootMenu';
7
6
  import { FileCreateModal } from '@/client/components/workspace/file-explorer/Modals';
@@ -11,9 +10,12 @@ import { useGitStatus } from '@/client/hooks/workspace/git-status';
11
10
  import { useFileListing } from '@/client/hooks/workspace/file-listing';
12
11
  import { useFileCreate } from '@/client/hooks/workspace/file-create';
13
12
  import { useFileClipboard, type ClipboardTarget } from '@/client/hooks/workspace/file-clipboard';
13
+ import { rekeyExpansion } from '@/shared/lib/fs/file-tree';
14
+ import { filterFileTree } from '@/client/components/workspace/file-explorer/filter';
14
15
  import { useRepoList, useRepoScope } from '@/client/hooks/workspace/repo-scope';
15
16
  import { useRealtimeTopic } from '@/client/hooks/ui/realtime';
16
17
  import { useToasts } from '@/client/hooks/ui/toasts';
18
+ import { useTouchDevice } from '@/client/hooks/ui/touch-device';
17
19
  import { ToastStack } from '@/client/components/common/ToastStack';
18
20
  import { fsTopic } from '@/shared/lib/realtime/protocol';
19
21
 
@@ -98,6 +100,53 @@ export function FileExplorer({ className = '', enabled = true, rootPath, onOpenF
98
100
  setActiveRepo(repo);
99
101
  };
100
102
 
103
+ /**
104
+ * Open every folder that leads to `path`.
105
+ *
106
+ * A nested create (`lib/parse.ts`) and a rename into a new folder both make
107
+ * directories the user has never opened, and a row placed inside a collapsed
108
+ * folder is a row nobody can see — the write would look like it failed. EVERY
109
+ * ancestor is opened, not only the direct parent: expanding just the last one
110
+ * still hides the entry behind a collapsed `lib`.
111
+ */
112
+ const revealPath = (path: string) => {
113
+ const segments = path.split('/').slice(0, -1);
114
+ if (segments.length === 0) return;
115
+ const chain = segments.map((_, index) => segments.slice(0, index + 1).join('/'));
116
+ setExpandedPaths(prev => {
117
+ if (chain.every((folder) => prev.has(folder))) return prev;
118
+ const next = new Set(prev);
119
+ for (const folder of chain) next.add(folder);
120
+ setStoredExpandedPaths(Array.from(next));
121
+ return next;
122
+ });
123
+ };
124
+
125
+ /** Place a row a create just produced, without re-reading the tree. */
126
+ const insertCreated = (entry: { path: string; kind: 'file' | 'folder' }) => {
127
+ listing.insertEntry(entry);
128
+ revealPath(entry.path);
129
+ };
130
+
131
+ /**
132
+ * Move a row a rename just relocated, without re-reading.
133
+ *
134
+ * A folder's descendants and every open branch beneath it carry its path as a
135
+ * prefix, so the expansion set is rekeyed in the same step — an open `src`
136
+ * renamed to `lib` must stay open as `lib`, not collapse.
137
+ */
138
+ const moveRenamed = (fromPath: string, toPath: string) => {
139
+ listing.moveEntry(fromPath, toPath);
140
+ setExpandedPaths(prev => {
141
+ const next = rekeyExpansion(prev, fromPath, toPath);
142
+ setStoredExpandedPaths(Array.from(next));
143
+ return next;
144
+ });
145
+ // A rename INTO a new folder (`a.ts` → `nested/a.ts`) has the same problem a
146
+ // nested create does: the destination folder is one nobody has opened.
147
+ revealPath(toPath);
148
+ };
149
+
101
150
  /**
102
151
  * Re-read everything this panel shows. Defined ABOVE the `!enabled` return so
103
152
  * the create flow below can use it: a hook cannot be called after a branch.
@@ -111,6 +160,9 @@ export function FileExplorer({ className = '', enabled = true, rootPath, onOpenF
111
160
  // One clipboard for the whole tree, owned here and passed down: a
112
161
  // subscription per row would be hundreds of listeners for a value that
113
162
  // changes on a right-click.
163
+ // A row is a drag source; on a finger the native drag is what turned a long
164
+ // press into a text selection of the whole panel (see `useLongPress`).
165
+ const touch = useTouchDevice();
114
166
  const { toasts, pushToast, dismissToast } = useToasts();
115
167
  const clipboard = useFileClipboard({
116
168
  rootPath,
@@ -155,7 +207,7 @@ export function FileExplorer({ className = '', enabled = true, rootPath, onOpenF
155
207
  folderPath: '.',
156
208
  rootPath,
157
209
  repo: activeRepo,
158
- onCreated: refresh,
210
+ onCreated: insertCreated,
159
211
  onOpenCreated: (file) => onOpenFile?.({ ...file, root: rootPath, repo: activeRepo }),
160
212
  });
161
213
  /** Portals need a real document; the first client render is the gate. */
@@ -172,34 +224,7 @@ export function FileExplorer({ className = '', enabled = true, rootPath, onOpenF
172
224
 
173
225
  const tree = listing.files;
174
226
 
175
- const getFilteredFiles = () => {
176
- if (!searchQuery.trim()) return tree;
177
-
178
- const lowerQuery = searchQuery.toLowerCase();
179
-
180
- const filterNode = (node: any): any => {
181
- const isMatch = node.name.toLowerCase().includes(lowerQuery);
182
-
183
- if (node.type === 'folder') {
184
- const filteredChildren = (node.children || []).map(filterNode).filter(Boolean);
185
-
186
- if (isMatch || filteredChildren.length > 0) {
187
- return {
188
- ...node,
189
- children: isMatch && filteredChildren.length === 0 ? node.children : filteredChildren,
190
- forceExpanded: true
191
- };
192
- }
193
- return null;
194
- }
195
-
196
- return isMatch ? node : null;
197
- };
198
-
199
- return tree.map(filterNode).filter(Boolean);
200
- };
201
-
202
- const files = getFilteredFiles();
227
+ const files = filterFileTree(tree, searchQuery);
203
228
  // The button spins for this panel's own read OR a topic push: the server
204
229
  // republishes `fs:` on every tool call, so the tree moved with nothing on
205
230
  // screen saying so otherwise.
@@ -207,40 +232,23 @@ export function FileExplorer({ className = '', enabled = true, rootPath, onOpenF
207
232
 
208
233
  return (
209
234
  <div className={`flex flex-col h-full bg-paper ${className}`}>
210
- <div className="p-3 border-b border-ink/10 flex flex-col space-y-2">
211
- <div className="flex items-center justify-between">
212
- <GitRepoPicker
213
- rootPath={rootPath}
214
- activeRepo={activeRepo}
215
- onSelectRepo={handleSelectRepo}
216
- repos={repos}
217
- scanning={reposScanning}
218
- onRefreshRepos={rescanRepos}
219
- />
220
- <button
221
- onClick={refresh}
222
- className="p-1.5 text-ink/40 hover:text-ink hover:bg-ink/5 rounded transition-colors"
223
- title="Refresh"
224
- >
225
- <RefreshCw size={14} className={isRefreshing ? 'animate-spin' : ''} />
226
- </button>
227
- </div>
228
- <div className="relative">
229
- <Search size={14} className="absolute left-2.5 top-1/2 -translate-y-1/2 text-ink/40" />
230
- <input
231
- type="text"
232
- placeholder="Search files..."
233
- value={searchQuery}
234
- onChange={(e) => setSearchQuery(e.currentTarget.value)}
235
- className="w-full bg-canvas border border-ink/20 rounded pl-8 pr-3 py-1.5 text-xs focus:outline-none focus:border-ink transition-colors text-ink placeholder-ink/40"
236
- />
237
- </div>
238
- </div>
235
+ <FileExplorerToolbar
236
+ rootPath={rootPath}
237
+ activeRepo={activeRepo}
238
+ repos={repos}
239
+ reposScanning={reposScanning}
240
+ onRefreshRepos={rescanRepos}
241
+ onSelectRepo={handleSelectRepo}
242
+ searchQuery={searchQuery}
243
+ onSearchChange={setSearchQuery}
244
+ onRefresh={refresh}
245
+ refreshing={isRefreshing}
246
+ />
239
247
 
240
248
  <div
241
249
  className={`flex-1 scrollbar-overlay-container p-2 font-mono text-[11px] text-ink/80 ${
242
- rootAcceptsDrop ? 'bg-info/[0.06]' : ''
243
- }`}
250
+ touch ? 'file-tree-touch' : ''
251
+ } ${rootAcceptsDrop ? 'bg-info/[0.06]' : ''}`}
244
252
  onContextMenu={(e) => {
245
253
  // The panel's own background. A row stops propagation before this
246
254
  // runs, so this handler only ever sees a right-click that named NO
@@ -273,7 +281,7 @@ export function FileExplorer({ className = '', enabled = true, rootPath, onOpenF
273
281
  ) : (
274
282
  files.map(file => (
275
283
  <FileTreeItem
276
- key={file.id}
284
+ key={file.path}
277
285
  file={file}
278
286
  rootPath={rootPath}
279
287
  basePath={listing.root}
@@ -281,6 +289,8 @@ export function FileExplorer({ className = '', enabled = true, rootPath, onOpenF
281
289
  onLoadChildren={loadChildren}
282
290
  onOpenFile={onOpenFile}
283
291
  onActionComplete={refresh}
292
+ onCreated={insertCreated}
293
+ onRenamed={moveRenamed}
284
294
  expandedPaths={expandedPaths}
285
295
  onToggleFolder={handleToggleFolder}
286
296
  gitFileMap={gitFileMap}
@@ -293,6 +303,7 @@ export function FileExplorer({ className = '', enabled = true, rootPath, onOpenF
293
303
  if (!dragging) return;
294
304
  clipboard.move(dragging, target);
295
305
  }}
306
+ touch={touch}
296
307
  />
297
308
  ))
298
309
  )}
@@ -312,7 +323,8 @@ export function FileExplorer({ className = '', enabled = true, rootPath, onOpenF
312
323
  busy={clipboard.busy}
313
324
  onAction={(action) => {
314
325
  setRootMenu(null);
315
- if (action === 'new_file') rootCreate.openDialog();
326
+ if (action === 'new_file') rootCreate.openDialog('file');
327
+ if (action === 'new_folder') rootCreate.openDialog('folder');
316
328
  if (action === 'paste') clipboard.paste(rootTarget);
317
329
  }}
318
330
  />
@@ -321,6 +333,7 @@ export function FileExplorer({ className = '', enabled = true, rootPath, onOpenF
321
333
  {mounted && rootCreate.open && (
322
334
  <FileCreateModal
323
335
  folderPath="."
336
+ kind={rootCreate.kind}
324
337
  value={rootCreate.name}
325
338
  error={rootCreate.error}
326
339
  isLoading={rootCreate.busy}
@@ -0,0 +1,135 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * The Files panel's local tree edits: a create and a rename land WITHOUT a
8
+ * reload.
9
+ *
10
+ * Both used to answer a mutation by re-reading the root plus every open folder,
11
+ * which is one request per open folder to learn something the response already
12
+ * names — and it also collapsed nothing but cost a visible spinner. The two
13
+ * specs here assert the two halves that matter: the row appears (or moves) in
14
+ * the rendered tree, and NO `/api/fs/dir` read is issued for the root to make
15
+ * that happen.
16
+ *
17
+ * A nested create is the third case, because it is the one a naive insert gets
18
+ * wrong: the folders leading to the entry do not exist in the tree yet, and a
19
+ * row placed under a folder nobody has opened is a row the user cannot see —
20
+ * so the create also opens the chain it created.
21
+ */
22
+
23
+ import { afterAll, afterEach, beforeAll, describe, expect, test } from 'bun:test';
24
+ import { createFileExplorerHarness } from '@/test-support/file-explorer.test-util';
25
+
26
+ const panel = createFileExplorerHarness();
27
+
28
+ beforeAll(() => {
29
+ panel.install();
30
+ });
31
+
32
+ afterEach(() => {
33
+ panel.unmount();
34
+ });
35
+
36
+ afterAll(() => {
37
+ panel.restore();
38
+ });
39
+
40
+ /** The root's own reads, which a reload would add one of. */
41
+ function rootReads(): number {
42
+ return panel.dirReads.filter((path) => path === '.').length;
43
+ }
44
+
45
+ describe('Files panel local edits', () => {
46
+ test('a created file appears in the tree without re-reading the root', async () => {
47
+ panel.disk = { '.': { 'existing.ts': 'file' } };
48
+ panel.dirReads = [];
49
+ await panel.mount();
50
+ const before = rootReads();
51
+
52
+ await panel.openRootNewFile();
53
+ await panel.submitNewFile('fresh.ts');
54
+
55
+ expect(panel.buttonLabels()).not.toContain('Create');
56
+ expect(panel.visibleNames()).toContain('fresh.ts');
57
+ // The create answered with the path, so the tree placed it: a reload here
58
+ // would be a root read plus one per open folder.
59
+ expect(rootReads()).toBe(before);
60
+ });
61
+
62
+ test('a created FOLDER appears and is not opened in an editor', async () => {
63
+ panel.disk = { '.': {} };
64
+ panel.dirReads = [];
65
+ await panel.mount();
66
+ const before = rootReads();
67
+
68
+ await panel.openRootNewFolder();
69
+ await panel.submitNewFile('packages');
70
+
71
+ expect(panel.createRequests.at(-1)).toEqual({
72
+ actionType: 'create',
73
+ path: '.',
74
+ name: 'packages',
75
+ kind: 'folder',
76
+ root: '/ws',
77
+ });
78
+ expect(panel.visibleNames()).toContain('packages');
79
+ // A folder has nothing to open — opening one would be an empty editor tab.
80
+ expect(panel.openedFiles).toEqual([]);
81
+ expect(rootReads()).toBe(before);
82
+ });
83
+
84
+ test('a nested create opens the chain it created, so the row is visible', async () => {
85
+ panel.disk = { '.': {} };
86
+ panel.dirReads = [];
87
+ await panel.mount();
88
+
89
+ await panel.openRootNewFile();
90
+ await panel.submitNewFile('lib/deep/parse.ts');
91
+
92
+ // Every folder on the way is expanded and reads its own directory: the
93
+ // entry is not somewhere the user has to go hunting for.
94
+ expect(panel.visibleNames()).toContain('parse.ts');
95
+ expect(panel.visibleNames()).toContain('deep');
96
+ expect(panel.visibleNames()).toContain('lib');
97
+ });
98
+
99
+ test('a rename moves the row in place without re-reading the root', async () => {
100
+ panel.disk = { '.': { 'old.ts': 'file' } };
101
+ panel.dirReads = [];
102
+ await panel.mount();
103
+ const before = rootReads();
104
+
105
+ await panel.rightClickRow('old.ts');
106
+ await panel.clickMenuRow('Rename');
107
+ await panel.submitRename('new.ts');
108
+
109
+ expect(panel.renameRequests.at(-1)).toEqual({
110
+ actionType: 'rename',
111
+ path: 'old.ts',
112
+ newPath: 'new.ts',
113
+ root: '/ws',
114
+ });
115
+ expect(panel.visibleNames()).toContain('new.ts');
116
+ expect(panel.visibleNames()).not.toContain('old.ts');
117
+ expect(rootReads()).toBe(before);
118
+ });
119
+
120
+ test('a refused rename keeps the dialog open with the server’s reason', async () => {
121
+ panel.disk = { '.': { 'old.ts': 'file', 'taken.ts': 'file' } };
122
+ panel.dirReads = [];
123
+ await panel.mount();
124
+
125
+ await panel.rightClickRow('old.ts');
126
+ await panel.clickMenuRow('Rename');
127
+ await panel.submitRename('taken.ts');
128
+
129
+ // The field is still there: "already exists" is a message about what to
130
+ // type next, not a notification about something that happened.
131
+ expect(panel.renameInput()).not.toBeNull();
132
+ expect(panel.documentText()).toContain('taken.ts already exists');
133
+ expect(panel.visibleNames()).toContain('old.ts');
134
+ });
135
+ });
@@ -6,7 +6,7 @@ import { SearchResults } from '@/client/components/workspace/search-panel/Result
6
6
  import { setSearchReveal } from '@/client/lib/search-reveal';
7
7
  import { groupSearchResults, MAX_SEARCH_RESULTS } from '@/shared/lib/fs/search-list';
8
8
  import type { SearchResultItem } from '@/shared/types/fs';
9
- import type { FindOptions } from '@/shared/lib/code/editor/find';
9
+ import type { FindOptions } from '@/shared/lib/code/editor/find-scan';
10
10
 
11
11
  interface SearchPanelBodyProps {
12
12
  className: string;