@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,4 +1,20 @@
1
- import { AlertCircle, Check, Copy, Download, Eye, EyeOff, Maximize, Minimize, Save, Search, WrapText, ZoomIn, ZoomOut } from 'lucide-preact';
1
+ import {
2
+ AlertCircle,
3
+ Check,
4
+ Copy,
5
+ Download,
6
+ Eye,
7
+ EyeOff,
8
+ Maximize,
9
+ Minimize,
10
+ Save,
11
+ Search,
12
+ WrapText,
13
+ ZoomIn,
14
+ ZoomOut,
15
+ } from 'lucide-preact';
16
+ import { EditorViewButtons, useEditorViewActions } from '@/client/components/common/editor-view-buttons';
17
+ import { ToolbarRow, type ToolbarCluster } from '@/client/components/common/toolbar-row';
2
18
 
3
19
  export type EditorSaveStatus = 'idle' | 'saving' | 'saved' | 'error' | 'needs-permission';
4
20
 
@@ -94,146 +110,227 @@ export function EditorToolbar({
94
110
  ? 'Save failed — the file on disk was not changed'
95
111
  : 'Save file (Ctrl+S)';
96
112
 
97
- return (
98
- // `@container`: the path below hides on a NARROW PANEL, not a narrow
99
- // viewport. At the editor panel's 320px floor the action strip is 273px of
100
- // `flex-shrink-0`, which left the path a measured 23px — a truncated
101
- // fragment of a filename is not information, and the tab above already
102
- // carries it.
103
- <div className="@container flex items-center justify-between px-3 py-2 border-b border-ink/10 bg-paper flex-shrink-0">
104
- {/* Left status */}
105
- <div className="hidden @[420px]:flex items-center space-x-2 text-xs font-mono text-ink/40 min-w-0">
106
- <span className="truncate max-w-[300px]" title={path}>{path}</span>
107
- </div>
113
+ const saveButton = (
114
+ <button
115
+ type="button"
116
+ onClick={onSave}
117
+ disabled={saveDisabled || saveStatus === 'saving'}
118
+ className={`${ACTION_CLASS} hover:bg-ink/5`}
119
+ title={saveTitle}
120
+ aria-label={saveTitle}
121
+ >
122
+ {saveStatus === 'saving' ? (
123
+ <Save size={14} className="text-amber-500 animate-pulse" />
124
+ ) : saveStatus === 'saved' ? (
125
+ <Check size={14} className="text-success" />
126
+ ) : saveStatus === 'error' ? (
127
+ <AlertCircle size={14} className="text-error" />
128
+ ) : saveStatus === 'needs-permission' ? (
129
+ // Not an error: the buffer is fine and one click grants access.
130
+ <Save size={14} className="text-warning" />
131
+ ) : (
132
+ <Save size={14} className="hover:text-ink" />
133
+ )}
134
+ </button>
135
+ );
108
136
 
109
- {/* Right actions. `ml-auto` keeps them right-aligned once the path block
110
- above is hidden, which `justify-between` alone would not do (a single
111
- child sits at the start). */}
112
- <div className="flex items-center space-x-1 text-ink/40 flex-shrink-0 ml-auto">
113
- {!byteViewer && (
114
- <div className="flex items-center pr-2 mr-1 border-r border-ink/10">
115
- <button
116
- type="button"
117
- onClick={onSave}
118
- disabled={saveDisabled || saveStatus === 'saving'}
119
- className={`${ACTION_CLASS} hover:bg-ink/5`}
120
- title={saveTitle}
121
- aria-label={saveTitle}
122
- >
123
- {saveStatus === 'saving' ? (
124
- <Save size={14} className="text-amber-500 animate-pulse" />
125
- ) : saveStatus === 'saved' ? (
126
- <Check size={14} className="text-success" />
127
- ) : saveStatus === 'error' ? (
128
- <AlertCircle size={14} className="text-error" />
129
- ) : saveStatus === 'needs-permission' ? (
130
- // Not an error: the buffer is fine and one click grants access.
131
- <Save size={14} className="text-warning" />
132
- ) : (
133
- <Save size={14} className="hover:text-ink" />
134
- )}
135
- </button>
136
- </div>
137
- )}
137
+ const copyLabel = copied ? 'Copied' : byteViewer ? 'Copy image' : 'Copy content';
138
+ const copyButton = (
139
+ <button
140
+ type="button"
141
+ onClick={onCopy}
142
+ className={`${ACTION_CLASS} hover:bg-ink/5 hover:text-ink`}
143
+ title={copyLabel}
144
+ aria-label={copyLabel}
145
+ >
146
+ {copied ? <Check size={14} className="text-success" /> : <Copy size={14} />}
147
+ </button>
148
+ );
138
149
 
139
- {hasPreview && (
140
- <div className="flex items-center pr-2 mr-1 border-r border-ink/10">
141
- <button
142
- type="button"
143
- onClick={onTogglePreview}
144
- className={`${ACTION_CLASS} hover:bg-ink/5 ${isPreview ? 'text-ink' : 'hover:text-ink'}`}
145
- title={isPreview ? 'Show source' : 'Show preview'}
146
- aria-label={isPreview ? 'Show source' : 'Show preview'}
147
- aria-pressed={isPreview}
148
- >
149
- {isPreview ? <EyeOff size={14} /> : <Eye size={14} />}
150
- </button>
151
- </div>
152
- )}
150
+ const downloadButton = (
151
+ <button
152
+ type="button"
153
+ onClick={onDownload}
154
+ className={`${ACTION_CLASS} hover:bg-ink/5 hover:text-ink`}
155
+ title="Download file"
156
+ aria-label="Download file"
157
+ >
158
+ <Download size={14} />
159
+ </button>
160
+ );
153
161
 
154
- {!byteViewer && (
155
- <>
156
- <div className="flex items-center pr-2 mr-1 border-r border-ink/10">
157
- <button
158
- type="button"
159
- onClick={onToggleFind}
160
- className={`${ACTION_CLASS} hover:bg-ink/5 ${findOpen ? 'text-ink' : 'hover:text-ink'}`}
161
- title="Find and replace (Ctrl+F)"
162
- aria-label="Find and replace"
163
- aria-pressed={findOpen}
164
- >
165
- <Search size={14} />
166
- </button>
167
- </div>
162
+ const maximizeLabel = isMaximized ? 'Exit full screen' : 'Full screen';
163
+ const maximizeButton = (
164
+ <button
165
+ type="button"
166
+ onClick={onToggleMaximize}
167
+ className={`${ACTION_CLASS} hover:bg-ink/5 hover:text-ink ${isMaximized ? 'text-ink' : ''}`}
168
+ title={maximizeLabel}
169
+ aria-label={maximizeLabel}
170
+ aria-pressed={isMaximized}
171
+ >
172
+ {isMaximized ? <Minimize size={14} /> : <Maximize size={14} />}
173
+ </button>
174
+ );
168
175
 
169
- <div className="flex items-center pr-2 mr-1 border-r border-ink/10">
170
- <button
171
- type="button"
172
- onClick={onToggleWordWrap}
173
- className={`${ACTION_CLASS} hover:bg-ink/5 ${wordWrap ? 'text-ink' : 'hover:text-ink'}`}
174
- title={wordWrap ? 'Word wrap on' : 'Word wrap off'}
175
- aria-label={wordWrap ? 'Word wrap on' : 'Word wrap off'}
176
- aria-pressed={wordWrap}
177
- >
178
- <WrapText size={14} />
179
- </button>
180
- </div>
176
+ const viewModeActions = useEditorViewActions();
177
+ const findLabel = 'Find and replace';
178
+ const wordWrapLabel = wordWrap ? 'Word wrap on' : 'Word wrap off';
179
+ const previewLabel = isPreview ? 'Show source' : 'Show preview';
181
180
 
182
- <div className="flex items-center pr-2 mr-1 border-r border-ink/10">
183
- <button
184
- type="button"
185
- onClick={onZoomOut}
186
- className={`${ACTION_CLASS} hover:bg-ink/5 hover:text-ink`}
187
- title="Decrease font size"
188
- aria-label="Decrease font size"
189
- >
190
- <ZoomOut size={14} />
191
- </button>
192
- <button
193
- type="button"
194
- onClick={onZoomIn}
195
- className={`${ACTION_CLASS} hover:bg-ink/5 hover:text-ink`}
196
- title="Increase font size"
197
- aria-label="Increase font size"
198
- >
199
- <ZoomIn size={14} />
200
- </button>
201
- </div>
202
- </>
203
- )}
181
+ /**
182
+ * The controls, as clusters that hide as a unit.
183
+ *
184
+ * ORDER is the hiding order, most important first: the row keeps the width
185
+ * modes and full screen (the controls this panel is FOR), then copy/download,
186
+ * and the reading aids (find, wrap, zoom, preview) move into the `⋯` menu on a
187
+ * narrow panel. Grouping is the existing one — zoom is a pair, copy/download is
188
+ * a pair, the modes are a pair — because splitting a pair leaves half of it on
189
+ * the row.
190
+ */
191
+ const clusters: ToolbarCluster[] = [];
204
192
 
205
- <div className="flex items-center pr-2 mr-1 border-r border-ink/10">
193
+ if (!byteViewer) {
194
+ clusters.push({
195
+ id: 'save',
196
+ // The primary action of the panel: a menu row must never be the only way
197
+ // to save a file.
198
+ pinned: true,
199
+ actions: [{ label: 'Save file', icon: Save, onSelect: onSave, disabled: saveDisabled }],
200
+ children: saveButton,
201
+ });
202
+ }
203
+ if (hasPreview) {
204
+ clusters.push({
205
+ id: 'preview',
206
+ actions: [{ label: previewLabel, icon: isPreview ? EyeOff : Eye, onSelect: onTogglePreview, active: isPreview }],
207
+ children: (
208
+ <button
209
+ type="button"
210
+ onClick={onTogglePreview}
211
+ className={`${ACTION_CLASS} hover:bg-ink/5 ${isPreview ? 'text-ink' : 'hover:text-ink'}`}
212
+ title={previewLabel}
213
+ aria-label={previewLabel}
214
+ aria-pressed={isPreview}
215
+ >
216
+ {isPreview ? <EyeOff size={14} /> : <Eye size={14} />}
217
+ </button>
218
+ ),
219
+ });
220
+ }
221
+ if (!byteViewer) {
222
+ clusters.push({
223
+ id: 'find',
224
+ actions: [{ label: findLabel, icon: Search, onSelect: onToggleFind, active: findOpen }],
225
+ children: (
226
+ <button
227
+ type="button"
228
+ onClick={onToggleFind}
229
+ className={`${ACTION_CLASS} hover:bg-ink/5 ${findOpen ? 'text-ink' : 'hover:text-ink'}`}
230
+ title={`${findLabel} (Ctrl+F)`}
231
+ aria-label={findLabel}
232
+ aria-pressed={findOpen}
233
+ >
234
+ <Search size={14} />
235
+ </button>
236
+ ),
237
+ });
238
+ clusters.push({
239
+ id: 'wrap',
240
+ actions: [{ label: wordWrapLabel, icon: WrapText, onSelect: onToggleWordWrap, active: wordWrap }],
241
+ children: (
242
+ <button
243
+ type="button"
244
+ onClick={onToggleWordWrap}
245
+ className={`${ACTION_CLASS} hover:bg-ink/5 ${wordWrap ? 'text-ink' : 'hover:text-ink'}`}
246
+ title={wordWrapLabel}
247
+ aria-label={wordWrapLabel}
248
+ aria-pressed={wordWrap}
249
+ >
250
+ <WrapText size={14} />
251
+ </button>
252
+ ),
253
+ });
254
+ clusters.push({
255
+ id: 'zoom',
256
+ actions: [
257
+ { label: 'Decrease font size', icon: ZoomOut, onSelect: onZoomOut },
258
+ { label: 'Increase font size', icon: ZoomIn, onSelect: onZoomIn },
259
+ ],
260
+ children: (
261
+ <>
206
262
  <button
207
263
  type="button"
208
- onClick={onCopy}
264
+ onClick={onZoomOut}
209
265
  className={`${ACTION_CLASS} hover:bg-ink/5 hover:text-ink`}
210
- title={copied ? 'Copied' : byteViewer ? 'Copy image' : 'Copy content'}
211
- aria-label={copied ? 'Copied' : byteViewer ? 'Copy image' : 'Copy content'}
266
+ title="Decrease font size"
267
+ aria-label="Decrease font size"
212
268
  >
213
- {copied ? <Check size={14} className="text-success" /> : <Copy size={14} />}
269
+ <ZoomOut size={14} />
214
270
  </button>
215
271
  <button
216
272
  type="button"
217
- onClick={onDownload}
273
+ onClick={onZoomIn}
218
274
  className={`${ACTION_CLASS} hover:bg-ink/5 hover:text-ink`}
219
- title="Download file"
220
- aria-label="Download file"
275
+ title="Increase font size"
276
+ aria-label="Increase font size"
221
277
  >
222
- <Download size={14} />
278
+ <ZoomIn size={14} />
223
279
  </button>
224
- </div>
280
+ </>
281
+ ),
282
+ });
283
+ }
225
284
 
226
- <button
227
- type="button"
228
- onClick={onToggleMaximize}
229
- className={`${ACTION_CLASS} hover:bg-ink/5 hover:text-ink ${isMaximized ? 'text-ink' : ''}`}
230
- title={isMaximized ? 'Exit full screen' : 'Full screen'}
231
- aria-label={isMaximized ? 'Exit full screen' : 'Full screen'}
232
- aria-pressed={isMaximized}
233
- >
234
- {isMaximized ? <Minimize size={14} /> : <Maximize size={14} />}
235
- </button>
285
+ clusters.push({
286
+ id: 'copy',
287
+ actions: [
288
+ { label: copyLabel, icon: Copy, onSelect: onCopy },
289
+ { label: 'Download file', icon: Download, onSelect: onDownload },
290
+ ],
291
+ children: (
292
+ <>
293
+ {copyButton}
294
+ {downloadButton}
295
+ </>
296
+ ),
297
+ });
298
+
299
+ // The width modes and full SCREEN are the controls this panel exists for, so
300
+ // they are the LAST clusters — the tail is what the menu takes. Three controls
301
+ // about one axis, three different glyphs: compact collapses the chat (a
302
+ // crossed-out message), full collapses the chat and the sidebar (a closing
303
+ // panel edge), and full screen covers the right panel too (a maximise frame).
304
+ clusters.push({
305
+ id: 'view-modes',
306
+ // The two width modes are why this toolbar grew these buttons, and a mode
307
+ // reachable only from a menu is a mode half the readers never find.
308
+ pinned: true,
309
+ actions: viewModeActions,
310
+ children: <EditorViewButtons size={14} />,
311
+ });
312
+ clusters.push({
313
+ id: 'maximize',
314
+ pinned: true,
315
+ actions: [{ label: maximizeLabel, icon: isMaximized ? Minimize : Maximize, onSelect: onToggleMaximize, active: isMaximized }],
316
+ children: maximizeButton,
317
+ });
318
+
319
+ return (
320
+ // `@container`: the path below hides on a NARROW PANEL, not a narrow
321
+ // viewport. At the editor panel's 320px floor the path would be a truncated
322
+ // fragment of a filename — not information, and the tab above already
323
+ // carries it.
324
+ <div className="@container flex items-center justify-between gap-2 px-3 py-2 border-b border-ink/10 bg-paper flex-shrink-0">
325
+ {/* Left status */}
326
+ <div className="hidden @[420px]:flex items-center space-x-2 text-xs font-mono text-ink/40 min-w-0">
327
+ <span className="truncate max-w-[300px]" title={path}>{path}</span>
236
328
  </div>
329
+
330
+ {/* One row, never two: what does not fit moves into `⋯` (see
331
+ `ToolbarRow`). A wrapped toolbar pushed the document down by its own
332
+ height and read as a second toolbar. */}
333
+ <ToolbarRow clusters={clusters} className="text-ink/40 ml-auto" />
237
334
  </div>
238
335
  );
239
336
  }
@@ -5,7 +5,7 @@ import { EditorTabs } from '@/client/components/workspace/editor/Tabs';
5
5
  import { EDITOR_KEY_BINDINGS, isFindBarCommand, type EditorCommand } from '@/shared/lib/code/editor/keymap';
6
6
  import { EDITOR_DEFAULT_FONT_SIZE } from '@/shared/lib/code/editor/typography';
7
7
  import { useEditorFontStack } from '@/client/hooks/editor/font';
8
- import type { TextRange } from '@/shared/lib/code/editor/commands';
8
+ import { useEditorTheme } from '@/client/hooks/editor/theme';
9
9
  import type { CodeSurfaceHandle } from '@/client/components/common/code-surface';
10
10
  import { resolveBinding } from '@/shared/lib/ui/key-binding';
11
11
  import { useActiveEditorPanel } from '@/client/components/workspace/plugin-panel/resolve';
@@ -62,12 +62,11 @@ export function Editor({
62
62
  const [zoomLevel, setZoomLevel] = useSessionState<number>('editor.zoomLevel', EDITOR_DEFAULT_FONT_SIZE);
63
63
  const [isMaximized, setIsMaximized] = useState(false);
64
64
  const [wordWrap, setWordWrap] = useSessionState<boolean>('editor.wordWrap', true);
65
- /** Extra ⌘D ranges; the surface paints them and the editor edits them by replication. */
66
- const [occurrences, setOccurrences] = useState<readonly TextRange[]>([]);
67
65
  const [paletteOpen, setPaletteOpen] = useState(false);
68
66
  const { sessionId } = useSessionStateContext();
69
67
  const surfaceRef = useRef<CodeSurfaceHandle | null>(null);
70
68
  const editorFontFamily = useEditorFontStack();
69
+ const editorTheme = useEditorTheme();
71
70
 
72
71
  // A deleted file keeps its tab (its buffer is still on screen), but the tab
73
72
  // strip must say so. Only a plain FILE tab is probed: a diff tab's path names
@@ -216,10 +215,10 @@ export function Editor({
216
215
  * Editor shortcuts, on the CAPTURE phase of the panel.
217
216
  *
218
217
  * Capture is what makes them work while focus is inside the document: the
219
- * `<textarea>` has its own handler (Tab, undo, Escape-to-blur) and would
220
- * otherwise see these first. Only the commands the PANEL owns are consumed
221
- * here — the editor's own keymap runs in the textarea, where it has the
222
- * buffer and the caret in hand.
218
+ * contenteditable has CodeMirror's own handlers (the generated keymap, and
219
+ * `defaultKeymap` behind it) and would otherwise see these first. Only the
220
+ * commands the PANEL owns are consumed here — the editor's own keymap runs in
221
+ * the document, where it has the buffer and the caret in hand.
223
222
  *
224
223
  * The panel is the scope, not the window: another panel's text field keeps
225
224
  * its own ⌘F, and the composer is untouched.
@@ -312,12 +311,11 @@ export function Editor({
312
311
  setIsMaximized={setIsMaximized}
313
312
  zoomLevel={zoomLevel}
314
313
  setZoomLevel={setZoomLevel}
315
- occurrences={occurrences}
316
- setOccurrences={setOccurrences}
317
314
  paletteOpen={paletteOpen}
318
315
  setPaletteOpen={setPaletteOpen}
319
316
  runCommand={runCommand}
320
317
  editorFontFamily={editorFontFamily}
318
+ editorTheme={editorTheme}
321
319
  togglePreview={togglePreview}
322
320
  revealActive={Boolean(searchReveal)}
323
321
  missing={activeFileMissing}
@@ -29,7 +29,7 @@
29
29
 
30
30
  import { createPortal } from 'preact/compat';
31
31
  import type { ReactNode, RefObject } from 'preact/compat';
32
- import { ClipboardPaste, Copy, CopyPlus, Edit2, ExternalLink, Eye, FilePlus, GitCompare, History, Scissors, Trash2 } from 'lucide-preact';
32
+ import { ClipboardPaste, Copy, CopyPlus, Edit2, ExternalLink, Eye, FilePlus, FolderPlus, GitCompare, History, Scissors, Trash2 } from 'lucide-preact';
33
33
 
34
34
  interface ContextMenuProps {
35
35
  x: number;
@@ -61,6 +61,7 @@ function buildRows(options: { isFolder: boolean; hasGitStatus?: boolean; pasteLa
61
61
 
62
62
  if (isFolder) {
63
63
  entries.push({ kind: 'item', id: 'new_file', label: 'New File', icon: <FilePlus size={13} className="text-ink/60" /> });
64
+ entries.push({ kind: 'item', id: 'new_folder', label: 'New Folder', icon: <FolderPlus size={13} className="text-ink/60" /> });
64
65
  entries.push({ kind: 'separator' });
65
66
  } else {
66
67
  entries.push({ kind: 'item', id: 'view', label: 'View', icon: <Eye size={13} className="text-ink/60" /> });
@@ -4,10 +4,12 @@ import { History, RefreshCw, X } from 'lucide-preact';
4
4
  import type { FsActionResponse } from '@/shared/types';
5
5
 
6
6
  interface NewFileModalProps {
7
- /** Folder the file will be created in, relative to the listed root. */
7
+ /** Folder the entry will be created in, relative to the listed root. */
8
8
  folderPath: string;
9
+ /** What the dialog creates — a file or a folder. */
10
+ kind: 'file' | 'folder';
9
11
  value: string;
10
- /** Server's refusal (a name that already exists, a bad character), shown inline. */
12
+ /** Server's refusal (a name that already exists, a bad path), shown inline. */
11
13
  error?: string | null;
12
14
  isLoading: boolean;
13
15
  onChange: (value: string) => void;
@@ -16,21 +18,26 @@ interface NewFileModalProps {
16
18
  }
17
19
 
18
20
  /**
19
- * The "New File" dialog.
21
+ * The "New File" / "New Folder" dialog.
22
+ *
23
+ * The folder is the one that was right-clicked, shown as a fixed prefix so the
24
+ * field's meaning is unambiguous — and the name may carry SEGMENTS
25
+ * (`lib/parse.ts`, `src/nested/x.ts`), which creates every directory on the way
26
+ * in one step. The grammar that keeps that inside the named folder is the
27
+ * server's, so the field does not restate it; it only says what it accepts.
20
28
  *
21
- * Only a name is asked for — the folder is the one that was right-clicked, and
22
- * showing it as a fixed prefix is what makes the field's meaning unambiguous.
23
29
  * The input starts empty (no `.txt` default): a name is what the user has in
24
30
  * mind, and a prefilled suffix is a character to delete before every keystroke
25
31
  * lands. The server's refusal is rendered INLINE rather than as a toast: the
26
32
  * field is still open, and "already exists" is a message about what to type
27
33
  * next, not a notification about something that happened.
28
34
  */
29
- export function FileCreateModal({ folderPath, value, error, isLoading, onChange, onCancel, onSubmit }: NewFileModalProps) {
35
+ export function FileCreateModal({ folderPath, kind, value, error, isLoading, onChange, onCancel, onSubmit }: NewFileModalProps) {
36
+ const label = kind === 'folder' ? 'Folder' : 'File';
30
37
  return createPortal(
31
38
  <div className="fixed inset-0 bg-ink/20 z-[100] flex items-center justify-center p-4 backdrop-blur-sm">
32
39
  <form onSubmit={onSubmit} className="bg-paper rounded shadow-xl p-4 w-full max-w-sm border border-ink/10 font-sans">
33
- <h3 className="font-semibold text-ink mb-3 text-sm">New File</h3>
40
+ <h3 className="font-semibold text-ink mb-3 text-sm">New {label}</h3>
34
41
  <div className="flex items-center gap-1 mb-1 text-xs font-mono text-ink/50 min-w-0">
35
42
  <span className="truncate" title={folderPath}>{folderPath || '.'}/</span>
36
43
  </div>
@@ -39,12 +46,15 @@ export function FileCreateModal({ folderPath, value, error, isLoading, onChange,
39
46
  type="text"
40
47
  value={value}
41
48
  onChange={e => onChange(e.currentTarget.value)}
42
- placeholder="file name"
43
- aria-label="New file name"
44
- className={`w-full bg-paper border rounded px-2 py-1.5 text-xs font-mono focus:outline-none mb-2 ${
49
+ placeholder={kind === 'folder' ? 'folder name or nested/path' : 'file name or nested/path'}
50
+ aria-label={`New ${label.toLowerCase()} name`}
51
+ className={`w-full bg-paper border rounded px-2 py-1.5 text-xs font-mono focus:outline-none mb-1 ${
45
52
  error ? 'border-error' : 'border-ink/20 focus:border-ink'
46
53
  }`}
47
54
  />
55
+ <p className="text-[10px] text-ink/40 mb-2">
56
+ Nested paths create the folders they need — <span className="font-mono">lib/parse.ts</span>.
57
+ </p>
48
58
  {error && <p className="text-[11px] text-error mb-2">{error}</p>}
49
59
  <div className="flex justify-end space-x-2 mt-2">
50
60
  <button
@@ -109,13 +119,24 @@ interface RenameModalProps {
109
119
  isFolder: boolean;
110
120
  renameValue: string;
111
121
  originalPath: string;
122
+ /** The server's refusal (an existing destination, a bad path), shown inline. */
123
+ error?: string | null;
112
124
  isLoading: boolean;
113
125
  onChange: (value: string) => void;
114
126
  onCancel: () => void;
115
127
  onSubmit: (e: FormEvent) => void;
116
128
  }
117
129
 
118
- export function FileRenameModal({ isFolder, renameValue, originalPath, isLoading, onChange, onCancel, onSubmit }: RenameModalProps) {
130
+ /**
131
+ * Rename, with the WHOLE path in the field.
132
+ *
133
+ * The field holds the path relative to the listed root, not just the leaf name,
134
+ * because that is what makes a nested destination reachable: `src/a.ts` →
135
+ * `lib/deep/a.ts` both renames the file and moves it, and its parents are
136
+ * created on the way. A leaf-only field would have made that a create-then-
137
+ * delete, which is not a rename.
138
+ */
139
+ export function FileRenameModal({ isFolder, renameValue, originalPath, error, isLoading, onChange, onCancel, onSubmit }: RenameModalProps) {
119
140
  return createPortal(
120
141
  <div className="fixed inset-0 bg-ink/20 z-[100] flex items-center justify-center p-4 backdrop-blur-sm">
121
142
  <form onSubmit={onSubmit} className="bg-paper rounded shadow-xl p-4 w-full max-w-sm border border-ink/10 font-sans">
@@ -125,8 +146,15 @@ export function FileRenameModal({ isFolder, renameValue, originalPath, isLoading
125
146
  type="text"
126
147
  value={renameValue}
127
148
  onChange={e => onChange(e.currentTarget.value)}
128
- className="w-full bg-paper border border-ink/20 rounded px-2 py-1.5 text-xs font-mono focus:outline-none focus:border-ink mb-4"
149
+ aria-label={`Rename ${isFolder ? 'folder' : 'file'} path`}
150
+ className={`w-full bg-paper border rounded px-2 py-1.5 text-xs font-mono focus:outline-none mb-1 ${
151
+ error ? 'border-error' : 'border-ink/20 focus:border-ink'
152
+ }`}
129
153
  />
154
+ <p className="text-[10px] text-ink/40 mb-2">
155
+ Paths are relative to the tree — <span className="font-mono">lib/deep/name.ts</span> moves it too.
156
+ </p>
157
+ {error && <p className="text-[11px] text-error mb-2">{error}</p>}
130
158
  <div className="flex justify-end space-x-2">
131
159
  <button
132
160
  type="button"
@@ -32,6 +32,7 @@ import { SessionStateProvider } from '@/client/components/common/session-state-p
32
32
  import { forgetSession } from '@/shared/lib/workspace/session-state/store';
33
33
  import { installDomGlobals, restoreDomGlobals } from '@/test-support/pristine-globals';
34
34
  import { publishEditorDocument, resetEditorDocument, type EditorDocument } from '@/client/lib/editor-document';
35
+ import { MAX_OUTLINE_ROWS } from '@/shared/lib/code/outline/rows';
35
36
  import { getLineReveal, setLineReveal } from '@/client/lib/line-reveal';
36
37
 
37
38
  const SESSION = 'new-outline-section';
@@ -215,4 +216,21 @@ describe('OutlineSection', () => {
215
216
  expect(reveal().getAttribute('data-expanded')).toBe('true');
216
217
  expect(toggle().getAttribute('aria-expanded')).toBe('true');
217
218
  });
219
+
220
+ // The cap is what keeps a large file's outline from becoming a DOM: 63 618
221
+ // declarations mounted 446 308 nodes and charged 953 ms to every keystroke
222
+ // (measured), where the editor's own cost is 34 ms.
223
+ test('a large outline renders only the first rows, and says how many it is not showing', async () => {
224
+ owner = {};
225
+ const many = Array.from({ length: MAX_OUTLINE_ROWS + 200 }, (_, i) => `export function fn${i}() {}`).join('\n');
226
+ publishEditorDocument(owner, { ...READY, path: 'src/many.ts', content: many });
227
+ await mount();
228
+ await expand();
229
+
230
+ expect(rowLabels()).toHaveLength(MAX_OUTLINE_ROWS);
231
+ // The count badge shows the window against the total, so the number on the
232
+ // toggle cannot be read as "this file has 500 symbols".
233
+ expect(toggle().textContent).toContain(`${MAX_OUTLINE_ROWS}/700`);
234
+ expect(container.textContent).toContain('Showing the first 500 of 700 symbols.');
235
+ });
218
236
  });
@@ -35,6 +35,7 @@ import { setLineReveal } from '@/client/lib/line-reveal';
35
35
  import { useSessionState } from '@/client/hooks/workspace/session-state';
36
36
  import { useOutlineView } from '@/client/hooks/editor/use-outline-view';
37
37
  import type { OutlineSymbol } from '@/shared/lib/code/outline/types';
38
+ import { outlineTruncatedNotice } from '@/shared/lib/code/outline/rows';
38
39
 
39
40
  export function OutlineSection() {
40
41
  const [expanded, setExpanded] = useSessionState<boolean>('files.outlineExpanded', false);
@@ -44,11 +45,12 @@ export function OutlineSection() {
44
45
  // Recomputed from the LIVE buffer, so an edit moves the rows with it. The
45
46
  // scan is a depth-aware line walk (see `outline/extract.ts`): measured at
46
47
  // 0.1–2.2 ms on this repository's largest sources.
47
- const { symbols, notice } = useOutlineView({
48
+ const { symbols, total, truncated, notice } = useOutlineView({
48
49
  content: current.content,
49
50
  language: current.language,
50
51
  state: current.state,
51
52
  note: current.note,
53
+ path: current.path,
52
54
  });
53
55
 
54
56
  // A diff tab HIDES the section rather than explaining that it has none. The
@@ -91,9 +93,9 @@ export function OutlineSection() {
91
93
  <span className="w-3 text-center font-mono" aria-hidden="true">{expanded ? '▾' : '▸'}</span>
92
94
  <ListTree size={12} />
93
95
  <span>Outline</span>
94
- {symbols.length > 0 && (
96
+ {total > 0 && (
95
97
  <span className="font-normal ml-1 border border-ink/20 rounded-full px-1.5 text-[9px] text-ink/50">
96
- {symbols.length}
98
+ {truncated ? `${symbols.length}/${total.toLocaleString()}` : total}
97
99
  </span>
98
100
  )}
99
101
  </button>
@@ -125,6 +127,11 @@ export function OutlineSection() {
125
127
  <span className="flex-shrink-0 font-mono text-[9px] text-ink/35">{symbol.line}</span>
126
128
  </button>
127
129
  ))}
130
+ {truncated && (
131
+ <div className="px-1 py-1 text-[10px] italic text-ink/40">
132
+ {outlineTruncatedNotice(total)}
133
+ </div>
134
+ )}
128
135
  </div>
129
136
  )}
130
137
  </div>