@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,209 +0,0 @@
1
- import { useMemo } from 'preact/hooks';
2
- import type { SplitDiffRow } from '@/shared/lib/fs/diff-parser';
3
- import { highlightLines } from '@/shared/lib/code/syntax-highlight';
4
- import { useSyntaxReady } from '@/client/hooks/ui/syntax-ready';
5
- import { useScrollRestore } from '@/client/hooks/editor/use-scroll-restore';
6
-
7
- interface SplitViewProps {
8
- rows: SplitDiffRow[];
9
- language: string;
10
- /** Wrap long lines instead of scrolling horizontally. */
11
- wordWrap: boolean;
12
- /**
13
- * The diff tab these rows belong to, so its scroll offset is remembered — or
14
- * null for a caller with no tab of its own (a diff inside a chat tool card,
15
- * where the timeline owns the scrolling).
16
- */
17
- tabKey: string | null;
18
- }
19
-
20
- export function SplitView({ rows, language, wordWrap, tabKey }: SplitViewProps) {
21
- const syntaxReady = useSyntaxReady();
22
- // Its own offset: split rows and unified lines are different heights, and
23
- // switching shape must not carry one view's position onto the other's rows.
24
- const scroll = useScrollRestore({ tabKey, surface: 'split' });
25
-
26
- const htmlByIndex = useMemo(() => {
27
- const left = new Array<string>(rows.length).fill('');
28
- const right = new Array<string>(rows.length).fill('');
29
- let start = -1;
30
- const flush = (end: number) => {
31
- if (start < 0) return;
32
- const group = rows.slice(start, end);
33
- const leftHtml = highlightLines(group.map((r) => r.left?.text ?? '').join('\n'), language);
34
- const rightHtml = highlightLines(group.map((r) => r.right?.text ?? '').join('\n'), language);
35
- for (let i = start; i < end; i++) {
36
- left[i] = leftHtml[i - start] ?? '';
37
- right[i] = rightHtml[i - start] ?? '';
38
- }
39
- start = -1;
40
- };
41
- rows.forEach((row, idx) => {
42
- if (row.isMeta) flush(idx);
43
- else if (start < 0) start = idx;
44
- });
45
- flush(rows.length);
46
- return { left, right };
47
- }, [rows, language, syntaxReady]);
48
-
49
- if (rows.length === 0) {
50
- return (
51
- <div className="flex-1 flex items-center justify-center text-ink/40 font-mono text-xs p-8">
52
- No differences between working file and target.
53
- </div>
54
- );
55
- }
56
-
57
- return (
58
- <div
59
- ref={scroll.ref}
60
- onScroll={scroll.onScroll}
61
- data-testid="diff-scroller"
62
- className="w-full h-full overflow-auto font-mono text-xs select-text bg-paper text-ink code-surface"
63
- >
64
- {/* The room under the last row belongs to the content, not the scroller:
65
- a scroll container's own bottom padding is not part of its scrollable
66
- overflow, so the padding has to sit inside it to have any effect. */}
67
- <div className={`pb-4 ${wordWrap ? 'w-full' : 'w-max min-w-full min-w-[700px]'}`}>
68
- <div className="grid grid-cols-2 sticky top-0 z-10 bg-canvas border-b border-ink/10 text-[11px] text-ink/70 font-sans select-none shadow-xs">
69
- <div className="px-3 py-1 font-medium border-r border-ink/10 flex items-center justify-between">
70
- <span>Original (HEAD / Base)</span>
71
- </div>
72
- <div className="px-3 py-1 font-medium flex items-center justify-between">
73
- <span>Modified (Working Tree)</span>
74
- </div>
75
- </div>
76
-
77
- {/* Wrap needs `table-fixed`: auto layout sizes a column by its widest
78
- cell's UNAMBIGUOUS minimum (`overflow-wrap: break-word` does not
79
- lower it), so a long line would widen the table and scroll instead of
80
- wrapping. Fixed layout takes its widths from the FIRST row — which
81
- here is the `colSpan={4}` meta row, so every column came out equal
82
- (measured 99px) and the line-number gutters grew to the width of a
83
- code column. The colgroup states them instead: two 48px gutters and
84
- the two content columns sharing the rest. */}
85
- <table className={`border-collapse ${wordWrap ? 'table-fixed w-full' : 'w-full'}`}>
86
- {wordWrap && (
87
- <colgroup>
88
- <col className="w-12" />
89
- <col />
90
- <col className="w-12" />
91
- <col />
92
- </colgroup>
93
- )}
94
- <tbody>
95
- {rows.map((row, idx) => {
96
- if (row.isMeta) {
97
- return (
98
- <tr key={idx} className="bg-canvas border-y border-ink/10 text-meta select-none">
99
- <td colSpan={4} className="py-1 px-3 text-[11px] font-semibold text-meta text-center font-mono">
100
- {row.metaText}
101
- </td>
102
- </tr>
103
- );
104
- }
105
-
106
- const left = row.left;
107
- const right = row.right;
108
-
109
- const isLeftDel = left?.type === 'del';
110
- const isRightAdd = right?.type === 'add';
111
-
112
- return (
113
- <tr key={idx} className="border-b border-ink/[0.04]">
114
- {/* Left Line Number */}
115
- <td
116
- className={`w-12 py-0.5 px-2 text-right text-[10px] select-none border-r border-ink/10 font-mono ${
117
- wordWrap ? 'align-top' : ''
118
- } ${
119
- isLeftDel
120
- ? 'bg-error/15 text-error font-semibold'
121
- : left
122
- ? 'bg-canvas/50 text-ink/40'
123
- : 'bg-canvas/80 text-transparent'
124
- }`}
125
- >
126
- {left?.lineNumber || ''}
127
- </td>
128
-
129
- {/* Left Content (Original) */}
130
- <td
131
- className={`py-0.5 px-2 border-r border-ink/10 text-ink ${
132
- wordWrap ? 'whitespace-pre-wrap break-words align-top' : 'min-w-[320px] whitespace-pre'
133
- } ${
134
- isLeftDel
135
- ? 'bg-error/10'
136
- : left
137
- ? 'hover:bg-ink/[0.02]'
138
- : 'bg-canvas/40 select-none'
139
- }`}
140
- >
141
- {left ? (
142
- <>
143
- <span className={`inline-block w-4 text-center select-none font-bold ${isLeftDel ? 'text-error' : 'opacity-0'}`}>
144
- {isLeftDel ? '-' : ' '}
145
- </span>
146
- <span
147
- className="shiki"
148
- dangerouslySetInnerHTML={{
149
- __html: left.text ? htmlByIndex.left[idx] || '&nbsp;' : '&nbsp;',
150
- }}
151
- />
152
- </>
153
- ) : (
154
- <span className="opacity-0">&nbsp;</span>
155
- )}
156
- </td>
157
-
158
- {/* Right Line Number */}
159
- <td
160
- className={`w-12 py-0.5 px-2 text-right text-[10px] select-none border-r border-ink/10 font-mono ${
161
- wordWrap ? 'align-top' : ''
162
- } ${
163
- isRightAdd
164
- ? 'bg-success/15 text-success font-semibold'
165
- : right
166
- ? 'bg-canvas/50 text-ink/40'
167
- : 'bg-canvas/80 text-transparent'
168
- }`}
169
- >
170
- {right?.lineNumber || ''}
171
- </td>
172
-
173
- {/* Right Content (Modified) */}
174
- <td
175
- className={`py-0.5 px-2 text-ink ${
176
- wordWrap ? 'whitespace-pre-wrap break-words align-top' : 'min-w-[320px] whitespace-pre'
177
- } ${
178
- isRightAdd
179
- ? 'bg-success/10'
180
- : right
181
- ? 'hover:bg-ink/[0.02]'
182
- : 'bg-canvas/40 select-none'
183
- }`}
184
- >
185
- {right ? (
186
- <>
187
- <span className={`inline-block w-4 text-center select-none font-bold ${isRightAdd ? 'text-success' : 'opacity-0'}`}>
188
- {isRightAdd ? '+' : ' '}
189
- </span>
190
- <span
191
- className="shiki"
192
- dangerouslySetInnerHTML={{
193
- __html: right.text ? htmlByIndex.right[idx] || '&nbsp;' : '&nbsp;',
194
- }}
195
- />
196
- </>
197
- ) : (
198
- <span className="opacity-0">&nbsp;</span>
199
- )}
200
- </td>
201
- </tr>
202
- );
203
- })}
204
- </tbody>
205
- </table>
206
- </div>
207
- </div>
208
- );
209
- }
@@ -1,150 +0,0 @@
1
- import { useMemo } from 'preact/hooks';
2
- import type { DiffLine } from '@/shared/lib/fs/diff-parser';
3
- import { highlightLines } from '@/shared/lib/code/syntax-highlight';
4
- import { useSyntaxReady } from '@/client/hooks/ui/syntax-ready';
5
- import { useScrollRestore } from '@/client/hooks/editor/use-scroll-restore';
6
-
7
- interface UnifiedViewProps {
8
- lines: DiffLine[];
9
- language: string;
10
- /** Wrap long lines instead of scrolling horizontally. */
11
- wordWrap: boolean;
12
- /**
13
- * The diff tab these rows belong to, so its scroll offset is remembered — or
14
- * null for a caller with no tab of its own (a diff inside a chat tool card,
15
- * where the timeline owns the scrolling).
16
- */
17
- tabKey: string | null;
18
- }
19
-
20
- export function UnifiedView({ lines, language, wordWrap, tabKey }: UnifiedViewProps) {
21
- const syntaxReady = useSyntaxReady();
22
- // The unified and split shapes are different row counts and heights for the
23
- // same diff, so each keeps its own offset (see `ScrollSurface`).
24
- const scroll = useScrollRestore({ tabKey, surface: 'unified' });
25
-
26
- const htmlByIndex = useMemo(() => {
27
- const out = new Array<string>(lines.length).fill('');
28
- let start = -1;
29
- const flush = (end: number) => {
30
- if (start < 0) return;
31
- const html = highlightLines(lines.slice(start, end).map((l) => l.text).join('\n'), language);
32
- for (let i = start; i < end; i++) out[i] = html[i - start] ?? '';
33
- start = -1;
34
- };
35
- lines.forEach((line, idx) => {
36
- if (line.type === 'meta') flush(idx);
37
- else if (start < 0) start = idx;
38
- });
39
- flush(lines.length);
40
- return out;
41
- }, [lines, language, syntaxReady]);
42
-
43
- if (lines.length === 0) {
44
- return (
45
- <div className="flex-1 flex items-center justify-center text-ink/40 font-mono text-xs p-8">
46
- No differences between working file and target.
47
- </div>
48
- );
49
- }
50
-
51
- return (
52
- <div
53
- ref={scroll.ref}
54
- onScroll={scroll.onScroll}
55
- data-testid="diff-scroller"
56
- className="w-full h-full overflow-auto font-mono text-xs select-text bg-paper text-ink code-surface"
57
- >
58
- {/* The room under the last row belongs to the content, not the scroller:
59
- a scroll container's own bottom padding is not part of its scrollable
60
- overflow, so the padding has to sit inside it to have any effect. */}
61
- <div className={`pb-4 ${wordWrap ? 'w-full' : 'w-max min-w-full'}`}>
62
- {/* Wrap needs `table-fixed`: auto layout sizes a column by its widest
63
- cell's UNAMBIGUOUS minimum (`overflow-wrap: break-word` does not
64
- lower it), so a long line would still widen the table and scroll
65
- instead of wrapping. Fixed layout takes the width from the row's
66
- own box, which is what makes the cell's wrap rule effective. */}
67
- <table className={`border-collapse ${wordWrap ? 'table-fixed w-full' : 'w-max min-w-full'}`}>
68
- <tbody>
69
- {lines.map((line, idx) => {
70
- if (line.type === 'meta') {
71
- return (
72
- <tr key={idx} className="bg-canvas border-y border-ink/10 text-meta select-none">
73
- <td className="w-12 text-center py-1 px-2 text-[10px] opacity-60 border-r border-ink/10 font-mono">...</td>
74
- <td className="w-12 text-center py-1 px-2 text-[10px] opacity-60 border-r border-ink/10 font-mono">...</td>
75
- <td className="py-1 px-3 text-[11px] font-semibold text-meta font-mono tracking-tight">{line.text}</td>
76
- </tr>
77
- );
78
- }
79
-
80
- const isAdd = line.type === 'add';
81
- const isDel = line.type === 'del';
82
-
83
- return (
84
- <tr
85
- key={idx}
86
- className={`transition-colors group ${
87
- isAdd
88
- ? 'bg-success/10 hover:bg-success/15'
89
- : isDel
90
- ? 'bg-error/10 hover:bg-error/15'
91
- : 'hover:bg-ink/[0.03]'
92
- }`}
93
- >
94
- {/* Old line number. Top-aligned when the row wraps, or the
95
- number would float to the middle of a multi-line cell
96
- instead of naming its first line. */}
97
- <td
98
- className={`w-12 py-0.5 px-2 text-right text-[10px] select-none border-r border-ink/10 font-mono ${
99
- wordWrap ? 'align-top' : ''
100
- } ${
101
- isDel
102
- ? 'bg-error/15 text-error font-semibold'
103
- : isAdd
104
- ? 'bg-success/15 text-success/70'
105
- : 'bg-canvas/50 text-ink/40'
106
- }`}
107
- >
108
- {line.oldLineNumber || ''}
109
- </td>
110
-
111
- {/* New line number */}
112
- <td
113
- className={`w-12 py-0.5 px-2 text-right text-[10px] select-none border-r border-ink/10 font-mono ${
114
- wordWrap ? 'align-top' : ''
115
- } ${
116
- isAdd
117
- ? 'bg-success/15 text-success font-semibold'
118
- : isDel
119
- ? 'bg-error/15 text-error/70'
120
- : 'bg-canvas/50 text-ink/40'
121
- }`}
122
- >
123
- {line.newLineNumber || ''}
124
- </td>
125
-
126
- {/* Change prefix & code text */}
127
- <td className={`py-0.5 px-3 text-ink pr-6 ${wordWrap ? 'whitespace-pre-wrap break-words align-top' : 'whitespace-pre'}`}>
128
- <span
129
- className={`inline-block w-4 text-center select-none font-bold ${
130
- isAdd ? 'text-success' : isDel ? 'text-error' : 'opacity-0'
131
- }`}
132
- >
133
- {isAdd ? '+' : isDel ? '-' : ' '}
134
- </span>
135
- <span
136
- className="shiki"
137
- dangerouslySetInnerHTML={{
138
- __html: line.text ? htmlByIndex[idx] || '&nbsp;' : '&nbsp;',
139
- }}
140
- />
141
- </td>
142
- </tr>
143
- );
144
- })}
145
- </tbody>
146
- </table>
147
- </div>
148
- </div>
149
- );
150
- }
@@ -1,102 +0,0 @@
1
- /**
2
- * @license
3
- * SPDX-License-Identifier: Apache-2.0
4
- */
5
-
6
- /**
7
- * The scroll-container walk, shared by the editor's lazy window and the find
8
- * widget's reveal.
9
- *
10
- * The two must agree: one resolves which lines to draw from the visible band,
11
- * the other moves that band. Two copies of this walk would let the widget scroll
12
- * a container the window never reads — the match would be marked somewhere
13
- * nobody can see. Exercised against a real DOM (happy-dom) because the answer is
14
- * a computed style.
15
- */
16
-
17
- import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
18
- import { Window } from 'happy-dom';
19
-
20
- import { findScrollContainer } from '@/client/hooks/editor/scroll-container';
21
-
22
- const installed: Record<string, unknown> = {};
23
- const displaced: Record<string, unknown> = {};
24
-
25
- beforeAll(() => {
26
- const win = new Window({ url: 'http://localhost' });
27
- for (const key of ['window', 'document', 'Node', 'Element', 'HTMLElement', 'getComputedStyle']) {
28
- displaced[key] = (globalThis as Record<string, unknown>)[key];
29
- installed[key] = (win as unknown as Record<string, unknown>)[key];
30
- (globalThis as Record<string, unknown>)[key] = installed[key];
31
- }
32
- });
33
-
34
- afterAll(() => {
35
- for (const key of Object.keys(installed)) {
36
- if (displaced[key] === undefined) delete (globalThis as Record<string, unknown>)[key];
37
- else (globalThis as Record<string, unknown>)[key] = displaced[key];
38
- }
39
- });
40
-
41
- /** An element whose computed style reports `overflow-y`, with a scrollable height. */
42
- function makeElement(overflowY: string, scrollable: boolean): HTMLElement {
43
- const element = document.createElement('div');
44
- element.style.overflowY = overflowY;
45
- if (scrollable) {
46
- // happy-dom reports clientHeight 0, so the walk's `scrollHeight > clientHeight`
47
- // test needs a stubbed pair to describe a container that can actually scroll.
48
- Object.defineProperty(element, 'scrollHeight', { value: 400, configurable: true });
49
- Object.defineProperty(element, 'clientHeight', { value: 100, configurable: true });
50
- }
51
- return element;
52
- }
53
-
54
- describe('findScrollContainer', () => {
55
- test('finds the nearest ancestor that scrolls', () => {
56
- const outer = makeElement('auto', true);
57
- const inner = makeElement('visible', false);
58
- const surface = document.createElement('div');
59
- inner.appendChild(surface);
60
- outer.appendChild(inner);
61
- document.body.appendChild(outer);
62
-
63
- expect(findScrollContainer(surface)).toBe(outer);
64
- outer.remove();
65
- });
66
-
67
- test('skips a container that cannot actually scroll', () => {
68
- // `overflow: auto` on a box whose content fits is not a scroller: returning
69
- // it would make `revealOffset` write scrollTop onto a box that ignores it.
70
- const outer = makeElement('auto', true);
71
- const fitted = makeElement('auto', false);
72
- const surface = document.createElement('div');
73
- fitted.appendChild(surface);
74
- outer.appendChild(fitted);
75
- document.body.appendChild(outer);
76
-
77
- expect(findScrollContainer(surface)).toBe(outer);
78
- outer.remove();
79
- });
80
-
81
- test('reports nothing when no ancestor scrolls', () => {
82
- const plain = document.createElement('div');
83
- const surface = document.createElement('div');
84
- plain.appendChild(surface);
85
- document.body.appendChild(plain);
86
-
87
- expect(findScrollContainer(surface)).toBeNull();
88
- plain.remove();
89
- });
90
-
91
- test('prefers the nearest scroller, so a nested panel scrolls itself', () => {
92
- const outer = makeElement('auto', true);
93
- const inner = makeElement('scroll', true);
94
- const surface = document.createElement('div');
95
- inner.appendChild(surface);
96
- outer.appendChild(inner);
97
- document.body.appendChild(outer);
98
-
99
- expect(findScrollContainer(surface)).toBe(inner);
100
- outer.remove();
101
- });
102
- });
@@ -1,26 +0,0 @@
1
- /**
2
- * @license
3
- * SPDX-License-Identifier: Apache-2.0
4
- */
5
-
6
- /**
7
- * The ancestor a code surface actually scrolls inside.
8
- *
9
- * The lazy editor window and the find widget's "scroll to this match" both need
10
- * this answer, and they must agree: one resolves which lines to draw from the
11
- * visible band, the other moves that band. Two copies of the walk would let the
12
- * widget scroll a container the window never reads.
13
- *
14
- * Walks up rather than taking a ref: the surface is mounted under different
15
- * scrollers (the desktop editor panel, the phone's full-screen editor) and is
16
- * also mounted inside a page that scrolls itself, which is the `null` case.
17
- */
18
- export function findScrollContainer(element: HTMLElement): HTMLElement | null {
19
- for (let node = element.parentElement; node; node = node.parentElement) {
20
- const overflowY = getComputedStyle(node).overflowY;
21
- if ((overflowY === 'auto' || overflowY === 'scroll' || overflowY === 'overlay') && node.scrollHeight > node.clientHeight) {
22
- return node;
23
- }
24
- }
25
- return null;
26
- }
@@ -1,100 +0,0 @@
1
- import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'preact/hooks';
2
- import type { RefObject } from 'preact';
3
-
4
- import { codeWindow, firstWindow, resolveWindow, sameWindow, type CodeWindow, type LineGeometry, type LineWindow } from '@/shared/lib/code/lazy-window';
5
- import { findScrollContainer } from '@/client/hooks/editor/scroll-container';
6
-
7
- export interface UseCodeWindowOptions {
8
- /** The surface's root box: the frame every line offset in `geometry` is measured from. */
9
- rootRef: RefObject<HTMLElement | null>;
10
- /** Lines in the document on screen. */
11
- lineCount: number;
12
- /** Document geometry, or null until the wrap/row measurement lands. */
13
- geometry: LineGeometry | null;
14
- /** False for short documents (they render whole) and when geometry cannot be measured. */
15
- enabled: boolean;
16
- }
17
-
18
- /**
19
- * The lines visible inside the scroll container `root` lives in.
20
- *
21
- * Both rectangles are read in viewport coordinates, so this needs no knowledge
22
- * of scroll offsets: the part of the editor's box the container actually shows
23
- * is exactly the band between the two.
24
- */
25
- function visibleWindow(root: HTMLElement, geometry: LineGeometry): LineWindow {
26
- const frameTop = root.getBoundingClientRect().top;
27
- const container = findScrollContainer(root);
28
- const viewTop = container ? container.getBoundingClientRect().top : 0;
29
- const viewBottom = container
30
- ? container.getBoundingClientRect().bottom
31
- : globalThis.innerHeight || geometry.rowHeight;
32
- return resolveWindow(geometry, viewTop - frameTop, viewBottom - viewTop);
33
- }
34
-
35
- /**
36
- * Rendering window for the lazy editor surface.
37
- *
38
- * A long document is highlighted, gutters and all, only where it can be seen;
39
- * this hook owns which lines those are and re-resolves them on scroll, on
40
- * resize, and whenever the geometry moves (a keystroke re-wraps a line). The
41
- * scroll listener is a capturing one on the window: scroll events do not
42
- * bubble, but they do reach a capturing listener, so the surface follows
43
- * whichever ancestor scrolls — the desktop editor panel, the phone's
44
- * full-screen editor, or the page.
45
- *
46
- * Returns null for a document that renders whole, which is the caller's cue to
47
- * keep the plain, unwindowed surface.
48
- */
49
- export function useCodeWindow({ rootRef, lineCount, geometry, enabled }: UseCodeWindowOptions): CodeWindow | null {
50
- const [lineWindow, setLineWindow] = useState<LineWindow | null>(null);
51
-
52
- const compute = useCallback(() => {
53
- const root = rootRef.current;
54
- if (!enabled || !root) return;
55
- const next = geometry ? visibleWindow(root, geometry) : firstWindow(lineCount);
56
- setLineWindow((previous) => (sameWindow(previous, next) ? previous : next));
57
- }, [enabled, geometry, lineCount, rootRef]);
58
-
59
- // Layout effect: the first window has to be resolved before the browser
60
- // paints, or a long document flashes its untouched opening lines.
61
- useLayoutEffect(() => {
62
- compute();
63
- }, [compute]);
64
-
65
- // The listeners follow `enabled`, not `compute`: re-resolving on every
66
- // keystroke must not tear down and rebuild the observer each time.
67
- const computeRef = useRef(compute);
68
- computeRef.current = compute;
69
- useEffect(() => {
70
- if (!enabled) return;
71
- let frame = 0;
72
- const schedule = () => {
73
- if (frame) return;
74
- frame = requestAnimationFrame(() => {
75
- frame = 0;
76
- computeRef.current();
77
- });
78
- };
79
- window.addEventListener('scroll', schedule, { passive: true, capture: true });
80
- window.addEventListener('resize', schedule);
81
- const observer = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(schedule);
82
- const root = rootRef.current;
83
- if (root) observer?.observe(root);
84
- return () => {
85
- if (frame) cancelAnimationFrame(frame);
86
- window.removeEventListener('scroll', schedule, { capture: true });
87
- window.removeEventListener('resize', schedule);
88
- observer?.disconnect();
89
- };
90
- }, [enabled, rootRef]);
91
-
92
- if (!enabled) return null;
93
- if (!geometry) {
94
- // First commit: the layout effect above lands the real window in the same
95
- // frame, so this only bounds how much the opening render is allowed to draw.
96
- const opening = firstWindow(lineCount);
97
- return { ...opening, top: 0, bottom: 0, breakAtEnd: opening.end >= lineCount };
98
- }
99
- return codeWindow(geometry, lineWindow ?? firstWindow(geometry.lineCount));
100
- }