@kabmo-labs/kabmo 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (351) hide show
  1. package/README.md +7 -0
  2. package/dist/client/index.html +1 -1
  3. package/dist/client/index.js +403 -384
  4. package/dist/client/static/js/{index-0wvbca0b.js → index-bpptfkz2.js} +905 -875
  5. package/dist/client/static/js/index-k315p1e8.css +1 -0
  6. package/package.json +10 -1
  7. package/src/client/components/common/code-surface/documents.test.ts +120 -0
  8. package/src/client/components/common/code-surface/documents.ts +201 -0
  9. package/src/client/components/common/code-surface/extensions.test.ts +110 -0
  10. package/src/client/components/common/code-surface/extensions.ts +162 -0
  11. package/src/client/components/common/code-surface/handle.ts +102 -0
  12. package/src/client/components/common/code-surface/index.tsx +253 -314
  13. package/src/client/components/common/code-surface/keymap.test.ts +133 -0
  14. package/src/client/components/common/code-surface/keymap.ts +267 -0
  15. package/src/client/components/common/code-surface/marks.ts +74 -0
  16. package/src/client/components/common/code-surface/shiki.ts +234 -0
  17. package/src/client/components/common/code-surface/sync.test.ts +185 -0
  18. package/src/client/components/common/code-surface/sync.ts +58 -0
  19. package/src/client/components/common/code-surface/theme.test.ts +202 -0
  20. package/src/client/components/common/code-surface/theme.ts +111 -0
  21. package/src/client/components/common/editor-view-buttons/index.test.tsx +148 -0
  22. package/src/client/components/common/editor-view-buttons/index.tsx +109 -0
  23. package/src/client/components/common/header-controls/index.tsx +24 -1
  24. package/src/client/components/common/header-controls/shell-chrome.test.tsx +272 -0
  25. package/src/client/components/common/shell-update-button/Modal.tsx +163 -0
  26. package/src/client/components/common/shell-update-button/index.test.tsx +251 -0
  27. package/src/client/components/common/shell-update-button/index.tsx +125 -0
  28. package/src/client/components/common/toolbar-row/index.test.tsx +166 -0
  29. package/src/client/components/common/toolbar-row/index.tsx +189 -0
  30. package/src/client/components/common/workspace-options-menu/index.tsx +19 -1
  31. package/src/client/components/layout/desktop-layout/TopNavbar.tsx +5 -6
  32. package/src/client/components/layout/desktop-layout/WorkspacePanels.tsx +41 -78
  33. package/src/client/components/layout/desktop-layout/index.tsx +55 -69
  34. package/src/client/components/layout/desktop-layout/resizer/index.tsx +6 -3
  35. package/src/client/components/layout/session-sidebar/CategoryHeader.tsx +24 -0
  36. package/src/client/components/layout/session-sidebar/CategoryHeader.worktree.test.tsx +126 -0
  37. package/src/client/components/layout/session-sidebar/CategoryItem.tsx +4 -0
  38. package/src/client/components/layout/session-sidebar/Footer.tsx +8 -15
  39. package/src/client/components/layout/session-sidebar/SessionItem.tsx +12 -0
  40. package/src/client/components/layout/session-sidebar/SessionList.tsx +4 -0
  41. package/src/client/components/layout/session-sidebar/SessionsView.tsx +24 -0
  42. package/src/client/components/layout/session-sidebar/WorktreeBadge.test.ts +48 -0
  43. package/src/client/components/layout/session-sidebar/WorktreeBadge.tsx +55 -0
  44. package/src/client/components/mobile/LayoutWrapper.tsx +23 -16
  45. package/src/client/components/mobile/RightSidebar.tsx +21 -10
  46. package/src/client/components/mobile/SessionSidebar.tsx +8 -0
  47. package/src/client/components/mobile/header-titlebar.test.tsx +172 -0
  48. package/src/client/components/mobile/mobile-main-view/Header.tsx +10 -1
  49. package/src/client/components/mobile/mobile-right-sidebar/FullDiff.tsx +3 -0
  50. package/src/client/components/mobile/mobile-right-sidebar/FullEditor.tsx +63 -38
  51. package/src/client/components/mobile/mobile-right-sidebar/OutlineButton.test.tsx +137 -0
  52. package/src/client/components/mobile/mobile-right-sidebar/OutlineButton.tsx +14 -3
  53. package/src/client/components/mobile/mobile-session-sidebar/Footer.tsx +9 -21
  54. package/src/client/components/mobile/mobile-session-sidebar/Header.tsx +6 -2
  55. package/src/client/components/mobile/mobile-session-sidebar/Item.tsx +16 -0
  56. package/src/client/components/mobile/mobile-session-sidebar/List.tsx +4 -0
  57. package/src/client/components/mobile/mobile-session-sidebar/SessionRow.tsx +5 -0
  58. package/src/client/components/mobile/mobile-session-sidebar/WorktreeDialog.tsx +49 -0
  59. package/src/client/components/mobile/worktree-session.ts +51 -0
  60. package/src/client/components/settings/Modal.tsx +8 -0
  61. package/src/client/components/settings/categories/AppearanceSettings.tsx +3 -0
  62. package/src/client/components/settings/categories/ProjectSettings.tsx +22 -8
  63. package/src/client/components/settings/categories/appearance-settings/EditorThemeSection.test.tsx +167 -0
  64. package/src/client/components/settings/categories/appearance-settings/EditorThemeSection.tsx +184 -0
  65. package/src/client/components/settings/categories/project-settings/WorktreeSettings.tsx +146 -0
  66. package/src/client/components/settings/categories/project-settings/worktree-row.test.ts +83 -0
  67. package/src/client/components/settings/categories/project-settings/worktree-row.ts +47 -0
  68. package/src/client/components/workspace/chat-timeline/EmptyWorkspacePrompt.tsx +36 -55
  69. package/src/client/components/workspace/chat-timeline/PendingChatView.tsx +8 -0
  70. package/src/client/components/workspace/chat-timeline/SystemNotice.tsx +9 -4
  71. package/src/client/components/workspace/chat-timeline/TaskResultContent.tsx +9 -7
  72. package/src/client/components/workspace/chat-timeline/index.tsx +5 -0
  73. package/src/client/components/workspace/chat-timeline/target-picker/WorkspacePicker.test.tsx +132 -0
  74. package/src/client/components/workspace/chat-timeline/target-picker/WorkspacePicker.tsx +120 -0
  75. package/src/client/components/workspace/chat-timeline/target-picker/WorktreePicker.test.tsx +319 -0
  76. package/src/client/components/workspace/chat-timeline/target-picker/WorktreePicker.tsx +285 -0
  77. package/src/client/components/workspace/chat-timeline/tool-renderers/ask-panel/index.test.ts +2 -0
  78. package/src/client/components/workspace/chat-timeline/tool-renderers/hashline-patch/index.tsx +34 -29
  79. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/AstEdit.tsx +11 -9
  80. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Bash.tsx +10 -7
  81. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Edit.tsx +25 -8
  82. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Eval.test.tsx +146 -0
  83. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Eval.tsx +32 -17
  84. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/FileMatches.test.ts +114 -0
  85. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/FileMatches.tsx +111 -0
  86. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Read.tsx +7 -7
  87. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/SearchTool.tsx +9 -59
  88. package/src/client/components/workspace/chat-timeline/tool-renderers/shared/ArtifactBody.tsx +2 -7
  89. package/src/client/components/workspace/chat-timeline/tool-renderers/shared/DiffView.test.ts +27 -28
  90. package/src/client/components/workspace/chat-timeline/tool-renderers/shared/DiffView.tsx +39 -26
  91. package/src/client/components/workspace/chat-timeline/tool-renderers/shared/ExcerptCode.tsx +8 -8
  92. package/src/client/components/workspace/chat-timeline/tool-renderers/shared/HighlightedCode.tsx +35 -0
  93. package/src/client/components/workspace/chat-timeline/tool-renderers/shared/JsonCodeBlock.tsx +4 -4
  94. package/src/client/components/workspace/diff-panel/MergeDiffView.tsx +179 -0
  95. package/src/client/components/workspace/diff-panel/ThemedMergeDiffView.tsx +24 -0
  96. package/src/client/components/workspace/diff-panel/Toolbar.tsx +224 -186
  97. package/src/client/components/workspace/diff-panel/ToolbarSegments.tsx +98 -0
  98. package/src/client/components/workspace/diff-panel/index.tsx +76 -145
  99. package/src/client/components/workspace/diff-panel/merge.ts +315 -0
  100. package/src/client/components/workspace/editor/Body.tsx +4 -7
  101. package/src/client/components/workspace/editor/CommandPalette.tsx +2 -2
  102. package/src/client/components/workspace/editor/FileDocument.tsx +58 -63
  103. package/src/client/components/workspace/editor/FilePane.tsx +4 -7
  104. package/src/client/components/workspace/editor/FindWidget.tsx +7 -2
  105. package/src/client/components/workspace/editor/Toolbar.tsx +220 -123
  106. package/src/client/components/workspace/editor/index.tsx +7 -9
  107. package/src/client/components/workspace/file-explorer/ContextMenu.tsx +2 -1
  108. package/src/client/components/workspace/file-explorer/Modals.tsx +40 -12
  109. package/src/client/components/workspace/file-explorer/OutlineSection.test.tsx +18 -0
  110. package/src/client/components/workspace/file-explorer/OutlineSection.tsx +10 -3
  111. package/src/client/components/workspace/file-explorer/RootMenu.tsx +14 -5
  112. package/src/client/components/workspace/file-explorer/Toolbar.tsx +74 -0
  113. package/src/client/components/workspace/file-explorer/TreeItem.tsx +38 -3
  114. package/src/client/components/workspace/file-explorer/filter.ts +47 -0
  115. package/src/client/components/workspace/file-explorer/index.tsx +77 -64
  116. package/src/client/components/workspace/file-explorer/local-edit.test.ts +135 -0
  117. package/src/client/components/workspace/search-panel/Body.tsx +1 -1
  118. package/src/client/components/workspace/worktree-modal/index.test.tsx +118 -0
  119. package/src/client/components/workspace/worktree-modal/index.tsx +261 -0
  120. package/src/client/hooks/chat/omp/reconnect.test.ts +338 -0
  121. package/src/client/hooks/chat/omp/reconnect.ts +187 -0
  122. package/src/client/hooks/chat/omp/run-liveness.test.ts +71 -0
  123. package/src/client/hooks/chat/omp/run-liveness.ts +65 -0
  124. package/src/client/hooks/chat/omp/session-list.activity.test.ts +114 -0
  125. package/src/client/hooks/chat/omp/session-list.test-util.ts +164 -0
  126. package/src/client/hooks/chat/omp/session-list.test.ts +30 -150
  127. package/src/client/hooks/chat/omp/session-list.tsx +69 -9
  128. package/src/client/hooks/chat/timeline/generating.ts +19 -2
  129. package/src/client/hooks/chat/timeline/index.ts +27 -28
  130. package/src/client/hooks/chat/timeline/lost-run.test.ts +329 -0
  131. package/src/client/hooks/chat/timeline/lost-run.ts +177 -0
  132. package/src/client/hooks/chat/timeline/send-execute.test.ts +2 -0
  133. package/src/client/hooks/chat/timeline/send-steer.test.ts +2 -0
  134. package/src/client/hooks/chat/timeline/send.test.ts +2 -0
  135. package/src/client/hooks/chat/timeline/send.ts +11 -1
  136. package/src/client/hooks/chat/timeline/session-load.test-util.ts +205 -0
  137. package/src/client/hooks/chat/timeline/session-load.test.ts +61 -162
  138. package/src/client/hooks/chat/timeline/session-load.ts +69 -37
  139. package/src/client/hooks/chat/timeline/session-switch.test.ts +65 -0
  140. package/src/client/hooks/chat/timeline/session-switch.ts +63 -0
  141. package/src/client/hooks/chat/timeline/spawn-target.ts +52 -0
  142. package/src/client/hooks/chat/timeline/tool-open.test.ts +74 -0
  143. package/src/client/hooks/chat/timeline/tool-open.ts +52 -8
  144. package/src/client/hooks/code/deferred-highlight.test.ts +182 -0
  145. package/src/client/hooks/code/deferred-highlight.ts +129 -0
  146. package/src/client/hooks/editor/find-state.ts +1 -1
  147. package/src/client/hooks/editor/theme.test.ts +206 -0
  148. package/src/client/hooks/editor/theme.ts +82 -0
  149. package/src/client/hooks/editor/use-editor-drop.test.ts +101 -0
  150. package/src/client/hooks/editor/use-editor-drop.ts +67 -14
  151. package/src/client/hooks/editor/use-editor-find.ts +7 -7
  152. package/src/client/hooks/editor/use-outline-view.test.ts +182 -0
  153. package/src/client/hooks/editor/use-outline-view.ts +94 -7
  154. package/src/client/hooks/editor/use-scroll-restore.test.ts +55 -2
  155. package/src/client/hooks/editor/use-scroll-restore.ts +124 -4
  156. package/src/client/hooks/editor/use-search-reveal.ts +1 -1
  157. package/src/client/hooks/ui/long-press.test.ts +174 -0
  158. package/src/client/hooks/ui/long-press.ts +112 -0
  159. package/src/client/hooks/ui/settings-modal.ts +56 -0
  160. package/src/client/hooks/ui/updates/shell-download.test.ts +75 -0
  161. package/src/client/hooks/ui/updates/shell-download.ts +153 -28
  162. package/src/client/hooks/ui/updates/shell-offer.test.ts +213 -0
  163. package/src/client/hooks/ui/updates/shell-update.test.ts +140 -0
  164. package/src/client/hooks/ui/updates/shell-update.ts +119 -0
  165. package/src/client/hooks/workspace/diff-file.ts +29 -10
  166. package/src/client/hooks/workspace/diff-git-actions.ts +147 -0
  167. package/src/client/hooks/workspace/editor-view.ts +61 -0
  168. package/src/client/hooks/workspace/file-create.ts +41 -11
  169. package/src/client/hooks/workspace/file-listing.ts +56 -1
  170. package/src/client/hooks/workspace/file-tabs.test.ts +62 -5
  171. package/src/client/hooks/workspace/file-tabs.ts +24 -11
  172. package/src/client/hooks/workspace/file-tree-actions.ts +37 -5
  173. package/src/client/hooks/workspace/navbar-session-actions.ts +72 -0
  174. package/src/client/hooks/workspace/worktree/index.ts +165 -0
  175. package/src/client/lib/desktop/bridge.ts +45 -0
  176. package/src/client/lib/search-reveal.ts +1 -1
  177. package/src/client/main.tsx +5 -1
  178. package/src/client/tailwind.css +56 -8
  179. package/src/server/index.ts +7 -2
  180. package/src/server/lib/auth/guard.test.ts +28 -0
  181. package/src/server/lib/auth/guard.ts +10 -0
  182. package/src/server/lib/bundler/titlebar-css.test.ts +42 -1
  183. package/src/server/lib/db/schema.ts +4 -0
  184. package/src/server/lib/db/worktree-registry.server.ts +91 -0
  185. package/src/server/lib/fs/canonical.ts +52 -0
  186. package/src/server/lib/fs/relative-path.ts +48 -0
  187. package/src/server/lib/fs/root-worktree.test.ts +116 -0
  188. package/src/server/lib/fs/root.ts +39 -10
  189. package/src/server/lib/fs/worktree/carry.ts +134 -0
  190. package/src/server/lib/fs/worktree/create.test.ts +249 -0
  191. package/src/server/lib/fs/worktree/create.ts +262 -0
  192. package/src/server/lib/fs/worktree/errors.ts +43 -0
  193. package/src/server/lib/fs/worktree/list.test.ts +207 -0
  194. package/src/server/lib/fs/worktree/list.ts +142 -0
  195. package/src/server/lib/fs/worktree/location.test.ts +206 -0
  196. package/src/server/lib/fs/worktree/location.ts +114 -0
  197. package/src/server/lib/fs/worktree/project-id.ts +53 -0
  198. package/src/server/lib/fs/worktree/remove.test.ts +242 -0
  199. package/src/server/lib/fs/worktree/remove.ts +152 -0
  200. package/src/server/lib/fs/worktree/settings.server.ts +56 -0
  201. package/src/server/lib/fs/worktree/topology.ts +142 -0
  202. package/src/server/lib/lifecycle/claim-port.test.ts +156 -0
  203. package/src/server/lib/lifecycle/port-guard.test.ts +20 -164
  204. package/src/server/lib/lifecycle/port-guard.ts +39 -7
  205. package/src/server/lib/lifecycle/probe-health.test.ts +166 -0
  206. package/src/server/lib/lifecycle/probe.test.ts +77 -164
  207. package/src/server/lib/lifecycle/probe.ts +121 -13
  208. package/src/server/lib/omp/rpc/frame-fold-signals.test.ts +112 -0
  209. package/src/server/lib/omp/rpc/frame-fold.ts +10 -5
  210. package/src/server/lib/omp/session/sidebar-activity.server.test.ts +162 -0
  211. package/src/server/lib/omp/session/sidebar-activity.server.ts +70 -0
  212. package/src/server/lib/omp/session/sidebar-data.canonical.test.ts +155 -0
  213. package/src/server/lib/omp/session/sidebar-data.server.ts +28 -1
  214. package/src/server/lib/omp/session/telemetry/compute.ts +13 -1
  215. package/src/server/lib/omp/session/telemetry.test.ts +33 -0
  216. package/src/server/lib/panels/local-edits.server.ts +33 -0
  217. package/src/server/lib/panels/sync.fixture.test-util.ts +92 -0
  218. package/src/server/lib/panels/sync.local-edits.test.ts +150 -0
  219. package/src/server/lib/panels/sync.server.test.ts +125 -147
  220. package/src/server/lib/panels/sync.server.ts +153 -48
  221. package/src/server/lib/realtime/signals.server.ts +10 -0
  222. package/src/server/lib/realtime/topics/catalog.server.ts +4 -1
  223. package/src/server/lib/realtime/topics.server.ts +16 -0
  224. package/src/server/lib/updates/shell-assets.test.ts +114 -0
  225. package/src/server/lib/updates/shell-assets.ts +200 -0
  226. package/src/server/lib/updates/shell-cache.ts +105 -0
  227. package/src/server/lib/updates/shell-pages.test.ts +187 -0
  228. package/src/server/lib/updates/shell-pages.ts +157 -0
  229. package/src/server/lib/updates/shell-release-desktop.test.ts +166 -0
  230. package/src/server/lib/updates/shell-release.test.ts +59 -71
  231. package/src/server/lib/updates/shell-release.ts +178 -198
  232. package/src/server/routes/fs/action-create.test.ts +18 -13
  233. package/src/server/routes/fs/action-nested.test.ts +184 -0
  234. package/src/server/routes/fs/action.ts +47 -16
  235. package/src/server/routes/fs/git-merge.test.ts +11 -1
  236. package/src/server/routes/fs/read.test.ts +24 -7
  237. package/src/server/routes/fs/scope.test.ts +15 -10
  238. package/src/server/routes/fs/scope.ts +3 -14
  239. package/src/server/routes/index.ts +2 -0
  240. package/src/server/routes/updates/android.ts +6 -1
  241. package/src/server/routes/updates/desktop.ts +59 -0
  242. package/src/server/routes/updates/index.ts +2 -0
  243. package/src/server/routes/updates/ios.ts +5 -1
  244. package/src/server/routes/worktrees/create.ts +138 -0
  245. package/src/server/routes/worktrees/index.ts +25 -0
  246. package/src/server/routes/worktrees/list.ts +134 -0
  247. package/src/server/routes/worktrees/remove.ts +102 -0
  248. package/src/server/routes/worktrees/routes.test.ts +326 -0
  249. package/src/server/routes/worktrees/shared.ts +90 -0
  250. package/src/shared/lib/chat/timeline/omp-callbacks.test.ts +36 -0
  251. package/src/shared/lib/chat/timeline/omp-callbacks.ts +18 -0
  252. package/src/shared/lib/chat/timeline/run-signals.test.ts +2 -0
  253. package/src/shared/lib/code/editor/comment-syntax.test.ts +37 -0
  254. package/src/shared/lib/code/editor/comment-syntax.ts +55 -0
  255. package/src/shared/lib/code/editor/find-replace.test.ts +134 -0
  256. package/src/shared/lib/code/editor/{find.ts → find-replace.ts} +23 -107
  257. package/src/shared/lib/code/editor/{find.test.ts → find-scan.test.ts} +19 -73
  258. package/src/shared/lib/code/editor/find-scan.ts +177 -0
  259. package/src/shared/lib/code/editor/keymap.ts +3 -3
  260. package/src/shared/lib/code/editor/lines.test.ts +7 -55
  261. package/src/shared/lib/code/editor/lines.ts +8 -48
  262. package/src/shared/lib/code/editor/marks.test.ts +4 -5
  263. package/src/shared/lib/code/editor/marks.ts +13 -25
  264. package/src/shared/lib/code/editor/theme-loader.ts +97 -0
  265. package/src/shared/lib/code/editor/themes.generated.ts +156 -0
  266. package/src/shared/lib/code/editor/themes.test.ts +165 -0
  267. package/src/shared/lib/code/editor/themes.ts +165 -0
  268. package/src/shared/lib/code/highlight-memo.test.ts +103 -0
  269. package/src/shared/lib/code/highlight-memo.ts +149 -0
  270. package/src/shared/lib/code/highlighter-warmup.ts +112 -0
  271. package/src/shared/lib/code/highlighter.ts +3 -1
  272. package/src/shared/lib/code/idle.ts +37 -0
  273. package/src/shared/lib/code/outline/rows.test.ts +96 -0
  274. package/src/shared/lib/code/outline/rows.ts +94 -0
  275. package/src/shared/lib/code/syntax-highlight.test.ts +1 -79
  276. package/src/shared/lib/code/syntax-highlight.ts +2 -3
  277. package/src/shared/lib/fs/file-kind-classify.test.ts +25 -11
  278. package/src/shared/lib/fs/file-kind.ts +47 -13
  279. package/src/shared/lib/fs/file-tree.test.ts +183 -0
  280. package/src/shared/lib/fs/file-tree.ts +159 -0
  281. package/src/shared/lib/fs/file-url.test.ts +52 -0
  282. package/src/shared/lib/fs/file-url.ts +37 -0
  283. package/src/shared/lib/fs/patch-sides.test.ts +114 -0
  284. package/src/shared/lib/fs/patch-sides.ts +139 -0
  285. package/src/shared/lib/fs/paths.test.ts +24 -1
  286. package/src/shared/lib/fs/paths.ts +17 -0
  287. package/src/shared/lib/fs/worktree/settings-keys.ts +18 -0
  288. package/src/shared/lib/omp/session/prewarm.ts +7 -0
  289. package/src/shared/lib/realtime/protocol.ts +16 -0
  290. package/src/shared/lib/ui/toolbar-overflow.test.ts +83 -0
  291. package/src/shared/lib/ui/toolbar-overflow.ts +86 -0
  292. package/src/shared/lib/updates/shell-arch.ts +54 -0
  293. package/src/shared/lib/updates/shell-ua.ts +24 -4
  294. package/src/shared/lib/workspace/active-project.ts +23 -0
  295. package/src/shared/lib/workspace/editor-view-mode.test.ts +70 -0
  296. package/src/shared/lib/workspace/editor-view-mode.ts +48 -0
  297. package/src/shared/lib/workspace/panel-widths.test.ts +30 -0
  298. package/src/shared/lib/workspace/panel-widths.ts +25 -5
  299. package/src/shared/lib/workspace/session-state/cache.ts +159 -0
  300. package/src/shared/lib/workspace/session-state/harness.test-util.ts +55 -0
  301. package/src/shared/lib/workspace/session-state/merge.ts +100 -0
  302. package/src/shared/lib/workspace/session-state/store-load.test.ts +206 -0
  303. package/src/shared/lib/workspace/session-state/store.test.ts +17 -139
  304. package/src/shared/lib/workspace/session-state/store.ts +92 -130
  305. package/src/shared/lib/workspace/workspace-modules.test.ts +49 -1
  306. package/src/shared/lib/workspace/workspace-panel-widths.ts +124 -0
  307. package/src/shared/lib/workspace/worktree/branch-name.test.ts +95 -0
  308. package/src/shared/lib/workspace/worktree/branch-name.ts +115 -0
  309. package/src/shared/lib/workspace/worktree/types.ts +104 -0
  310. package/src/shared/types/fs.ts +10 -0
  311. package/src/shared/types/realtime.ts +29 -0
  312. package/src/shared/types/settings/state.ts +10 -0
  313. package/src/shared/types/workspace.ts +14 -0
  314. package/src/test-support/file-explorer-disk.test-util.ts +76 -0
  315. package/src/test-support/file-explorer.test-util.ts +71 -40
  316. package/src/test-support/loopback.ts +183 -0
  317. package/dist/client/static/js/index-nzyjf92m.css +0 -1
  318. package/src/client/components/common/code-editor/handler.test.ts +0 -274
  319. package/src/client/components/common/code-editor/handler.ts +0 -175
  320. package/src/client/components/common/code-editor/index.tsx +0 -314
  321. package/src/client/components/common/code-surface/measure.ts +0 -148
  322. package/src/client/components/workspace/diff-panel/SplitView.tsx +0 -209
  323. package/src/client/components/workspace/diff-panel/UnifiedView.tsx +0 -150
  324. package/src/client/hooks/editor/scroll-container.test.ts +0 -102
  325. package/src/client/hooks/editor/scroll-container.ts +0 -26
  326. package/src/client/hooks/editor/use-code-window.ts +0 -100
  327. package/src/client/hooks/editor/use-editor-history.test.ts +0 -216
  328. package/src/client/hooks/editor/use-editor-history.ts +0 -134
  329. package/src/client/hooks/editor/use-occurrences.test.ts +0 -236
  330. package/src/client/hooks/editor/use-occurrences.ts +0 -98
  331. package/src/shared/lib/code/editor/commands.test.ts +0 -155
  332. package/src/shared/lib/code/editor/commands.ts +0 -260
  333. package/src/shared/lib/code/editor/edits.test.ts +0 -100
  334. package/src/shared/lib/code/editor/edits.ts +0 -122
  335. package/src/shared/lib/code/editor/history.test.ts +0 -118
  336. package/src/shared/lib/code/editor/history.ts +0 -101
  337. package/src/shared/lib/code/editor/layers.test.ts +0 -77
  338. package/src/shared/lib/code/editor/layers.ts +0 -67
  339. package/src/shared/lib/code/editor/line-edits.test.ts +0 -131
  340. package/src/shared/lib/code/editor/line-edits.ts +0 -159
  341. package/src/shared/lib/code/editor/occurrences.test.ts +0 -117
  342. package/src/shared/lib/code/editor/occurrences.ts +0 -111
  343. package/src/shared/lib/code/editor/reset-css.test.ts +0 -35
  344. package/src/shared/lib/code/editor/reset-css.ts +0 -32
  345. package/src/shared/lib/code/editor/text-commands.test.ts +0 -178
  346. package/src/shared/lib/code/editor/text-commands.ts +0 -154
  347. package/src/shared/lib/code/lazy-window.test.ts +0 -161
  348. package/src/shared/lib/code/lazy-window.ts +0 -156
  349. package/src/shared/lib/code/windowed-highlight.ts +0 -114
  350. package/src/shared/lib/fs/diff-parser.test.ts +0 -116
  351. package/src/shared/lib/fs/diff-parser.ts +0 -213
@@ -0,0 +1,112 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * Pay the tokenizer's start-up cost before the reader taps anything.
8
+ *
9
+ * `EAGER_LANGS` are loaded in the background at boot, but loading a grammar is
10
+ * not compiling it: the grammar's regexes are built and JIT-ed on the first
11
+ * tokenize, which is why the FIRST expand of a card in a given language measured
12
+ * 220–300 ms at 4× CPU while the same card re-expanded in 33 ms. That first
13
+ * expand is the one a reader actually experiences — a transcript opens with
14
+ * `read` and `bash` cards.
15
+ *
16
+ * So each eager grammar is tokenized once, on a tiny sample, while the main
17
+ * thread is idle. The sample's markup is discarded; the work being paid for is
18
+ * the compile behind it. One language per idle slice, so a slow grammar cannot
19
+ * become the long task this exists to prevent — and a grammar that has not
20
+ * landed yet is retried rather than skipped, because tokenizing before it
21
+ * arrives compiles nothing.
22
+ *
23
+ * Booted from `main.tsx` rather than from `highlighter.ts`: the warm-up needs
24
+ * the memo, which needs the highlighter, and an import in the other direction
25
+ * would close that cycle.
26
+ */
27
+
28
+ import { isLanguageReady } from '@/shared/lib/code/highlighter-lazy';
29
+ import { highlightBlock } from '@/shared/lib/code/highlight-memo';
30
+ import { scheduleIdle } from '@/shared/lib/code/idle';
31
+ import { EAGER_LANGS } from '@/shared/lib/code/shiki-langs';
32
+
33
+ /**
34
+ * A few lines that make the grammar walk its own rule set: a keyword, a string,
35
+ * a comment, a call and a block. The exact text does not matter — the compile
36
+ * does — so these are deliberately tiny.
37
+ */
38
+ const SAMPLES: Record<string, string> = {
39
+ typescript: '// warm\nconst value: number = 1;\nfunction read(input: string) {\n return input;\n}\n',
40
+ tsx: '// warm\nexport const View = ({ title }: { title: string }) => (\n <div className="row">{title}</div>\n);\n',
41
+ javascript: '// warm\nconst value = 1;\nfunction read(input) {\n return input;\n}\n',
42
+ jsx: '// warm\nexport const View = ({ title }) => (\n <div className="row">{title}</div>\n);\n',
43
+ json: '{\n "name": "warm",\n "items": [1, 2, 3],\n "ok": true\n}\n',
44
+ bash: '#!/bin/sh\n# warm\nset -e\nfor file in *.ts; do\n echo "$file"\ndone\n',
45
+ python: '# warm\ndef read(value: str) -> str:\n return value.strip()\n',
46
+ markdown: '# warm\n\nA paragraph with **bold** text and a `code` span.\n\n- item\n',
47
+ yaml: 'name: warm\nitems:\n - one\n - two\nenabled: true\n',
48
+ diff: '--- a/file.ts\n+++ b/file.ts\n@@ -1,2 +1,2 @@\n-old line\n+new line\n context\n',
49
+ };
50
+
51
+ const DEFAULT_SAMPLE = '// warm\nconst value = 1;\nfunction read(input) {\n return input;\n}\n';
52
+
53
+ /** How many idle slices a not-yet-loaded grammar is retried for. */
54
+ const MAX_WAIT_SLICES = 120;
55
+
56
+ export interface WarmOptions {
57
+ /** Languages to warm; defaults to the eager set. */
58
+ languages?: readonly string[];
59
+ /** The tokenizer; injected so a test can observe the schedule. */
60
+ highlight?: (code: string, language: string) => void;
61
+ /** Grammar readiness; injected for the same reason. */
62
+ ready?: (language: string) => boolean;
63
+ /** The idle scheduler; injected so a test can drive it synchronously. */
64
+ schedule?: (run: () => void) => () => void;
65
+ }
66
+
67
+ let started = false;
68
+
69
+ /**
70
+ * Warm every eager grammar once per page load, one per idle slice.
71
+ *
72
+ * Idempotent: a second call is a no-op, so a re-evaluated client entry cannot
73
+ * re-tokenize every language.
74
+ */
75
+ export function warmSyntaxTokenizers(options: WarmOptions = {}): void {
76
+ if (started) return;
77
+ if (typeof window === 'undefined') return;
78
+ started = true;
79
+
80
+ const languages = options.languages ?? EAGER_LANGS;
81
+ const highlight = options.highlight ?? ((code: string, language: string) => { highlightBlock(code, language); });
82
+ const ready = options.ready ?? isLanguageReady;
83
+ const schedule = options.schedule ?? ((run: () => void) => scheduleIdle(run));
84
+
85
+ const queue = [...languages];
86
+ let waiting = 0;
87
+ const step = () => {
88
+ const language = queue.shift();
89
+ if (language === undefined) return;
90
+ if (!ready(language)) {
91
+ // The grammar is still being fetched. Warming it now would compile
92
+ // nothing and the sample would not even be cacheable.
93
+ if (++waiting <= MAX_WAIT_SLICES) queue.push(language);
94
+ schedule(step);
95
+ return;
96
+ }
97
+ waiting = 0;
98
+ try {
99
+ highlight(SAMPLES[language] ?? DEFAULT_SAMPLE, language);
100
+ } catch {
101
+ // A grammar that cannot warm is not an error: the first real highlight
102
+ // falls back to escaped text and retries when the grammar lands.
103
+ }
104
+ schedule(step);
105
+ };
106
+ schedule(step);
107
+ }
108
+
109
+ /** Test/restart seam: forget that the warm-up already ran. */
110
+ export function resetTokenizerWarmup(): void {
111
+ started = false;
112
+ }
@@ -69,7 +69,9 @@ async function boot(): Promise<HighlighterCore | null> {
69
69
  notify();
70
70
  // Grammars arrive on demand (`ensureLanguage`); the eager set is only a
71
71
  // head start for the languages a chat session hits immediately, and boot
72
- // does not wait on it.
72
+ // does not wait on it. `main.tsx` pays the tokenizer's compile cost once
73
+ // the eager set has landed — loading a grammar is not compiling it, and the
74
+ // first card expand used to pay that compile inside the tap.
73
75
  void ensureLanguages(highlighter, EAGER_LANGS);
74
76
  return highlighter;
75
77
  } catch (error) {
@@ -0,0 +1,37 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * Run work when the main thread is next free.
8
+ *
9
+ * Tokenizing a tool card's code is the expensive half of expanding it — measured
10
+ * at 730–1484 ms on a throttled phone for a `read`/`grep` card — and running it
11
+ * inside the render that a tap scheduled put all of that in the tap's own task:
12
+ * the card appeared only after the grammar had walked every line. Deferring it
13
+ * costs one plain-text frame and moves the work out of the interaction.
14
+ *
15
+ * `requestIdleCallback` is the browser's own "nothing else is waiting" signal;
16
+ * the timeout is a FLOOR on latency rather than a budget, so a busy page still
17
+ * paints the colour within a quarter second. Runtimes without it (happy-dom,
18
+ * older Safari) fall back to the next macrotask.
19
+ */
20
+
21
+ const IDLE_TIMEOUT_MS = 250;
22
+
23
+ type IdleHost = {
24
+ requestIdleCallback?: (callback: () => void, options?: { timeout: number }) => number;
25
+ cancelIdleCallback?: (handle: number) => void;
26
+ };
27
+
28
+ /** Schedule `run`; the returned function cancels it. */
29
+ export function scheduleIdle(run: () => void, timeoutMs: number = IDLE_TIMEOUT_MS): () => void {
30
+ const host = globalThis as unknown as IdleHost;
31
+ if (typeof host.requestIdleCallback === 'function') {
32
+ const handle = host.requestIdleCallback(run, { timeout: timeoutMs });
33
+ return () => host.cancelIdleCallback?.(handle);
34
+ }
35
+ const handle = setTimeout(run, 0);
36
+ return () => clearTimeout(handle);
37
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * The outline's two bounds.
8
+ *
9
+ * Both exist because a large file's outline is not a long list, it is a DOM:
10
+ * measured on a 10 MB source with 63 618 declarations, the panel mounted
11
+ * 446 308 nodes and a keystroke cost 953 ms (against the editor's own 34 ms).
12
+ * These cases pin the arithmetic that keeps that from happening, and the
13
+ * sentences that stop a truncated list from reading as a complete one.
14
+ */
15
+
16
+ import { describe, expect, test } from 'bun:test';
17
+ import {
18
+ MAX_OUTLINE_ROWS,
19
+ MAX_OUTLINE_SCAN_CHARS,
20
+ outlineTooLargeNotice,
21
+ outlineTruncatedNotice,
22
+ outlineWindow,
23
+ } from '@/shared/lib/code/outline/rows';
24
+ import type { OutlineSymbol } from '@/shared/lib/code/outline/types';
25
+
26
+ function symbols(count: number): OutlineSymbol[] {
27
+ return Array.from({ length: count }, (_, i) => ({
28
+ name: `fn${i}`,
29
+ kind: 'function' as const,
30
+ line: i + 1,
31
+ column: 0,
32
+ depth: 0,
33
+ }));
34
+ }
35
+
36
+ describe('outlineWindow', () => {
37
+ test('a list under the cap is returned whole, in source order', () => {
38
+ const all = symbols(5);
39
+ const window = outlineWindow(all);
40
+
41
+ expect(window.rows).toEqual(all);
42
+ expect(window.total).toBe(5);
43
+ expect(window.truncated).toBe(false);
44
+ });
45
+
46
+ test('exactly the cap is not truncated', () => {
47
+ const window = outlineWindow(symbols(MAX_OUTLINE_ROWS));
48
+
49
+ expect(window.rows).toHaveLength(MAX_OUTLINE_ROWS);
50
+ expect(window.truncated).toBe(false);
51
+ });
52
+
53
+ test('past the cap the list is the FIRST rows, and it says how many there are', () => {
54
+ const window = outlineWindow(symbols(MAX_OUTLINE_ROWS + 120));
55
+
56
+ expect(window.rows).toHaveLength(MAX_OUTLINE_ROWS);
57
+ expect(window.total).toBe(MAX_OUTLINE_ROWS + 120);
58
+ expect(window.truncated).toBe(true);
59
+ // Source order, not a sample: the outline is a map of the document, so the
60
+ // top of the file is what a window shows.
61
+ expect(window.rows[0].name).toBe('fn0');
62
+ expect(window.rows[MAX_OUTLINE_ROWS - 1].name).toBe(`fn${MAX_OUTLINE_ROWS - 1}`);
63
+ });
64
+
65
+ test('an empty list is not truncated', () => {
66
+ const window = outlineWindow([]);
67
+
68
+ expect(window.rows).toEqual([]);
69
+ expect(window.total).toBe(0);
70
+ expect(window.truncated).toBe(false);
71
+ });
72
+ });
73
+
74
+ describe('outlineTooLargeNotice', () => {
75
+ test('a buffer at the scan ceiling is still outlined', () => {
76
+ expect(outlineTooLargeNotice(MAX_OUTLINE_SCAN_CHARS)).toBeNull();
77
+ expect(outlineTooLargeNotice(1024)).toBeNull();
78
+ });
79
+
80
+ test('past it the sentence names the size, so the empty panel is explained', () => {
81
+ const notice = outlineTooLargeNotice(MAX_OUTLINE_SCAN_CHARS + 1);
82
+
83
+ expect(notice).not.toBeNull();
84
+ expect(notice).toContain('8.0 MB');
85
+ expect(notice).toContain('too large to outline');
86
+ });
87
+ });
88
+
89
+ describe('outlineTruncatedNotice', () => {
90
+ test('it names both the shown count and the total, thousands separated', () => {
91
+ const notice = outlineTruncatedNotice(63618);
92
+
93
+ expect(notice).toContain('500');
94
+ expect(notice).toContain('63,618');
95
+ });
96
+ });
@@ -0,0 +1,94 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * How much of an outline a panel is allowed to draw, and when it is not drawn
8
+ * at all.
9
+ *
10
+ * Both bounds exist because a large file's outline is not a long list — it is a
11
+ * DOM. Measured in the browser on a 10 MB source file with 63 618 declarations:
12
+ * **446 308 DOM nodes, 415 MB of JS heap, and 953 ms per keystroke** while the
13
+ * panel was on screen (the editor's own cost is 34 ms). Expanding the section
14
+ * took 2.7 s. The scan is not the problem — it is 366 ms in Bun, once per pause
15
+ * — the ROWS are.
16
+ *
17
+ * Two answers, and they are different questions:
18
+ *
19
+ * - **`MAX_OUTLINE_ROWS` caps the list.** An outline is a map you scan by eye or
20
+ * jump from; nobody reads 63 000 rows, and the panel says how many it is not
21
+ * showing rather than silently truncating. Same shape as the search panel's
22
+ * `MAX_SEARCH_RESULTS`.
23
+ * - **`MAX_OUTLINE_SCAN_CHARS` skips the scan above a size where it costs more
24
+ * than the list is worth.** At 10 MB a scan is ~366 ms of blocked main thread
25
+ * every time the buffer settles, which is a stutter per typing pause. The
26
+ * sentence says so instead of leaving the panel empty, which would read as
27
+ * "this file has no declarations".
28
+ *
29
+ * The size is counted in CODE UNITS (`content.length`), not bytes: measuring
30
+ * UTF-8 bytes would mean encoding the whole buffer to answer a guard, and for
31
+ * the ASCII that dominates source files the two numbers are the same. A file of
32
+ * multibyte characters therefore trips the guard slightly early — the safe
33
+ * direction.
34
+ */
35
+
36
+ import type { OutlineSymbol } from '@/shared/lib/code/outline/types';
37
+
38
+ /**
39
+ * Rows a panel renders. Past this the list is truncated with a count, not
40
+ * silently shortened.
41
+ */
42
+ export const MAX_OUTLINE_ROWS = 500;
43
+
44
+ /** Buffer size above which the outline is not scanned at all. */
45
+ /**
46
+ * Buffer size above which the outline is not scanned at all.
47
+ *
48
+ * The scan is one blocked stretch of main thread after each typing pause, and it
49
+ * measured 38 ms at 1 MB, 160 ms at 5 MB, 262 ms at 10 MB and 519 ms at 20 MB —
50
+ * linear in the buffer. 8 MiB keeps that pause under ~210 ms, which is less than
51
+ * the debounce that schedules it and therefore reads as "the list settled"
52
+ * rather than as a stutter.
53
+ *
54
+ * Past it the panel says the file is too large to outline instead of scanning
55
+ * it: a declaration list for a 30 MB file is not worth a half-second freeze on
56
+ * every pause, and an empty list would read as "no declarations here".
57
+ */
58
+ export const MAX_OUTLINE_SCAN_CHARS = 8 * 1024 * 1024;
59
+
60
+ export interface OutlineWindow {
61
+ /** The rows to render — at most `MAX_OUTLINE_ROWS`. */
62
+ rows: OutlineSymbol[];
63
+ /** Symbols the scan actually found, including the ones not rendered. */
64
+ total: number;
65
+ /** True when `rows` is shorter than `total`. */
66
+ truncated: boolean;
67
+ }
68
+
69
+ /**
70
+ * The rows a panel should render, and whether it is showing all of them.
71
+ *
72
+ * Source order is preserved: an outline is a map of a document, so the first N
73
+ * declarations are the top of the file rather than an arbitrary sample.
74
+ */
75
+ export function outlineWindow(symbols: readonly OutlineSymbol[]): OutlineWindow {
76
+ const total = symbols.length;
77
+ if (total <= MAX_OUTLINE_ROWS) return { rows: symbols as OutlineSymbol[], total, truncated: false };
78
+ return { rows: symbols.slice(0, MAX_OUTLINE_ROWS), total, truncated: true };
79
+ }
80
+
81
+ /** The sentence printed under a truncated list. */
82
+ export function outlineTruncatedNotice(total: number): string {
83
+ return `Showing the first ${MAX_OUTLINE_ROWS.toLocaleString()} of ${total.toLocaleString()} symbols.`;
84
+ }
85
+
86
+ /**
87
+ * The sentence a panel prints instead of rows when the buffer is past the scan
88
+ * ceiling, or null when it is not.
89
+ */
90
+ export function outlineTooLargeNotice(contentLength: number): string | null {
91
+ if (contentLength <= MAX_OUTLINE_SCAN_CHARS) return null;
92
+ const mb = contentLength / (1024 * 1024);
93
+ return `This file is ${mb.toFixed(1)} MB — too large to outline.`;
94
+ }
@@ -6,10 +6,9 @@
6
6
  import { beforeAll, describe, expect, test } from 'bun:test';
7
7
 
8
8
  import { bootSyntax, onLanguageReady, requestLanguage } from '@/shared/lib/code/highlighter';
9
- import { findMatches } from '@/shared/lib/code/editor/find';
9
+ import { findMatches } from '@/shared/lib/code/editor/find-scan';
10
10
  import { getLanguageFromPath } from '@/shared/lib/code/language';
11
11
  import { highlightCode, highlightLines } from '@/shared/lib/code/syntax-highlight';
12
- import { highlightCodeWindow } from '@/shared/lib/code/windowed-highlight';
13
12
 
14
13
  /** Shiki emits `<span class="shiki">…` with dual-theme CSS variables per token. */
15
14
  const SHIKI_RE = /<span class="shiki">/;
@@ -115,54 +114,6 @@ describe('highlightCode', () => {
115
114
  });
116
115
  });
117
116
 
118
- describe('highlightCodeWindow', () => {
119
- const source = Array.from({ length: 120 }, (_, index) =>
120
- index === 0 ? 'export const first = 1;' : `export const value${index} = ${index}; // line ${index}`,
121
- ).join('\n');
122
-
123
- /** The wrapper the app's `.shiki span` colour rule depends on. */
124
- const unwrap = (html: string) => html.replace(/^<span class="shiki">/, '').replace(/<\/span>$/, '');
125
-
126
- test('wraps the window in the class the token colours are scoped to', () => {
127
- const window = highlightCodeWindow(source, 'typescript', { start: 0, end: 3 });
128
-
129
- // Without this class no `.shiki span` rule matches, so the tokens would
130
- // render in the plain ink colour.
131
- expect(window).toMatch(SHIKI_RE);
132
- expect(window.endsWith('</span>')).toBe(true);
133
- });
134
-
135
- test('returns only the requested lines, joined as the editor renders them', () => {
136
- const window = highlightCodeWindow(source, 'typescript', { start: 10, end: 13 });
137
- const rows = highlightLines(source, 'typescript');
138
-
139
- expect(unwrap(window).split('\n')).toEqual(rows.slice(10, 13));
140
- });
141
-
142
- test('keeps multi-line grammar state from the lines above the window', () => {
143
- const commented = ['/*', ' * a block comment', ' * spanning several lines', ' */', 'const after = 1;'].join('\n');
144
- const rows = highlightLines(commented, 'typescript');
145
- // Line 2 sits inside the comment, whose opening line is outside the window.
146
- const window = highlightCodeWindow(commented, 'typescript', { start: 2, end: 4 });
147
-
148
- expect(unwrap(window).split('\n')).toEqual(rows.slice(2, 4));
149
- });
150
-
151
- test('renders an over-long line as plain text instead of tokenizing it', () => {
152
- const minified = `const bundled = "${'x'.repeat(200)}";\nconst next = 1;`;
153
- const window = highlightCodeWindow(minified, 'javascript', { start: 0, end: 2 }, { maxLineLength: 50 });
154
-
155
- expect(window).toContain('const bundled');
156
- expect(unwrap(window).split('\n')[0]).not.toContain('--shiki-light:');
157
- expect(unwrap(window).split('\n')[1]).toContain('--shiki-light:');
158
- });
159
-
160
- test('returns nothing for a window outside the document', () => {
161
- expect(highlightCodeWindow('const a = 1;', 'typescript', { start: 5, end: 9 })).toBe('');
162
- expect(highlightCodeWindow('const a = 1;', 'typescript', { start: 0, end: 0 })).toBe('');
163
- });
164
- });
165
-
166
117
  describe('find marks', () => {
167
118
  const code = 'const alpha = 1;\nconst beta = alpha + 2;\n';
168
119
  const PLAIN = { matchCase: false, wholeWord: false, isRegex: false };
@@ -206,35 +157,6 @@ describe('find marks', () => {
206
157
  expect(plain).not.toContain('<mark');
207
158
  });
208
159
 
209
- test('keeps a windowed match on its own line, re-based to the slice', () => {
210
- const marks = findMatches(code, 'alpha', PLAIN).matches;
211
- // The window renders line 1 only. The match on line 0 must not reappear on
212
- // line 1's text, and the current index (line 0's match) must not mark
213
- // anything here.
214
- const window = highlightCodeWindow(code, 'typescript', { start: 1, end: 2 }, { marks, currentMark: 0 });
215
-
216
- expect(marked(window).join('')).toBe('alpha');
217
- expect(window).not.toContain('data-find-current');
218
- });
219
-
220
- test('re-bases the current index onto the marks inside the window', () => {
221
- const marks = findMatches(code, 'alpha', PLAIN).matches;
222
- const window = highlightCodeWindow(code, 'typescript', { start: 1, end: 2 }, { marks, currentMark: 1 });
223
-
224
- expect(window.match(/data-find-current/g)?.length).toBe(1);
225
- expect(marked(window).join('')).toBe('alpha');
226
- });
227
-
228
- test('a mark reaching past the window is clipped to it', () => {
229
- // The last line has no trailing newline, so a mark that runs to the end of
230
- // the slice would be clamped by the rebase.
231
- const window = highlightCodeWindow(code, 'typescript', { start: 1, end: 3 }, {
232
- marks: [{ start: 30, end: 999 }],
233
- currentMark: 0,
234
- });
235
-
236
- expect(marked(window).join('')).toBe('alpha + 2;');
237
- });
238
160
  });
239
161
 
240
162
  describe('getLanguageFromPath', () => {
@@ -168,12 +168,11 @@ export function highlightLines(
168
168
  const rawLines = code.split('\n');
169
169
  const marks = options.marks;
170
170
  const currentMark = options.currentMark ?? -1;
171
- const occurrenceFrom = options.occurrenceFrom ?? -1;
172
171
  // A grammar that has not arrived yet, or a highlighter that failed to boot,
173
172
  // still gets the find marks: the widget's feedback must not depend on Shiki.
174
173
  const plain = (lineIndex: number): string => {
175
174
  if (!marks || marks.length === 0) return escapeCode(rawLines[lineIndex]);
176
- return tokenMarkup(rawLines[lineIndex], lineStartOffset(code, lineIndex), marks, currentMark, occurrenceFrom);
175
+ return tokenMarkup(rawLines[lineIndex], lineStartOffset(code, lineIndex), marks, currentMark);
177
176
  };
178
177
  if (!code) return rawLines;
179
178
  const hl = getHighlighterSync();
@@ -191,7 +190,7 @@ export function highlightLines(
191
190
  let offset = lineStartOffset(code, lineIndex);
192
191
  return lineTokens
193
192
  .map((tok) => {
194
- const content = tokenMarkup(tok.content, offset, marks, currentMark, occurrenceFrom);
193
+ const content = tokenMarkup(tok.content, offset, marks, currentMark);
195
194
  offset += tok.content.length;
196
195
  if (!tok.htmlStyle) return content;
197
196
  const style = Object.entries(tok.htmlStyle)
@@ -28,6 +28,7 @@ import {
28
28
  PDF_MAX_FILE_BYTES,
29
29
  unsupportedEditorMessage,
30
30
  } from '@/shared/lib/fs/file-kind';
31
+ import { formatBytes } from '@/shared/lib/format/number';
31
32
 
32
33
  const bytes = (...values: number[]): Uint8Array => new Uint8Array(values);
33
34
 
@@ -96,8 +97,11 @@ describe('classifyEditorFile', () => {
96
97
 
97
98
  // The document ceiling does not reach plain text: a 6 MB source file is still
98
99
  // refused, because a textarea holds that buffer for the life of the tab.
99
- test('plain text keeps the text ceiling', () => {
100
- expect(classifyEditorFile({ path: 'big.ts', size: 6 * 1024 * 1024 })).toBe('too-large');
100
+ test('plain text keeps the text ceiling, which a real source file clears', () => {
101
+ // 6 MB was over the old 5 MiB ceiling and is a normal generated file, not a
102
+ // refusal. The ceiling moved; this case is what keeps it from moving back.
103
+ expect(classifyEditorFile({ path: 'big.ts', size: 6 * 1024 * 1024 })).toBe('text');
104
+ expect(classifyEditorFile({ path: 'big.log', size: EDITOR_MAX_FILE_BYTES })).toBe('text');
101
105
  expect(classifyEditorFile({ path: 'big.log', size: EDITOR_MAX_FILE_BYTES + 1 })).toBe('too-large');
102
106
  });
103
107
 
@@ -120,24 +124,34 @@ describe('unsupportedEditorMessage', () => {
120
124
  });
121
125
 
122
126
  test('a size refusal names both the actual size and the ceiling', () => {
123
- const message = unsupportedEditorMessage('too-large', 9 * 1024 * 1024);
124
- expect(message).toContain('9.00 MB');
125
- expect(message).toContain('5.00 MB');
127
+ // Derived, never spelled out: the point of the sentence is that its number
128
+ // is the one the classifier applied, so a literal here would pass while the
129
+ // two disagreed.
130
+ const message = unsupportedEditorMessage('too-large', 40 * 1024 * 1024);
131
+ expect(message).toContain('40.00 MB');
132
+ expect(message).toContain(formatBytes(EDITOR_MAX_FILE_BYTES));
126
133
  });
127
134
 
128
135
  test('a size refusal without a measured size names only the ceiling', () => {
129
136
  const message = unsupportedEditorMessage('too-large');
130
- expect(message).toContain('5.00 MB');
137
+ expect(message).toContain(formatBytes(EDITOR_MAX_FILE_BYTES));
131
138
  expect(message).not.toContain('undefined');
132
139
  });
133
140
 
141
+ test('the text ceiling leaves room for a real source file and stops short of the knee', () => {
142
+ // 32 MiB: measured, the last size before a tab's heap crosses a gigabyte and
143
+ // a 64 MB file stops opening at all (see the constant's own table).
144
+ expect(EDITOR_MAX_FILE_BYTES).toBe(32 * 1024 * 1024);
145
+ expect(EDITOR_MAX_FILE_BYTES).toBeGreaterThan(DOCUMENT_MAX_FILE_BYTES);
146
+ });
147
+
134
148
  // The sentence names the number the classifier actually applied: a PDF is
135
- // refused against 30 MB, so telling the user "5.00 MB" would describe a limit
136
- // the file was never measured against.
137
- test('a PDF size refusal names the PDF ceiling', () => {
149
+ // refused against its own ceiling, so naming the text one would describe a
150
+ // limit the file was never measured against.
151
+ test('a PDF size refusal names the PDF ceiling, not the text one', () => {
138
152
  const message = unsupportedEditorMessage('too-large', 31 * 1024 * 1024, 'docs/report.pdf');
139
153
  expect(message).toContain('31.00 MB');
140
- expect(message).toContain('30.00 MB');
141
- expect(message).not.toContain('5.00 MB');
154
+ expect(message).toContain(formatBytes(PDF_MAX_FILE_BYTES));
155
+ expect(message).not.toContain(formatBytes(EDITOR_MAX_FILE_BYTES));
142
156
  });
143
157
  });
@@ -143,16 +143,43 @@ export function getImageMimeFromBytes(bytes: Uint8Array): string | null {
143
143
  }
144
144
 
145
145
  /**
146
- * The largest file the editor will open when its bytes reach the page WHOLE.
146
+ * The largest text file the editor will open when its bytes reach the page
147
+ * WHOLE.
147
148
  *
148
- * Not a policy knob: the editor holds the whole file in one string and posts
149
- * the whole thing back on every save, so a ceiling has to exist. 5 MiB is above
150
- * any source file and well below the size at which a textarea stops responding.
149
+ * Not a policy knob: the editor holds the whole file in one string, CodeMirror
150
+ * holds a second representation of it, and the whole thing is posted back on
151
+ * every save — so a ceiling has to exist.
151
152
  *
152
- * A DOCX and an XLSX are parsed in the page too, but they get their own, higher
153
- * number — see `DOCUMENT_MAX_FILE_BYTES`.
153
+ * **32 MiB, measured.** The number used to be 5 MiB, and its justification died
154
+ * with the `<textarea>`: that limit existed because a textarea stops responding,
155
+ * and the surface is CodeMirror 6 now. Re-measured on the assembled app (a
156
+ * generated source file, Files panel open, Chromium):
157
+ *
158
+ * | File | Open | Keystroke | Save | JS heap |
159
+ * |---|---|---|---|---|
160
+ * | 5 MB | 99 ms | 17 ms | 289 ms | 73 MB |
161
+ * | 10 MB | 147 ms | 32 ms | 494 ms | 73 MB |
162
+ * | 20 MB | 212 ms | 34 ms | 969 ms | 73 MB |
163
+ * | 30 MB | 323 ms | 39 ms | 2 292 ms | — |
164
+ * | 40 MB | 283 ms | 45 ms | 1 793 ms | — |
165
+ * | 50 MB | 372 ms | 72 ms | 2 228 ms | **1 078 MB** |
166
+ * | 64 MB | never finished loading | — | — | — |
167
+ *
168
+ * The curve is flat through 40 MB and breaks at 50: the heap goes from a few
169
+ * hundred megabytes to over a gigabyte for ONE tab, and a 64 MB file does not
170
+ * open at all. 32 MiB is the last size with a comfortable margin below that
171
+ * knee — ten times any source file, and the point past which a tab is a risk to
172
+ * every other tab in the browser rather than a convenience.
173
+ *
174
+ * Two costs had to be removed before this number was reachable, and both were
175
+ * linear-in-file-size work paid per keystroke: the outline's scan-and-render of
176
+ * every declaration (`outline/rows.ts`), and a full `doc.toString()` on every
177
+ * render (`code-surface/index.tsx`).
178
+ *
179
+ * A DOCX and an XLSX are parsed in the page too, but they get their own number —
180
+ * see `DOCUMENT_MAX_FILE_BYTES`.
154
181
  */
155
- export const EDITOR_MAX_FILE_BYTES = 5 * 1024 * 1024;
182
+ export const EDITOR_MAX_FILE_BYTES = 32 * 1024 * 1024;
156
183
 
157
184
  /**
158
185
  * The largest Word document or workbook the editor opens.
@@ -160,9 +187,10 @@ export const EDITOR_MAX_FILE_BYTES = 5 * 1024 * 1024;
160
187
  * `mammoth` and `read-excel-file` parse the fetched `ArrayBuffer` in the page,
161
188
  * so the file does sit in memory — but it is unzipped and converted once and
162
189
  * then released, where a text buffer is held for as long as the tab is open and
163
- * re-posted on every save. That is why these get four times the text ceiling
164
- * rather than the text ceiling itself: the reason the 5 MiB limit exists (a
165
- * textarea that stops responding) does not apply to a read-only conversion.
190
+ * re-posted on every save. That is why these get a ceiling of their own rather
191
+ * than the text one: the reason the text limit exists (a buffer that must stay
192
+ * editable, and a save that re-posts it) does not apply to a read-only
193
+ * conversion.
166
194
  *
167
195
  * Measured on a 15 MB workbook: the fetch, the unzip and the grid render all
168
196
  * complete, and the panel stays responsive.
@@ -170,8 +198,13 @@ export const EDITOR_MAX_FILE_BYTES = 5 * 1024 * 1024;
170
198
  export const DOCUMENT_MAX_FILE_BYTES = 20 * 1024 * 1024;
171
199
 
172
200
  /**
173
- * The largest PDF the editor opens — above both, because a PDF is the one
174
- * document the page never holds.
201
+ * The largest PDF the editor opens.
202
+ *
203
+ * Its own number rather than the text ceiling, because a PDF is the one document
204
+ * the page never holds — and the only one whose limit is not about a buffer at
205
+ * all (see below). It is no longer the largest of the three: the TEXT ceiling
206
+ * moved past it, which is fine because the two numbers answer different
207
+ * questions rather than ranking a shared budget.
175
208
  *
176
209
  * `PdfView` points an `<iframe>` at `/api/fs/raw`, so the bytes go straight to
177
210
  * Chrome's own viewer, which fetches them with range requests (verified: a
@@ -180,7 +213,8 @@ export const DOCUMENT_MAX_FILE_BYTES = 20 * 1024 * 1024;
180
213
  * Nothing in Kabmo buffers the file, so the text ceiling has no reason to
181
214
  * apply to it — and applying it was the bug: a 6 MB PDF dragged from Finder was
182
215
  * refused as "larger than the 5.00 MB the editor opens" before the document
183
- * branch was ever reached, because the size test runs first.
216
+ * branch was ever reached, because the size test runs first. The 30 MB here is
217
+ * Chrome's own plugin's comfort zone, not Kabmo's memory.
184
218
  */
185
219
  export const PDF_MAX_FILE_BYTES = 30 * 1024 * 1024;
186
220