@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
@@ -12,14 +12,22 @@
12
12
  * on the old copy. Nothing else would ever notice: the install is a directory of
13
13
  * ordinary files, and the scan has no notion of a store version.
14
14
  *
15
- * The sync runs at boot, after the listener is up, and it is VERSION-GATED — the
16
- * store copy is written over the install only when its manifest version is
17
- * strictly newer. That is what makes the pass safe to run unconditionally: a
18
- * user's plugin that happens to share an id with a store entry but was installed
19
- * from a git URL carries no `.git` ONLY if it was copied by hand, and a
20
- * hand-copied directory keeps working because its version is compared rather
21
- * than its provenance. A plugin installed from a git URL is skipped outright,
22
- * because the store's copy is not its source of truth.
15
+ * The sync runs at boot, after the listener is up. For an install KABMO WROTE —
16
+ * one carrying a baseline — the store's TREE is the gate, not its version: the
17
+ * copy is written whenever the two tracked trees differ, in either direction.
18
+ * A version gate cannot be the whole answer, and believing it was is what left
19
+ * every bundled plugin un-updatable after the product rename: that release
20
+ * rewrote the store's files and reset every bundled version to `0.0.1`, so the
21
+ * store was never "newer" than the install and the pass skipped forever —
22
+ * measured on a real install, where the only way to get the new code was to
23
+ * remove the plugin and install it again. Content is also the honest test for a
24
+ * shipped fix that forgot its bump.
25
+ *
26
+ * A directory with NO baseline is of unknown provenance (hand-copied, or
27
+ * installed by a build that predates the baseline), so it keeps the conservative
28
+ * version gate: a copy the user made themselves must not be replaced by a store
29
+ * entry that merely shares its id. A git clone is skipped outright, because the
30
+ * store's copy is not its source of truth.
23
31
  *
24
32
  * The install's `node_modules` and `dist` are left alone until the copy lands:
25
33
  * the sync copies the store's SOURCES, then rebuilds, exactly as an install
@@ -36,14 +44,21 @@ import { readPluginManifest, subdirectories } from '@/server/lib/panels/files.se
36
44
  import { toManifest } from '@/server/lib/panels/manifest';
37
45
  import { buildPanelPlugin } from '@/server/lib/panels/build.server';
38
46
  import { readGitOrigin } from '@/server/lib/panels/git-clone.server';
39
- import { changedPaths, hashTrackedFiles } from '@/server/lib/panels/local-edits.server';
40
- import { readBaseline, clearBaseline, writeBaseline, writeBlockedSync } from '@/server/lib/panels/baseline.server';
47
+ import { baselineDescribesInstall, changedPaths, hashTrackedFiles, type TrackedFileHashes } from '@/server/lib/panels/local-edits.server';
48
+ import { readBaseline, writeBaseline, writeBlockedSync } from '@/server/lib/panels/baseline.server';
41
49
  import { emitRealtimeSignal } from '@/server/lib/realtime/signals.server';
42
50
 
43
51
  export interface BundledSyncOutcome {
44
52
  pluginId: string;
45
53
  from: string;
46
54
  to: string;
55
+ /**
56
+ * True when the store's files changed but its manifest version did not.
57
+ *
58
+ * The common case after a shipped fix whose bump was missed, and the reason
59
+ * the gate is content: reporting it as `1.0.0 → 1.0.0` reads as a no-op.
60
+ */
61
+ contentOnly?: boolean;
47
62
  /** A rebuild that failed after the files were replaced. */
48
63
  warning?: string;
49
64
  }
@@ -81,9 +96,13 @@ async function manifestVersion(dir: string): Promise<string | null> {
81
96
  * that says otherwise. When the store has no `dist`, nothing is removed and the
82
97
  * rebuild regenerates it from the new sources.
83
98
  */
84
- async function replaceFromStore(source: string, destination: string): Promise<void> {
99
+ async function replaceFromStore(
100
+ source: string,
101
+ destination: string,
102
+ knownInstallFiles?: TrackedFileHashes,
103
+ ): Promise<void> {
85
104
  const storeFiles = await hashTrackedFiles(source);
86
- const installFiles = await hashTrackedFiles(destination);
105
+ const installFiles = knownInstallFiles ?? (await hashTrackedFiles(destination));
87
106
  for (const path of Object.keys(installFiles)) {
88
107
  if (path in storeFiles) continue;
89
108
  await rm(join(destination, path), { force: true });
@@ -100,7 +119,49 @@ async function replaceFromStore(source: string, destination: string): Promise<vo
100
119
  }
101
120
 
102
121
  /**
103
- * Bring every installed bundled plugin up to the store's version.
122
+ * Write the store's copy over an install and rebuild it.
123
+ *
124
+ * Shared by the boot pass and the override, because everything after the gate is
125
+ * identical and the parts that matter are the ones a second copy gets wrong: the
126
+ * replacement is a MIRROR, the manifest is re-read from what landed, and the
127
+ * baseline is re-taken AFTER the build so the next pass compares against the
128
+ * tree this one produced.
129
+ */
130
+ async function applyStoreCopy(
131
+ pluginId: string,
132
+ storeDir: string,
133
+ destination: string,
134
+ from: string,
135
+ to: string,
136
+ installFiles: TrackedFileHashes,
137
+ ): Promise<{ outcome: BundledSyncOutcome } | { error: string }> {
138
+ await replaceFromStore(storeDir, destination, installFiles);
139
+
140
+ const read = await readPluginManifest(destination);
141
+ const manifest = read.kind === 'manifest' ? toManifest(read.value, destination) : null;
142
+ if (!manifest || 'error' in manifest) return { error: 'the store copy has no valid manifest' };
143
+
144
+ const build = await buildPanelPlugin(destination, manifest);
145
+ invalidatePanelScan();
146
+ // The new files are the baseline now — taken AFTER the build so the tree the
147
+ // next pass compares against is the one this pass produced.
148
+ await writeBaseline(pluginId, await hashTrackedFiles(destination));
149
+ await writeBlockedSync(pluginId, null);
150
+ return {
151
+ outcome: {
152
+ pluginId,
153
+ from,
154
+ to,
155
+ // The store changed but its manifest version did not. Naming it keeps a
156
+ // `1.0.0 → 1.0.0` line from reading as a no-op when a real fix landed.
157
+ ...(from === to ? { contentOnly: true } : {}),
158
+ ...(build.status === 'failed' ? { warning: build.reason ?? 'the plugin did not build' } : {}),
159
+ },
160
+ };
161
+ }
162
+
163
+ /**
164
+ * Bring every installed bundled plugin up to the store's copy.
104
165
  *
105
166
  * Never throws: one plugin that cannot be replaced must not stop the others, and
106
167
  * a failure is reported per plugin. Returns what actually moved.
@@ -132,9 +193,10 @@ export async function syncBundledPlugins(): Promise<{
132
193
  try {
133
194
  const storeVersion = await manifestVersion(join(storeRoot, pluginId));
134
195
  const installedVersion = await manifestVersion(destination);
135
- if (!storeVersion || !installedVersion || !isNewer(storeVersion, installedVersion)) {
136
- // Nothing to apply, so any earlier refusal no longer describes anything
137
- // the user has to act on.
196
+ // A manifest that cannot be read on either side means there is no
197
+ // comparison to make at all — the scan reports it, and the sync leaves the
198
+ // directory alone rather than guessing which side is wrong.
199
+ if (!storeVersion || !installedVersion) {
138
200
  await writeBlockedSync(pluginId, null);
139
201
  continue;
140
202
  }
@@ -143,9 +205,48 @@ export async function syncBundledPlugins(): Promise<{
143
205
  // differs from it is the user's. A plugin with no baseline predates this
144
206
  // check (or was installed by an older build), and running is the only
145
207
  // useful answer: refusing would freeze it forever with no way to say why.
146
- const baseline = await readBaseline(pluginId);
147
- if (baseline) {
148
- const changed = changedPaths(baseline, await hashTrackedFiles(destination));
208
+ //
209
+ // PROVENANCE and EVIDENCE are separate answers. A recorded baseline is what
210
+ // proves Kabmo wrote this directory, so the content gate below applies
211
+ // either way; only a baseline that still DESCRIBES the install may say a
212
+ // file is the user's — see `baselineDescribesInstall`. Obeying a stale one
213
+ // is what made the update unrecoverable: the rename rewrote every bundled
214
+ // file while an older baseline was recorded, so every path read as the
215
+ // user's edit and the only way out was removing the plugin.
216
+ const recorded = await readBaseline(pluginId);
217
+ const installFiles = await hashTrackedFiles(destination);
218
+ const storeFiles = await hashTrackedFiles(join(storeRoot, pluginId));
219
+ const trusted = recorded !== null && baselineDescribesInstall(recorded, installFiles, storeFiles);
220
+
221
+ // WHICH GATE APPLIES IS DECIDED BY PROVENANCE, and the two answer
222
+ // different questions:
223
+ //
224
+ // - A directory Kabmo wrote is compared by CONTENT. The store's tree is
225
+ // the source of truth for it, so any difference is an update — including
226
+ // a fix shipped without a version bump, and including the case that made
227
+ // this a bug: the product rename rewrote every bundled file AND reset the
228
+ // versions to 0.0.1, so a version gate could never see the store as
229
+ // newer and the plugin was stuck until it was removed and reinstalled
230
+ // (measured on a real install).
231
+ // - A directory with no recorded baseline is of unknown provenance, so the
232
+ // conservative version gate stays: a hand-copied directory that happens
233
+ // to share a store id must not be overwritten by a store entry that is
234
+ // merely newer than it.
235
+ const contentDiffers = changedPaths(installFiles, storeFiles).length > 0;
236
+ const versionIsNewer = isNewer(storeVersion, installedVersion);
237
+ const shouldApply = recorded ? contentDiffers : versionIsNewer;
238
+ if (!shouldApply) {
239
+ // Nothing to apply, so any earlier refusal no longer describes anything
240
+ // the user has to act on.
241
+ await writeBlockedSync(pluginId, null);
242
+ // A stale baseline is replaced by what is on disk, so the next release's
243
+ // update compares against reality rather than against history.
244
+ if (recorded && !trusted) await writeBaseline(pluginId, installFiles);
245
+ continue;
246
+ }
247
+
248
+ if (trusted) {
249
+ const changed = changedPaths(recorded, installFiles);
149
250
  if (changed.length > 0) {
150
251
  blocked.push({ pluginId, from: installedVersion, to: storeVersion, changed });
151
252
  await writeBlockedSync(pluginId, { at: Date.now(), changed });
@@ -153,26 +254,19 @@ export async function syncBundledPlugins(): Promise<{
153
254
  }
154
255
  }
155
256
 
156
- await replaceFromStore(join(storeRoot, pluginId), destination);
157
-
158
- const read = await readPluginManifest(destination);
159
- const manifest = read.kind === 'manifest' ? toManifest(read.value, destination) : null;
160
- if (!manifest || 'error' in manifest) {
161
- failed.push({ pluginId, error: 'the store copy has no valid manifest' });
257
+ const applied = await applyStoreCopy(
258
+ pluginId,
259
+ join(storeRoot, pluginId),
260
+ destination,
261
+ installedVersion,
262
+ storeVersion,
263
+ installFiles,
264
+ );
265
+ if ('error' in applied) {
266
+ failed.push({ pluginId, error: applied.error });
162
267
  continue;
163
268
  }
164
- const build = await buildPanelPlugin(destination, manifest);
165
- invalidatePanelScan();
166
- // The new files are the baseline now — taken AFTER the build so the tree
167
- // the next pass compares against is the one this pass produced.
168
- await writeBaseline(pluginId, await hashTrackedFiles(destination));
169
- await writeBlockedSync(pluginId, null);
170
- updated.push({
171
- pluginId,
172
- from: installedVersion,
173
- to: storeVersion,
174
- ...(build.status === 'failed' ? { warning: build.reason ?? 'the plugin did not build' } : {}),
175
- });
269
+ updated.push(applied.outcome);
176
270
  } catch (error) {
177
271
  failed.push({ pluginId, error: error instanceof Error ? error.message : String(error) });
178
272
  }
@@ -187,9 +281,11 @@ export async function syncBundledPlugins(): Promise<{
187
281
  *
188
282
  * The escape hatch for a refusal: the user has seen which files differ and asked
189
283
  * for the store's copy anyway, which is the only way past a baseline Kabmo
190
- * cannot reconcile. The baseline is dropped first, so the pass that follows takes
191
- * the store's files and re-records what it wrote — the same end state a clean
192
- * install reaches.
284
+ * cannot reconcile. It applies the copy DIRECTLY rather than clearing the
285
+ * baseline and re-running the pass: a cleared baseline makes the plugin read as
286
+ * "unknown provenance", which is exactly the state the conservative version gate
287
+ * governs — so the override silently became a no-op whenever the store's version
288
+ * was not newer, which is the case it most often exists for.
193
289
  *
194
290
  * Deliberately addressed by id and refusing anything that is not a bundled
195
291
  * install: this must never be a way to overwrite a git clone.
@@ -201,12 +297,21 @@ export async function forceSyncBundledPlugin(pluginId: string): Promise<{ ok: bo
201
297
  if (!(await pathExists(destination))) return { ok: false, error: `${pluginId} is not installed.` };
202
298
  if (await readGitOrigin(destination)) return { ok: false, error: `${pluginId} is a git clone; its own Update is the way to change it.` };
203
299
 
204
- await clearBaseline(pluginId);
205
- await writeBlockedSync(pluginId, null);
206
- const result = await syncBundledPlugins();
207
- const failed = result.failed.find((entry) => entry.pluginId === pluginId);
208
- if (failed) return { ok: false, error: failed.error };
209
- const outcome = result.updated.find((entry) => entry.pluginId === pluginId);
210
- if (!outcome) return { ok: false, error: `${pluginId} is already at the store's version.` };
211
- return { ok: true, ...(outcome.warning ? { warning: outcome.warning } : {}) };
300
+ const from = (await manifestVersion(destination)) ?? 'unknown';
301
+ const to = (await manifestVersion(storeDir)) ?? 'unknown';
302
+ try {
303
+ const applied = await applyStoreCopy(
304
+ pluginId,
305
+ storeDir,
306
+ destination,
307
+ from,
308
+ to,
309
+ await hashTrackedFiles(destination),
310
+ );
311
+ if ('error' in applied) return { ok: false, error: applied.error };
312
+ emitRealtimeSignal('panels-changed');
313
+ return { ok: true, ...(applied.outcome.warning ? { warning: applied.outcome.warning } : {}) };
314
+ } catch (error) {
315
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
316
+ }
212
317
  }
@@ -22,6 +22,16 @@ export type RealtimeSignal =
22
22
  | 'stream-status'
23
23
  /** The sidebar's structure changed (a session or folder was added/removed). */
24
24
  | 'sidebar-structure'
25
+ /**
26
+ * A RUNNING session's transcript grew, so its last-activity stamp moved.
27
+ *
28
+ * Raised per tool call, which is the whole reason it is not
29
+ * `sidebar-structure`: that payload is every folder and session (216 KB over
30
+ * 731 sessions, measured), while only a running session's stamp can move
31
+ * between turn boundaries. Republishing the structure here is what made a
32
+ * phone's stream feel slow — one full list per tool call.
33
+ */
34
+ | 'sidebar-activity'
25
35
  /** An omp child for `sessionId` became reachable, or was torn down. */
26
36
  | 'session-attached'
27
37
  /** A scheduled task was created, edited, paused, deleted or fired. */
@@ -20,6 +20,7 @@ import {
20
20
  TOPIC_PANELS,
21
21
  TOPIC_SCHEDULE,
22
22
  TOPIC_SIDEBAR,
23
+ TOPIC_SIDEBAR_ACTIVITY,
23
24
  TOPIC_SIDEBAR_STATUS,
24
25
  TOPIC_USAGE,
25
26
  TOPIC_WAITING,
@@ -28,10 +29,11 @@ import {
28
29
  sessionTelemetryTopic,
29
30
  sessionTodosTopic,
30
31
  } from '@/shared/lib/realtime/protocol';
31
- import type { SidebarPayload, SidebarStatusPayload } from '@/shared/types/realtime';
32
+ import type { SidebarActivityPayload, SidebarPayload, SidebarStatusPayload } from '@/shared/types/realtime';
32
33
  import { isValidPluginId } from '@/shared/lib/plugins/protocol';
33
34
  import type { TopicDescriptor } from '@/server/lib/realtime/hub.types';
34
35
  import { loadSidebarStatus, loadSidebarStructure } from '@/server/lib/omp/session/sidebar-data.server';
36
+ import { loadSidebarActivity } from '@/server/lib/omp/session/sidebar-activity.server';
35
37
  import { sessionDescriptor } from '@/server/lib/realtime/topics/session.server';
36
38
  import { btwDescriptor } from '@/server/lib/realtime/topics/btw.server';
37
39
  import { SESSION_DATA_TOPICS } from '@/server/lib/realtime/topics/session-data.server';
@@ -64,6 +66,7 @@ export const SCOPED_FAMILIES: Record<string, (scope: string) => TopicDescriptor>
64
66
  export const EXACT_TOPICS = new Map<string, TopicDescriptor>([
65
67
  [TOPIC_SIDEBAR, { resolve: async (): Promise<SidebarPayload> => loadSidebarStructure() }],
66
68
  [TOPIC_SIDEBAR_STATUS, { resolve: async (): Promise<SidebarStatusPayload> => loadSidebarStatus() }],
69
+ [TOPIC_SIDEBAR_ACTIVITY, { resolve: async (): Promise<SidebarActivityPayload> => loadSidebarActivity() }],
67
70
  [TOPIC_SCHEDULE, { resolve: () => globalDescriptor(TOPIC_SCHEDULE).resolve(TOPIC_SCHEDULE) }],
68
71
  [TOPIC_USAGE, { resolve: () => globalDescriptor(TOPIC_USAGE).resolve(TOPIC_USAGE) }],
69
72
  [TOPIC_PANELS, { resolve: () => globalDescriptor(TOPIC_PANELS).resolve(TOPIC_PANELS) }],
@@ -18,6 +18,7 @@ import {
18
18
  TOPIC_PANELS,
19
19
  TOPIC_SCHEDULE,
20
20
  TOPIC_SIDEBAR,
21
+ TOPIC_SIDEBAR_ACTIVITY,
21
22
  TOPIC_SIDEBAR_STATUS,
22
23
  TOPIC_USAGE,
23
24
  TOPIC_WAITING,
@@ -29,6 +30,7 @@ import { getRealtimeHub, registerTopics, type TopicDescriptor } from '@/server/l
29
30
  import { onRealtimeSignal } from '@/server/lib/realtime/signals.server';
30
31
  import { invalidateOmpSidebarData } from '@/server/lib/omp/session/reader';
31
32
  import { loadSidebarStatus, loadSidebarStructure } from '@/server/lib/omp/session/sidebar-data.server';
33
+ import { loadSidebarActivity } from '@/server/lib/omp/session/sidebar-activity.server';
32
34
  import { EXACT_TOPICS, descriptorFor, isKnownTopicName } from '@/server/lib/realtime/topics/catalog.server';
33
35
 
34
36
  declare global {
@@ -78,6 +80,7 @@ export function initRealtimeTopics(): void {
78
80
  onRealtimeSignal(({ signal, sessionId, root }) => {
79
81
  if (signal === 'stream-status') publishSidebarStatus();
80
82
  else if (signal === 'sidebar-structure') publishSidebarStructure();
83
+ else if (signal === 'sidebar-activity') publishSidebarActivity();
81
84
  else if (signal === 'session-attached' && sessionId) publishSessionState(sessionId);
82
85
  else if (signal === 'schedule-changed') publishTopic(TOPIC_SCHEDULE);
83
86
  else if (signal === 'panels-changed') publishTopic(TOPIC_PANELS);
@@ -244,6 +247,19 @@ export function publishSidebarStatus(): void {
244
247
  schedulePublish(TOPIC_SIDEBAR_STATUS, loadSidebarStatus);
245
248
  }
246
249
 
250
+ /**
251
+ * A RUNNING session's transcript grew: republish its activity stamps.
252
+ *
253
+ * Called per tool call (see `handleToolExecutionEnd`), so it must never be the
254
+ * structure publish: that one invalidates the discovery scan and re-serializes
255
+ * every folder and session. This payload carries one ISO string per live run
256
+ * and re-reads nothing but those files' own summaries (each memoized on
257
+ * `(size, mtimeMs)`, measured 0.1 ms on a cache miss).
258
+ */
259
+ export function publishSidebarActivity(): void {
260
+ schedulePublish(TOPIC_SIDEBAR_ACTIVITY, loadSidebarActivity);
261
+ }
262
+
247
263
  /**
248
264
  * The sidebar's STRUCTURE changed: a session was created, deleted, renamed, a
249
265
  * folder was added or removed, or a JSONL grew a new session.
@@ -0,0 +1,114 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ import { describe, expect, test } from 'bun:test';
7
+
8
+ import { versionFromAssetName } from '@/server/lib/updates/shell-assets';
9
+
10
+ /**
11
+ * The asset name is the only place a shell artifact's version exists: the
12
+ * release TAG names the SERVER version, and the two move independently. A parser
13
+ * that read the tag would compare a server version against a shell version.
14
+ *
15
+ * Both shells are pinned here, because they share the parser and differ only by
16
+ * prefix/extension — the shape that silently accepts the other shell's asset.
17
+ */
18
+ describe('versionFromAssetName', () => {
19
+ test('reads the version the release workflows write', () => {
20
+ expect(versionFromAssetName('android', 'Kabmo-0.5.0-android.apk')).toBe('0.5.0');
21
+ expect(versionFromAssetName('ios', 'Kabmo-0.1.0-ios.ipa')).toBe('0.1.0');
22
+ });
23
+
24
+ /**
25
+ * The old spelling is still READ, and that is not sentiment.
26
+ *
27
+ * These names are the only record of a published shell, and every release
28
+ * already on GitHub carries `kabmo-android-v<v>.apk`. A parser that only knew
29
+ * the new scheme would answer "no update" for an installed shell the moment
30
+ * this shipped — silently, because a name that does not match reads exactly
31
+ * like a release with no artifact.
32
+ */
33
+ test('still reads the legacy spelling every published release carries', () => {
34
+ expect(versionFromAssetName('android', 'kabmo-android-v0.5.0.apk')).toBe('0.5.0');
35
+ expect(versionFromAssetName('ios', 'kabmo-ios-v0.1.0.ipa')).toBe('0.1.0');
36
+ });
37
+
38
+ test('ignores anything that is not that shell’s artifact', () => {
39
+ expect(versionFromAssetName('android', 'Kabmo-1.2.3-android.apk.zip')).toBeNull();
40
+ expect(versionFromAssetName('android', 'source.tar.gz')).toBeNull();
41
+ expect(versionFromAssetName('android', 'Kabmo-latest-android.apk')).toBeNull();
42
+ expect(versionFromAssetName('android', 'Kabmo--android.apk')).toBeNull();
43
+ expect(versionFromAssetName('android', '')).toBeNull();
44
+ });
45
+
46
+ /**
47
+ * The desktop assets share the `Kabmo-` prefix, so the EXTENSION is what keeps
48
+ * them out. A probe that matched on the prefix alone would offer a Mac `.dmg`
49
+ * to an Android phone.
50
+ */
51
+ test('does not read a desktop asset as a mobile one', () => {
52
+ for (const name of [
53
+ 'Kabmo-0.1.2-mac-arm64.zip',
54
+ 'Kabmo-0.1.2-mac-arm64.dmg',
55
+ 'Kabmo-0.1.0-win-x64-setup.exe',
56
+ 'Kabmo-0.1.0-linux-x86_64.tar.gz',
57
+ 'Kabmo-0.1.0-linux-amd64.deb',
58
+ 'Kabmo-0.1.0-linux-x86_64.AppImage',
59
+ ]) {
60
+ expect(versionFromAssetName('android', name), name).toBeNull();
61
+ expect(versionFromAssetName('ios', name), name).toBeNull();
62
+ }
63
+ });
64
+
65
+ test('does not accept one shell’s asset for the other', () => {
66
+ // The probes read the SAME release list, so a parser that ignored the
67
+ // kind would let an iOS probe answer with the APK (and offer an `.apk` to an
68
+ // iPhone).
69
+ expect(versionFromAssetName('ios', 'Kabmo-0.5.0-android.apk')).toBeNull();
70
+ expect(versionFromAssetName('android', 'Kabmo-0.1.0-ios.ipa')).toBeNull();
71
+ expect(versionFromAssetName('ios', 'kabmo-android-v1.2.3.apk')).toBeNull();
72
+ expect(versionFromAssetName('android', 'kabmo-ios-v1.2.3.ipa')).toBeNull();
73
+ });
74
+
75
+ /**
76
+ * The desktop trio, one entry each.
77
+ *
78
+ * A desktop release carries SEVERAL files per platform (two arches × two
79
+ * macOS shapes, four Linux formats), so the match is on the segment after the
80
+ * version rather than the whole name: any one of them answers the same
81
+ * version, and the download the console offers is the release page.
82
+ */
83
+ test('reads every desktop spelling, and only its own platform', () => {
84
+ for (const name of ['Kabmo-0.2.0-mac-arm64.zip', 'Kabmo-0.2.0-mac-x64.dmg']) {
85
+ expect(versionFromAssetName('macos', name), name).toBe('0.2.0');
86
+ expect(versionFromAssetName('windows', name), name).toBeNull();
87
+ expect(versionFromAssetName('linux', name), name).toBeNull();
88
+ }
89
+ for (const name of ['Kabmo-0.2.0-win-x64-setup.exe', 'Kabmo-0.2.0-win-arm64-setup.exe']) {
90
+ expect(versionFromAssetName('windows', name), name).toBe('0.2.0');
91
+ expect(versionFromAssetName('macos', name), name).toBeNull();
92
+ }
93
+ for (const name of [
94
+ 'Kabmo-0.2.0-linux-x86_64.tar.gz',
95
+ 'Kabmo-0.2.0-linux-arm64.tar.gz',
96
+ 'Kabmo-0.2.0-linux-amd64.deb',
97
+ 'Kabmo-0.2.0-linux-arm64.rpm',
98
+ 'Kabmo-0.2.0-linux-x86_64.AppImage',
99
+ ]) {
100
+ expect(versionFromAssetName('linux', name), name).toBe('0.2.0');
101
+ expect(versionFromAssetName('macos', name), name).toBeNull();
102
+ }
103
+ });
104
+
105
+ test('a desktop shell reads no mobile asset, and vice versa', () => {
106
+ // The `Kabmo-` prefix is shared by all five, so a scheme that matched on the
107
+ // prefix alone would offer an APK to a Mac.
108
+ expect(versionFromAssetName('macos', 'Kabmo-0.6.0-android.apk')).toBeNull();
109
+ expect(versionFromAssetName('windows', 'Kabmo-0.2.0-ios.ipa')).toBeNull();
110
+ expect(versionFromAssetName('linux', 'Kabmo-0.6.0-android.apk')).toBeNull();
111
+ expect(versionFromAssetName('android', 'Kabmo-0.2.0-linux-x86_64.tar.gz')).toBeNull();
112
+ expect(versionFromAssetName('ios', 'Kabmo-0.2.0-win-x64-setup.exe')).toBeNull();
113
+ });
114
+ });