@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,208 +1,220 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
1
6
  /**
2
7
  * Which published native-shell artifact a browser or an installed shell should
3
8
  * download, and whether the installed one is behind.
4
9
  *
5
- * Neither shell is an npm package (see `.github/workflows/publish-android.yml`
6
- * and `publish-ios.yml`), so the npm probe that answers for the app and for
7
- * `omp` cannot answer here. The only record of a published shell is a GitHub
8
- * release asset, which means one unauthenticated REST read of the release LIST —
9
- * not `/releases/latest`, which answers the newest release of ANY kind (see
10
- * `fetchShellRelease`).
10
+ * No shell is an npm package (see `.github/workflows/publish-{android,ios,macos,
11
+ * windows,linux}.yml`), so the npm probe that answers for the app and for `omp`
12
+ * cannot answer here. The only record of a published shell is a GitHub release
13
+ * asset.
14
+ *
15
+ * ## Two reads, because one of them has a quota
11
16
  *
12
- * ONE module for both shells, because the question is identical and only the
13
- * asset naming differs. Two copies is how one of them starts reading the release
14
- * tag, or caching under a slot the other overwrites.
17
+ * The REST API is the precise read and the FIRST one tried. It is also
18
+ * unauthenticated at 60 requests an hour **per source IP**, and a phone on mobile
19
+ * data shares that IP with an entire carrier NAT — so `shell-pages` is the second
20
+ * path, and it is why the button appears at all on a bad day. See that module's
21
+ * own comment for the measured failure.
15
22
  *
16
- * Two facts travel back, and they answer different questions:
23
+ * ## What travels back
17
24
  *
18
- * - `url` is the asset to download. It is resolved from the release's OWN asset
19
- * list rather than built as `releases/latest/download/<name>`, because that
20
- * path needs the asset name to match exactly and the name carries the shell's
21
- * version (`Kabmo-0.5.0-android.apk`) — a name built here would be a second
22
- * copy of the workflow's naming rule, and the first release that renames it
23
- * 404s.
24
- * - `latest` is the version that asset ships, read from the asset name. The
25
- * release TAG cannot be used: the artifact is attached to the app's release,
26
- * whose tag names the SERVER version, and the two move independently.
25
+ * - `url` is what to download. For the mobile pair it is the artifact itself (an
26
+ * APK the shell installs, an IPA the release page explains). For the DESKTOP
27
+ * trio it is the artifact when the caller reported its ARCHITECTURE, and the
28
+ * release page otherwise: every desktop artifact is per-architecture, and a
29
+ * console served to another machine cannot guess. Naming the wrong arch's file
30
+ * would hand half the readers a build that will not launch.
31
+ * - `direct` says which of the two `url` is, so the client knows whether the
32
+ * offer can be installed from inside the shell.
33
+ * - `latest` is the version that asset ships, read from the asset NAME (see
34
+ * `shell-assets`). The release TAG cannot be used: the artifact is attached to
35
+ * the app's release, whose tag names the SERVER version.
27
36
  *
28
- * Failure is a normal answer here (`null`), never a throw: an offline box or a
29
- * rate-limited GitHub must not take the sidebar down, and a button that appears
30
- * because a probe failed would offer a download that does not exist.
37
+ * Failure is a normal answer here (`null`), never a throw: an offline box must
38
+ * not take the sidebar down, and a button that appears because a probe failed
39
+ * would offer a download that does not exist.
31
40
  */
32
41
 
33
42
  import { REPO } from '@/server/lib/updates/release-url';
43
+ import {
44
+ cacheSlot,
45
+ CACHE_MS,
46
+ githubApiBase,
47
+ gitHubHeaders,
48
+ shellCacheKey,
49
+ } from '@/server/lib/updates/shell-cache';
50
+ import { SHELL_ASSETS, selfUpdateArtifact, versionFromAssetName } from '@/server/lib/updates/shell-assets';
51
+ import { findAssetInPages } from '@/server/lib/updates/shell-pages';
52
+ import type { ShellArch } from '@/shared/lib/updates/shell-arch';
34
53
  import type { ShellKind } from '@/shared/lib/updates/shell-ua';
35
54
 
55
+ /**
56
+ * The test seam for the process-wide answer cache, re-exported here because this
57
+ * module is what owns the answers. The cache itself lives next door (`shell-cache`)
58
+ * so its key — shell AND architecture — is stated in one place.
59
+ */
60
+ export { resetShellReleaseCache } from '@/server/lib/updates/shell-cache';
61
+
36
62
  /** One published shell artifact, as the console needs it. */
37
63
  export interface ShellRelease {
38
- /** Asset download URL, or null when the release carries no artifact. */
64
+ /** Asset download URL, or the release page for a per-arch desktop build. */
39
65
  url: string | null;
40
- /** Version the asset name declares (`v1.2.3` → `1.2.3`), or null. */
66
+ /** Version the asset name declares (`Kabmo-1.2.3-android.apk` → `1.2.3`). */
41
67
  latest: string | null;
42
68
  /** The release page, for a "what changed" link. */
43
69
  releaseUrl: string | null;
70
+ /**
71
+ * True when `url` names the artifact itself rather than the release page.
72
+ *
73
+ * This is what tells the console whether the offer can be INSTALLED from
74
+ * inside the shell. It is true for the mobile pair (one file for every
75
+ * device) and for a desktop shell that reported its architecture — and false
76
+ * for a desktop probe without one, which still gets the page that lists every
77
+ * architecture.
78
+ */
79
+ direct: boolean;
44
80
  }
45
81
 
46
- /**
47
- * How each shell's asset is named by its publish workflow, plus the prefix it
48
- * carried BEFORE the rename.
49
- *
50
- * ONE scheme for every shell in the project — `<App>-<version>-<platform>.<ext>`:
51
- * `Kabmo-0.5.0-android.apk`, `Kabmo-0.1.0-ios.ipa`, and the desktop trio's
52
- * `Kabmo-0.1.2-mac-arm64.dmg` / `-win-x64-setup.exe` / `-linux-x86_64.tar.gz`.
53
- * The mobile pair carries no architecture because neither artifact is per-arch:
54
- * the APK is one universal build (the shell is pure Kotlin with no native
55
- * libraries, so there is nothing to split), and the IPA contains only the device
56
- * slice (`arm64-apple-ios`). A `-arm64` in those names would claim a distinction
57
- * that does not exist and would make the parser look for a second artifact.
58
- *
59
- * **`legacyPrefix` is not dead code.** These names are PARSED to answer "is a
60
- * newer shell published", and every release already on GitHub carries the old
61
- * spelling. Dropping it would make the console answer "no update" for an
62
- * installed shell the moment this ships — silently, because a name that does not
63
- * match reads exactly like a release with no artifact. The probe tries the
64
- * current prefix first, then the old one; new releases only ever write the
65
- * current one.
66
- */
67
- export const SHELL_ASSETS: Record<
68
- ShellKind,
69
- { prefix: string; extension: string; legacyPrefix: string; legacyExtension: string }
70
- > = {
71
- android: {
72
- prefix: 'Kabmo-',
73
- extension: '-android.apk',
74
- legacyPrefix: 'kabmo-android-v',
75
- legacyExtension: '.apk',
76
- },
77
- ios: { prefix: 'Kabmo-', extension: '-ios.ipa', legacyPrefix: 'kabmo-ios-v', legacyExtension: '.ipa' },
78
- };
79
-
80
- /**
81
- * `Kabmo-0.5.0-android.apk` → `0.5.0` (and the `.ipa` twin), plus the legacy
82
- * `kabmo-android-v0.5.0.apk` spelling every published release still carries.
83
- *
84
- * The version is the MIDDLE of the name in the current scheme and the TAIL of the
85
- * legacy one, so the two are parsed by different rules rather than by one that
86
- * tries to cover both.
87
- */
88
- export function versionFromAssetName(kind: ShellKind, name: string): string | null {
89
- const { prefix, extension, legacyPrefix, legacyExtension } = SHELL_ASSETS[kind];
90
-
91
- // Current: `<prefix><version><extension>`, where the prefix is the app's name
92
- // and the extension names the platform.
93
- if (name.startsWith(prefix) && name.endsWith(extension)) {
94
- const middle = name.slice(prefix.length, -extension.length);
95
- if (/^\d+\.\d+\.\d+$/.test(middle)) return middle;
96
- }
97
-
98
- // Legacy: `kabmo-android-v<version>.apk`. Kept for the releases already
99
- // published; no new release writes it.
100
- if (name.startsWith(legacyPrefix) && name.endsWith(legacyExtension)) {
101
- const middle = name.slice(legacyPrefix.length, -legacyExtension.length);
102
- const version = middle.startsWith('v') ? middle.slice(1) : middle;
103
- return /^\d+\.\d+\.\d+$/.test(version) ? version : null;
104
- }
105
-
106
- return null;
107
- }
82
+ /** The answer a probe gives when it found nothing: a valid result, not an error. */
83
+ const EMPTY_RELEASE: ShellRelease = { url: null, latest: null, releaseUrl: null, direct: false };
108
84
 
109
85
  /**
110
86
  * The newest release that actually carries this shell's artifact.
111
87
  *
112
88
  * NOT `/releases/latest`, which answers the newest release of ANY kind. This
113
- * repository publishes three artifacts from one push, and the packages are
114
- * released FIRST (`packages → marketplace → android → apk-build → app` in
89
+ * repository publishes several artifacts from one push, and the packages are
90
+ * released FIRST (`packages → marketplace → android → … → app` in
115
91
  * `release.yml`), so their `kabmo-sdk/v*` / `kabmo-ui/v*` releases are newer
116
- * than the app release that carries the shell. `@semantic-release/github` runs
92
+ * than the app release that carries the shells. `@semantic-release/github` runs
117
93
  * in both pipelines, so those are real releases — measured against a stub that
118
- * answered a package release: `/releases/latest` produced
119
- * `{url: null, latest: null}` and the sidebar's button disappeared even though
120
- * the APK sat on the release directly below.
94
+ * answered a package release: `/releases/latest` produced `{url: null, …}` and
95
+ * the sidebar's button disappeared even though the APK sat on the release
96
+ * directly below.
97
+ *
98
+ * `arch` is what a DESKTOP shell reports about its own machine, and it is the
99
+ * difference between "here is the page, pick your build" and "here is your
100
+ * installer". It is the THIRD parameter so every existing caller — including
101
+ * every test, which passes `fetchImpl` — keeps its shape.
121
102
  *
122
- * One request per shell, cached for five minutes on `globalThis` for the same
103
+ * Cached for five minutes on `globalThis` (see `shell-cache`) for the same
123
104
  * reason the database handle is anchored there: `bun --hot` re-evaluates this
124
- * module while the cache it would otherwise hold lives on. GitHub allows 60
125
- * unauthenticated reads an hour per address, and the sidebar asks on every
126
- * mount.
105
+ * module while the cache it would otherwise hold lives on.
127
106
  */
128
107
  export async function fetchShellRelease(
129
108
  kind: ShellKind,
130
109
  fetchImpl: typeof fetch = fetch,
110
+ arch: ShellArch | null = null,
131
111
  ): Promise<ShellRelease | null> {
132
- const cache = cacheSlot(kind);
112
+ const cache = cacheSlot<ShellRelease>(shellCacheKey(kind, arch));
133
113
  const now = Date.now();
134
114
  if (cache.entry && now - cache.at < CACHE_MS) return cache.entry;
135
115
 
116
+ const headers = gitHubHeaders();
117
+ const resolved =
118
+ (await readFromApi(kind, arch, fetchImpl, headers)) ??
119
+ (await readFromPages(kind, arch, fetchImpl, headers));
120
+ if (!resolved) return cache.entry ?? null;
121
+
122
+ cache.entry = resolved;
123
+ cache.at = now;
124
+ return resolved;
125
+ }
126
+
127
+ /** How many releases to look back through for the artifact. */
128
+ const RELEASE_SCAN = 20;
129
+
130
+ /**
131
+ * The REST read, or null when it did not answer with a usable release list.
132
+ *
133
+ * A non-2xx is not an error to report — it is the quota, and the caller has a
134
+ * second path.
135
+ */
136
+ async function readFromApi(
137
+ kind: ShellKind,
138
+ arch: ShellArch | null,
139
+ fetchImpl: typeof fetch,
140
+ headers: Record<string, string>,
141
+ ): Promise<ShellRelease | null> {
136
142
  try {
137
143
  const response = await fetchImpl(`${githubApiBase()}/repos/${REPO}/releases?per_page=${RELEASE_SCAN}`, {
138
- headers: {
139
- accept: 'application/vnd.github+json',
140
- // GitHub rejects a request with no User-Agent.
141
- 'user-agent': 'kabmo-console',
142
- },
144
+ headers: { accept: 'application/vnd.github+json', ...headers },
143
145
  });
144
- if (!response.ok) return cache.entry ?? null;
146
+ if (!response.ok) return null;
145
147
 
146
148
  const body = (await response.json()) as unknown;
147
149
  // A body that is not the release list is a failed probe, not an empty
148
- // repository: answering `{url: null, latest: null}` here would be
149
- // indistinguishable from "no release carries an artifact", and the caller
150
- // would cache that mistake for five minutes.
151
- if (!Array.isArray(body)) return cache.entry ?? null;
150
+ // repository: answering `{url: null, …}` here would be indistinguishable
151
+ // from "no release carries an artifact".
152
+ if (!Array.isArray(body)) return null;
152
153
 
153
154
  // Drafts are not published and prereleases are not what a console offers an
154
- // installed shell — the same two the `latest` endpoint excluded.
155
+ // installed shell.
155
156
  const published = body.filter(
156
157
  (entry): entry is Record<string, unknown> =>
157
158
  entry !== null && typeof entry === 'object' && entry.draft !== true && entry.prerelease !== true,
158
159
  );
159
160
 
160
161
  const release = published
161
- .map((entry) => ({ entry, asset: shellAsset(kind, entry.assets) }))
162
+ .map((entry) => ({ entry, asset: shellAsset(kind, arch, entry.assets) }))
162
163
  .find((candidate) => candidate.asset !== null);
163
164
 
164
- if (!release?.asset) {
165
- const fallback: ShellRelease = { url: null, latest: null, releaseUrl: null };
166
- cache.entry = fallback;
167
- cache.at = now;
168
- return fallback;
169
- }
165
+ if (!release?.asset) return EMPTY_RELEASE;
170
166
 
171
- const resolved: ShellRelease = {
172
- url: typeof release.asset.browser_download_url === 'string' ? release.asset.browser_download_url : null,
173
- latest: versionFromAssetName(kind, String(release.asset.name)),
167
+ return resolvedRelease(kind, arch, {
168
+ name: String(release.asset.name),
169
+ downloadUrl:
170
+ typeof release.asset.browser_download_url === 'string' ? release.asset.browser_download_url : null,
174
171
  releaseUrl: typeof release.entry.html_url === 'string' ? release.entry.html_url : null,
175
- };
176
-
177
- cache.entry = resolved;
178
- cache.at = now;
179
- return resolved;
172
+ });
180
173
  } catch {
181
- // Keep the last good answer rather than replacing it with a failure: a
182
- // momentary outage must not remove a download button that was valid.
183
- return cache.entry ?? null;
174
+ return null;
184
175
  }
185
176
  }
186
177
 
187
- /** How many releases to look back through for the artifact. */
188
- const RELEASE_SCAN = 20;
178
+ /** The public-pages read, wrapped so a failure is the caller's `null`. */
179
+ async function readFromPages(
180
+ kind: ShellKind,
181
+ arch: ShellArch | null,
182
+ fetchImpl: typeof fetch,
183
+ headers: Record<string, string>,
184
+ ): Promise<ShellRelease | null> {
185
+ try {
186
+ const found = await findAssetInPages(kind, fetchImpl, headers, arch);
187
+ if (found.kind === 'unreachable') return null;
188
+ if (found.kind === 'none') return EMPTY_RELEASE;
189
+ return resolvedRelease(kind, arch, { ...found.asset, releaseUrl: found.releaseUrl });
190
+ } catch {
191
+ return null;
192
+ }
193
+ }
189
194
 
190
195
  /**
191
196
  * The asset of a release that names this shell's artifact, or null.
192
197
  *
193
- * The CURRENT scheme wins over the legacy one, and that is not cosmetic. A
194
- * release that carries both — which happens the moment a rename lands and
195
- * `--clobber` has not yet replaced the old asset, or a hand-upload adds one
196
- * beside the other — would otherwise resolve to whichever the asset list
197
- * happened to order first. Measured: a release holding both
198
- * `kabmo-android-v0.4.0.apk` and `Kabmo-0.6.0-android.apk` answered `0.4.0`,
199
- * i.e. the console offered an update to a version it already had and hid the
200
- * newer one.
198
+ * Two questions, in this order:
201
199
  *
202
- * Both schemes are still ACCEPTED (a release with only the old spelling must
203
- * keep working), so the preference is a second pass rather than a filter.
200
+ * 1. **Does an architecture match?** When the caller named one, the only
201
+ * acceptable asset is the file that machine installs (`selfUpdateArtifact`).
202
+ * Everything else in the release — the other architecture, the `.dmg`, a
203
+ * `.deb` — is skipped, and the next release is tried.
204
+ * 2. **Which spelling?** The CURRENT scheme wins over the legacy one, and that
205
+ * is not cosmetic. A release that carries both — which happens the moment a
206
+ * rename lands and `--clobber` has not yet replaced the old asset, or a
207
+ * hand-upload adds one beside the other — would otherwise resolve to
208
+ * whichever the asset list happened to order first. Measured: a release
209
+ * holding both `kabmo-android-v0.4.0.apk` and `Kabmo-0.6.0-android.apk`
210
+ * answered `0.4.0`, i.e. the console offered an update to a version it
211
+ * already had and hid the newer one.
204
212
  */
205
- function shellAsset(kind: ShellKind, assets: unknown): Record<string, unknown> | null {
213
+ function shellAsset(
214
+ kind: ShellKind,
215
+ arch: ShellArch | null,
216
+ assets: unknown,
217
+ ): Record<string, unknown> | null {
206
218
  if (!Array.isArray(assets)) return null;
207
219
 
208
220
  const named = assets.filter(
@@ -213,70 +225,38 @@ function shellAsset(kind: ShellKind, assets: unknown): Record<string, unknown> |
213
225
  versionFromAssetName(kind, String((entry as Record<string, unknown>).name)) !== null,
214
226
  );
215
227
 
216
- const { prefix, extension } = SHELL_ASSETS[kind];
217
- return (
218
- named.find((asset) => {
219
- const name = String(asset.name);
220
- return name.startsWith(prefix) && name.endsWith(extension);
221
- }) ??
222
- named[0] ??
223
- null
224
- );
225
- }
226
-
227
- const CACHE_MS = 5 * 60_000;
228
-
229
- /**
230
- * Where the release read goes.
231
- *
232
- * Overridable for the same reason the npm probe honours `NPM_CONFIG_REGISTRY`: a
233
- * test and a rehearsal need to exercise the real parsing against a stub instead
234
- * of a live GitHub call, and hardcoding the host makes that impossible without
235
- * intercepting `fetch` globally.
236
- */
237
- function githubApiBase(): string {
238
- const configured = Bun.env.KABMO_GITHUB_API ?? '';
239
- return configured.trim().replace(/\/+$/, '') || 'https://api.github.com';
240
- }
228
+ if (arch) {
229
+ const exact = named.find(
230
+ (asset) => selfUpdateArtifact(kind, arch, String(asset.name)) !== null,
231
+ );
232
+ if (exact) return exact;
233
+ }
241
234
 
242
- interface CacheSlot {
243
- entry: ShellRelease | null;
244
- at: number;
235
+ return named.find((asset) => SHELL_ASSETS[kind].current.test(String(asset.name))) ?? named[0] ?? null;
245
236
  }
246
237
 
247
- type ShellCacheHost = typeof globalThis & {
248
- __kabmoShellRelease?: Partial<Record<ShellKind, CacheSlot>>;
249
- };
250
-
251
238
  /**
252
- * The cache slot for one shell.
239
+ * Turn one resolved asset into the payload.
253
240
  *
254
- * PER SHELL, not one shared slot: both probes read the same release list but
255
- * resolve different assets, so one slot would make the second shell answer with
256
- * the first one's artifact.
241
+ * `direct` is derived from the SAME decision that chose the asset: an arch was
242
+ * named and the asset is that arch's self-update file. Deriving it separately —
243
+ * from `directDownload`, or from the platform — is how the client ends up
244
+ * offering an install for a release page.
257
245
  */
258
- function cacheSlot(kind: ShellKind): CacheSlot {
259
- const host = globalThis as ShellCacheHost;
260
- host.__kabmoShellRelease ??= {};
261
- host.__kabmoShellRelease[kind] ??= { entry: null, at: 0 };
262
- return host.__kabmoShellRelease[kind] as CacheSlot;
263
- }
246
+ function resolvedRelease(
247
+ kind: ShellKind,
248
+ arch: ShellArch | null,
249
+ asset: { name: string; downloadUrl: string | null; releaseUrl: string | null },
250
+ ): ShellRelease {
251
+ const installable = arch !== null && selfUpdateArtifact(kind, arch, asset.name) !== null;
252
+ const direct = SHELL_ASSETS[kind].directDownload || installable;
264
253
 
265
- /**
266
- * Drop the cached answer for one shell (or every shell, with no argument).
267
- *
268
- * The cache lives on `globalThis` so `bun --hot` cannot orphan it, which also
269
- * means it outlives a test file: without this seam the second case in a suite
270
- * reads the first case's answer and passes for the wrong reason (measured — the
271
- * "malformed body" case returned the previous test's release). Production has
272
- * no caller; it exists because the cache is process-wide by design.
273
- */
274
- export function resetShellReleaseCache(kind?: ShellKind): void {
275
- const host = globalThis as ShellCacheHost;
276
- if (!kind) {
277
- host.__kabmoShellRelease = {};
278
- return;
279
- }
280
- host.__kabmoShellRelease ??= {};
281
- host.__kabmoShellRelease[kind] = { entry: null, at: 0 };
254
+ return {
255
+ // A per-arch desktop build cannot be named without an architecture — see
256
+ // the module comment — so the link is the page that lists every one.
257
+ url: direct ? asset.downloadUrl : asset.releaseUrl,
258
+ latest: versionFromAssetName(kind, asset.name),
259
+ releaseUrl: asset.releaseUrl,
260
+ direct: direct && asset.downloadUrl !== null,
261
+ };
282
262
  }
@@ -6,12 +6,12 @@
6
6
  /**
7
7
  * The Files panel's "New file" branch.
8
8
  *
9
- * Two rules are load-bearing and neither is visible from the happy path: the
10
- * name may not carry a path separator (the folder is named by `path`, and a
11
- * separator would turn a name into an arbitrary write under the root), and an
12
- * existing file is REFUSED rather than truncated. The second one is the one a
13
- * naive `Bun.write` gets wrong: `Bun.write` creates or truncates, so a name
14
- * that collides with a real file would silently blank it.
9
+ * Two rules are load-bearing and neither is visible from the happy path. The
10
+ * name may carry SEGMENTS (that is the nested-path grammar, and its refusals
11
+ * live in `action-nested.test.ts`), and an existing file is REFUSED rather
12
+ * than truncated — the second is the one a naive `Bun.write` gets wrong:
13
+ * `Bun.write` creates or truncates, so a name that collides with a real file
14
+ * would silently blank it.
15
15
  */
16
16
 
17
17
  import { afterEach, describe, expect, test } from 'bun:test';
@@ -34,7 +34,10 @@ async function create(folderPath: string, name: string) {
34
34
  form.append('path', folderPath);
35
35
  form.append('name', name);
36
36
  const res = await action({ request: new Request('http://localhost/api/fs/action', { method: 'POST', body: form }) } as never);
37
- return { status: res.status, body: (await res.json()) as { success?: boolean; path?: string; error?: string } };
37
+ return {
38
+ status: res.status,
39
+ body: (await res.json()) as { success?: boolean; kind?: string; path?: string; error?: string },
40
+ };
38
41
  }
39
42
 
40
43
  describe('fs create', () => {
@@ -43,7 +46,7 @@ describe('fs create', () => {
43
46
  const { status, body } = await create('tmp-create-probe', 'notes.md');
44
47
 
45
48
  expect(status).toBe(200);
46
- expect(body).toEqual({ success: true, path: 'tmp-create-probe/notes.md' });
49
+ expect(body).toEqual({ success: true, kind: 'file', path: 'tmp-create-probe/notes.md' });
47
50
  expect(fs.readFileSync(path.join(DIR, 'notes.md'), 'utf8')).toBe('');
48
51
  });
49
52
 
@@ -67,12 +70,14 @@ describe('fs create', () => {
67
70
  expect(fs.readFileSync(path.join(DIR, 'keep.txt'), 'utf8')).toBe('important');
68
71
  });
69
72
 
70
- test('refuses a name carrying a path separator', async () => {
73
+ test('a path separator is a NESTED path, and `..` is still refused', async () => {
71
74
  fs.mkdirSync(DIR, { recursive: true });
72
- for (const name of ['../escape.txt', 'sub/escape.txt', 'sub\\escape.txt']) {
73
- const { status } = await create('tmp-create-probe', name);
74
- expect(status).toBe(400);
75
- }
75
+ // A forward or back separator names a nested destination, which the dialog
76
+ // is for — the escape itself is the `..` segment, and that stays refused.
77
+ expect((await create('tmp-create-probe', 'sub/inside.txt')).status).toBe(200);
78
+ expect((await create('tmp-create-probe', 'sub\\also-inside.txt')).status).toBe(200);
79
+ expect((await create('tmp-create-probe', '../escape.txt')).status).toBe(400);
80
+ expect(fs.existsSync(path.join(DIR, 'sub/inside.txt'))).toBe(true);
76
81
  expect(fs.existsSync(path.join(process.cwd(), 'escape.txt'))).toBe(false);
77
82
  });
78
83