@kabmo-labs/kabmo 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (351) hide show
  1. package/README.md +7 -0
  2. package/dist/client/index.html +1 -1
  3. package/dist/client/index.js +403 -384
  4. package/dist/client/static/js/{index-0wvbca0b.js → index-bpptfkz2.js} +905 -875
  5. package/dist/client/static/js/index-k315p1e8.css +1 -0
  6. package/package.json +10 -1
  7. package/src/client/components/common/code-surface/documents.test.ts +120 -0
  8. package/src/client/components/common/code-surface/documents.ts +201 -0
  9. package/src/client/components/common/code-surface/extensions.test.ts +110 -0
  10. package/src/client/components/common/code-surface/extensions.ts +162 -0
  11. package/src/client/components/common/code-surface/handle.ts +102 -0
  12. package/src/client/components/common/code-surface/index.tsx +253 -314
  13. package/src/client/components/common/code-surface/keymap.test.ts +133 -0
  14. package/src/client/components/common/code-surface/keymap.ts +267 -0
  15. package/src/client/components/common/code-surface/marks.ts +74 -0
  16. package/src/client/components/common/code-surface/shiki.ts +234 -0
  17. package/src/client/components/common/code-surface/sync.test.ts +185 -0
  18. package/src/client/components/common/code-surface/sync.ts +58 -0
  19. package/src/client/components/common/code-surface/theme.test.ts +202 -0
  20. package/src/client/components/common/code-surface/theme.ts +111 -0
  21. package/src/client/components/common/editor-view-buttons/index.test.tsx +148 -0
  22. package/src/client/components/common/editor-view-buttons/index.tsx +109 -0
  23. package/src/client/components/common/header-controls/index.tsx +24 -1
  24. package/src/client/components/common/header-controls/shell-chrome.test.tsx +272 -0
  25. package/src/client/components/common/shell-update-button/Modal.tsx +163 -0
  26. package/src/client/components/common/shell-update-button/index.test.tsx +251 -0
  27. package/src/client/components/common/shell-update-button/index.tsx +125 -0
  28. package/src/client/components/common/toolbar-row/index.test.tsx +166 -0
  29. package/src/client/components/common/toolbar-row/index.tsx +189 -0
  30. package/src/client/components/common/workspace-options-menu/index.tsx +19 -1
  31. package/src/client/components/layout/desktop-layout/TopNavbar.tsx +5 -6
  32. package/src/client/components/layout/desktop-layout/WorkspacePanels.tsx +41 -78
  33. package/src/client/components/layout/desktop-layout/index.tsx +55 -69
  34. package/src/client/components/layout/desktop-layout/resizer/index.tsx +6 -3
  35. package/src/client/components/layout/session-sidebar/CategoryHeader.tsx +24 -0
  36. package/src/client/components/layout/session-sidebar/CategoryHeader.worktree.test.tsx +126 -0
  37. package/src/client/components/layout/session-sidebar/CategoryItem.tsx +4 -0
  38. package/src/client/components/layout/session-sidebar/Footer.tsx +8 -15
  39. package/src/client/components/layout/session-sidebar/SessionItem.tsx +12 -0
  40. package/src/client/components/layout/session-sidebar/SessionList.tsx +4 -0
  41. package/src/client/components/layout/session-sidebar/SessionsView.tsx +24 -0
  42. package/src/client/components/layout/session-sidebar/WorktreeBadge.test.ts +48 -0
  43. package/src/client/components/layout/session-sidebar/WorktreeBadge.tsx +55 -0
  44. package/src/client/components/mobile/LayoutWrapper.tsx +23 -16
  45. package/src/client/components/mobile/RightSidebar.tsx +21 -10
  46. package/src/client/components/mobile/SessionSidebar.tsx +8 -0
  47. package/src/client/components/mobile/header-titlebar.test.tsx +172 -0
  48. package/src/client/components/mobile/mobile-main-view/Header.tsx +10 -1
  49. package/src/client/components/mobile/mobile-right-sidebar/FullDiff.tsx +3 -0
  50. package/src/client/components/mobile/mobile-right-sidebar/FullEditor.tsx +63 -38
  51. package/src/client/components/mobile/mobile-right-sidebar/OutlineButton.test.tsx +137 -0
  52. package/src/client/components/mobile/mobile-right-sidebar/OutlineButton.tsx +14 -3
  53. package/src/client/components/mobile/mobile-session-sidebar/Footer.tsx +9 -21
  54. package/src/client/components/mobile/mobile-session-sidebar/Header.tsx +6 -2
  55. package/src/client/components/mobile/mobile-session-sidebar/Item.tsx +16 -0
  56. package/src/client/components/mobile/mobile-session-sidebar/List.tsx +4 -0
  57. package/src/client/components/mobile/mobile-session-sidebar/SessionRow.tsx +5 -0
  58. package/src/client/components/mobile/mobile-session-sidebar/WorktreeDialog.tsx +49 -0
  59. package/src/client/components/mobile/worktree-session.ts +51 -0
  60. package/src/client/components/settings/Modal.tsx +8 -0
  61. package/src/client/components/settings/categories/AppearanceSettings.tsx +3 -0
  62. package/src/client/components/settings/categories/ProjectSettings.tsx +22 -8
  63. package/src/client/components/settings/categories/appearance-settings/EditorThemeSection.test.tsx +167 -0
  64. package/src/client/components/settings/categories/appearance-settings/EditorThemeSection.tsx +184 -0
  65. package/src/client/components/settings/categories/project-settings/WorktreeSettings.tsx +146 -0
  66. package/src/client/components/settings/categories/project-settings/worktree-row.test.ts +83 -0
  67. package/src/client/components/settings/categories/project-settings/worktree-row.ts +47 -0
  68. package/src/client/components/workspace/chat-timeline/EmptyWorkspacePrompt.tsx +36 -55
  69. package/src/client/components/workspace/chat-timeline/PendingChatView.tsx +8 -0
  70. package/src/client/components/workspace/chat-timeline/SystemNotice.tsx +9 -4
  71. package/src/client/components/workspace/chat-timeline/TaskResultContent.tsx +9 -7
  72. package/src/client/components/workspace/chat-timeline/index.tsx +5 -0
  73. package/src/client/components/workspace/chat-timeline/target-picker/WorkspacePicker.test.tsx +132 -0
  74. package/src/client/components/workspace/chat-timeline/target-picker/WorkspacePicker.tsx +120 -0
  75. package/src/client/components/workspace/chat-timeline/target-picker/WorktreePicker.test.tsx +319 -0
  76. package/src/client/components/workspace/chat-timeline/target-picker/WorktreePicker.tsx +285 -0
  77. package/src/client/components/workspace/chat-timeline/tool-renderers/ask-panel/index.test.ts +2 -0
  78. package/src/client/components/workspace/chat-timeline/tool-renderers/hashline-patch/index.tsx +34 -29
  79. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/AstEdit.tsx +11 -9
  80. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Bash.tsx +10 -7
  81. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Edit.tsx +25 -8
  82. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Eval.test.tsx +146 -0
  83. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Eval.tsx +32 -17
  84. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/FileMatches.test.ts +114 -0
  85. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/FileMatches.tsx +111 -0
  86. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Read.tsx +7 -7
  87. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/SearchTool.tsx +9 -59
  88. package/src/client/components/workspace/chat-timeline/tool-renderers/shared/ArtifactBody.tsx +2 -7
  89. package/src/client/components/workspace/chat-timeline/tool-renderers/shared/DiffView.test.ts +27 -28
  90. package/src/client/components/workspace/chat-timeline/tool-renderers/shared/DiffView.tsx +39 -26
  91. package/src/client/components/workspace/chat-timeline/tool-renderers/shared/ExcerptCode.tsx +8 -8
  92. package/src/client/components/workspace/chat-timeline/tool-renderers/shared/HighlightedCode.tsx +35 -0
  93. package/src/client/components/workspace/chat-timeline/tool-renderers/shared/JsonCodeBlock.tsx +4 -4
  94. package/src/client/components/workspace/diff-panel/MergeDiffView.tsx +179 -0
  95. package/src/client/components/workspace/diff-panel/ThemedMergeDiffView.tsx +24 -0
  96. package/src/client/components/workspace/diff-panel/Toolbar.tsx +224 -186
  97. package/src/client/components/workspace/diff-panel/ToolbarSegments.tsx +98 -0
  98. package/src/client/components/workspace/diff-panel/index.tsx +76 -145
  99. package/src/client/components/workspace/diff-panel/merge.ts +315 -0
  100. package/src/client/components/workspace/editor/Body.tsx +4 -7
  101. package/src/client/components/workspace/editor/CommandPalette.tsx +2 -2
  102. package/src/client/components/workspace/editor/FileDocument.tsx +58 -63
  103. package/src/client/components/workspace/editor/FilePane.tsx +4 -7
  104. package/src/client/components/workspace/editor/FindWidget.tsx +7 -2
  105. package/src/client/components/workspace/editor/Toolbar.tsx +220 -123
  106. package/src/client/components/workspace/editor/index.tsx +7 -9
  107. package/src/client/components/workspace/file-explorer/ContextMenu.tsx +2 -1
  108. package/src/client/components/workspace/file-explorer/Modals.tsx +40 -12
  109. package/src/client/components/workspace/file-explorer/OutlineSection.test.tsx +18 -0
  110. package/src/client/components/workspace/file-explorer/OutlineSection.tsx +10 -3
  111. package/src/client/components/workspace/file-explorer/RootMenu.tsx +14 -5
  112. package/src/client/components/workspace/file-explorer/Toolbar.tsx +74 -0
  113. package/src/client/components/workspace/file-explorer/TreeItem.tsx +38 -3
  114. package/src/client/components/workspace/file-explorer/filter.ts +47 -0
  115. package/src/client/components/workspace/file-explorer/index.tsx +77 -64
  116. package/src/client/components/workspace/file-explorer/local-edit.test.ts +135 -0
  117. package/src/client/components/workspace/search-panel/Body.tsx +1 -1
  118. package/src/client/components/workspace/worktree-modal/index.test.tsx +118 -0
  119. package/src/client/components/workspace/worktree-modal/index.tsx +261 -0
  120. package/src/client/hooks/chat/omp/reconnect.test.ts +338 -0
  121. package/src/client/hooks/chat/omp/reconnect.ts +187 -0
  122. package/src/client/hooks/chat/omp/run-liveness.test.ts +71 -0
  123. package/src/client/hooks/chat/omp/run-liveness.ts +65 -0
  124. package/src/client/hooks/chat/omp/session-list.activity.test.ts +114 -0
  125. package/src/client/hooks/chat/omp/session-list.test-util.ts +164 -0
  126. package/src/client/hooks/chat/omp/session-list.test.ts +30 -150
  127. package/src/client/hooks/chat/omp/session-list.tsx +69 -9
  128. package/src/client/hooks/chat/timeline/generating.ts +19 -2
  129. package/src/client/hooks/chat/timeline/index.ts +27 -28
  130. package/src/client/hooks/chat/timeline/lost-run.test.ts +329 -0
  131. package/src/client/hooks/chat/timeline/lost-run.ts +177 -0
  132. package/src/client/hooks/chat/timeline/send-execute.test.ts +2 -0
  133. package/src/client/hooks/chat/timeline/send-steer.test.ts +2 -0
  134. package/src/client/hooks/chat/timeline/send.test.ts +2 -0
  135. package/src/client/hooks/chat/timeline/send.ts +11 -1
  136. package/src/client/hooks/chat/timeline/session-load.test-util.ts +205 -0
  137. package/src/client/hooks/chat/timeline/session-load.test.ts +61 -162
  138. package/src/client/hooks/chat/timeline/session-load.ts +69 -37
  139. package/src/client/hooks/chat/timeline/session-switch.test.ts +65 -0
  140. package/src/client/hooks/chat/timeline/session-switch.ts +63 -0
  141. package/src/client/hooks/chat/timeline/spawn-target.ts +52 -0
  142. package/src/client/hooks/chat/timeline/tool-open.test.ts +74 -0
  143. package/src/client/hooks/chat/timeline/tool-open.ts +52 -8
  144. package/src/client/hooks/code/deferred-highlight.test.ts +182 -0
  145. package/src/client/hooks/code/deferred-highlight.ts +129 -0
  146. package/src/client/hooks/editor/find-state.ts +1 -1
  147. package/src/client/hooks/editor/theme.test.ts +206 -0
  148. package/src/client/hooks/editor/theme.ts +82 -0
  149. package/src/client/hooks/editor/use-editor-drop.test.ts +101 -0
  150. package/src/client/hooks/editor/use-editor-drop.ts +67 -14
  151. package/src/client/hooks/editor/use-editor-find.ts +7 -7
  152. package/src/client/hooks/editor/use-outline-view.test.ts +182 -0
  153. package/src/client/hooks/editor/use-outline-view.ts +94 -7
  154. package/src/client/hooks/editor/use-scroll-restore.test.ts +55 -2
  155. package/src/client/hooks/editor/use-scroll-restore.ts +124 -4
  156. package/src/client/hooks/editor/use-search-reveal.ts +1 -1
  157. package/src/client/hooks/ui/long-press.test.ts +174 -0
  158. package/src/client/hooks/ui/long-press.ts +112 -0
  159. package/src/client/hooks/ui/settings-modal.ts +56 -0
  160. package/src/client/hooks/ui/updates/shell-download.test.ts +75 -0
  161. package/src/client/hooks/ui/updates/shell-download.ts +153 -28
  162. package/src/client/hooks/ui/updates/shell-offer.test.ts +213 -0
  163. package/src/client/hooks/ui/updates/shell-update.test.ts +140 -0
  164. package/src/client/hooks/ui/updates/shell-update.ts +119 -0
  165. package/src/client/hooks/workspace/diff-file.ts +29 -10
  166. package/src/client/hooks/workspace/diff-git-actions.ts +147 -0
  167. package/src/client/hooks/workspace/editor-view.ts +61 -0
  168. package/src/client/hooks/workspace/file-create.ts +41 -11
  169. package/src/client/hooks/workspace/file-listing.ts +56 -1
  170. package/src/client/hooks/workspace/file-tabs.test.ts +62 -5
  171. package/src/client/hooks/workspace/file-tabs.ts +24 -11
  172. package/src/client/hooks/workspace/file-tree-actions.ts +37 -5
  173. package/src/client/hooks/workspace/navbar-session-actions.ts +72 -0
  174. package/src/client/hooks/workspace/worktree/index.ts +165 -0
  175. package/src/client/lib/desktop/bridge.ts +45 -0
  176. package/src/client/lib/search-reveal.ts +1 -1
  177. package/src/client/main.tsx +5 -1
  178. package/src/client/tailwind.css +56 -8
  179. package/src/server/index.ts +7 -2
  180. package/src/server/lib/auth/guard.test.ts +28 -0
  181. package/src/server/lib/auth/guard.ts +10 -0
  182. package/src/server/lib/bundler/titlebar-css.test.ts +42 -1
  183. package/src/server/lib/db/schema.ts +4 -0
  184. package/src/server/lib/db/worktree-registry.server.ts +91 -0
  185. package/src/server/lib/fs/canonical.ts +52 -0
  186. package/src/server/lib/fs/relative-path.ts +48 -0
  187. package/src/server/lib/fs/root-worktree.test.ts +116 -0
  188. package/src/server/lib/fs/root.ts +39 -10
  189. package/src/server/lib/fs/worktree/carry.ts +134 -0
  190. package/src/server/lib/fs/worktree/create.test.ts +249 -0
  191. package/src/server/lib/fs/worktree/create.ts +262 -0
  192. package/src/server/lib/fs/worktree/errors.ts +43 -0
  193. package/src/server/lib/fs/worktree/list.test.ts +207 -0
  194. package/src/server/lib/fs/worktree/list.ts +142 -0
  195. package/src/server/lib/fs/worktree/location.test.ts +206 -0
  196. package/src/server/lib/fs/worktree/location.ts +114 -0
  197. package/src/server/lib/fs/worktree/project-id.ts +53 -0
  198. package/src/server/lib/fs/worktree/remove.test.ts +242 -0
  199. package/src/server/lib/fs/worktree/remove.ts +152 -0
  200. package/src/server/lib/fs/worktree/settings.server.ts +56 -0
  201. package/src/server/lib/fs/worktree/topology.ts +142 -0
  202. package/src/server/lib/lifecycle/claim-port.test.ts +156 -0
  203. package/src/server/lib/lifecycle/port-guard.test.ts +20 -164
  204. package/src/server/lib/lifecycle/port-guard.ts +39 -7
  205. package/src/server/lib/lifecycle/probe-health.test.ts +166 -0
  206. package/src/server/lib/lifecycle/probe.test.ts +77 -164
  207. package/src/server/lib/lifecycle/probe.ts +121 -13
  208. package/src/server/lib/omp/rpc/frame-fold-signals.test.ts +112 -0
  209. package/src/server/lib/omp/rpc/frame-fold.ts +10 -5
  210. package/src/server/lib/omp/session/sidebar-activity.server.test.ts +162 -0
  211. package/src/server/lib/omp/session/sidebar-activity.server.ts +70 -0
  212. package/src/server/lib/omp/session/sidebar-data.canonical.test.ts +155 -0
  213. package/src/server/lib/omp/session/sidebar-data.server.ts +28 -1
  214. package/src/server/lib/omp/session/telemetry/compute.ts +13 -1
  215. package/src/server/lib/omp/session/telemetry.test.ts +33 -0
  216. package/src/server/lib/panels/local-edits.server.ts +33 -0
  217. package/src/server/lib/panels/sync.fixture.test-util.ts +92 -0
  218. package/src/server/lib/panels/sync.local-edits.test.ts +150 -0
  219. package/src/server/lib/panels/sync.server.test.ts +125 -147
  220. package/src/server/lib/panels/sync.server.ts +153 -48
  221. package/src/server/lib/realtime/signals.server.ts +10 -0
  222. package/src/server/lib/realtime/topics/catalog.server.ts +4 -1
  223. package/src/server/lib/realtime/topics.server.ts +16 -0
  224. package/src/server/lib/updates/shell-assets.test.ts +114 -0
  225. package/src/server/lib/updates/shell-assets.ts +200 -0
  226. package/src/server/lib/updates/shell-cache.ts +105 -0
  227. package/src/server/lib/updates/shell-pages.test.ts +187 -0
  228. package/src/server/lib/updates/shell-pages.ts +157 -0
  229. package/src/server/lib/updates/shell-release-desktop.test.ts +166 -0
  230. package/src/server/lib/updates/shell-release.test.ts +59 -71
  231. package/src/server/lib/updates/shell-release.ts +178 -198
  232. package/src/server/routes/fs/action-create.test.ts +18 -13
  233. package/src/server/routes/fs/action-nested.test.ts +184 -0
  234. package/src/server/routes/fs/action.ts +47 -16
  235. package/src/server/routes/fs/git-merge.test.ts +11 -1
  236. package/src/server/routes/fs/read.test.ts +24 -7
  237. package/src/server/routes/fs/scope.test.ts +15 -10
  238. package/src/server/routes/fs/scope.ts +3 -14
  239. package/src/server/routes/index.ts +2 -0
  240. package/src/server/routes/updates/android.ts +6 -1
  241. package/src/server/routes/updates/desktop.ts +59 -0
  242. package/src/server/routes/updates/index.ts +2 -0
  243. package/src/server/routes/updates/ios.ts +5 -1
  244. package/src/server/routes/worktrees/create.ts +138 -0
  245. package/src/server/routes/worktrees/index.ts +25 -0
  246. package/src/server/routes/worktrees/list.ts +134 -0
  247. package/src/server/routes/worktrees/remove.ts +102 -0
  248. package/src/server/routes/worktrees/routes.test.ts +326 -0
  249. package/src/server/routes/worktrees/shared.ts +90 -0
  250. package/src/shared/lib/chat/timeline/omp-callbacks.test.ts +36 -0
  251. package/src/shared/lib/chat/timeline/omp-callbacks.ts +18 -0
  252. package/src/shared/lib/chat/timeline/run-signals.test.ts +2 -0
  253. package/src/shared/lib/code/editor/comment-syntax.test.ts +37 -0
  254. package/src/shared/lib/code/editor/comment-syntax.ts +55 -0
  255. package/src/shared/lib/code/editor/find-replace.test.ts +134 -0
  256. package/src/shared/lib/code/editor/{find.ts → find-replace.ts} +23 -107
  257. package/src/shared/lib/code/editor/{find.test.ts → find-scan.test.ts} +19 -73
  258. package/src/shared/lib/code/editor/find-scan.ts +177 -0
  259. package/src/shared/lib/code/editor/keymap.ts +3 -3
  260. package/src/shared/lib/code/editor/lines.test.ts +7 -55
  261. package/src/shared/lib/code/editor/lines.ts +8 -48
  262. package/src/shared/lib/code/editor/marks.test.ts +4 -5
  263. package/src/shared/lib/code/editor/marks.ts +13 -25
  264. package/src/shared/lib/code/editor/theme-loader.ts +97 -0
  265. package/src/shared/lib/code/editor/themes.generated.ts +156 -0
  266. package/src/shared/lib/code/editor/themes.test.ts +165 -0
  267. package/src/shared/lib/code/editor/themes.ts +165 -0
  268. package/src/shared/lib/code/highlight-memo.test.ts +103 -0
  269. package/src/shared/lib/code/highlight-memo.ts +149 -0
  270. package/src/shared/lib/code/highlighter-warmup.ts +112 -0
  271. package/src/shared/lib/code/highlighter.ts +3 -1
  272. package/src/shared/lib/code/idle.ts +37 -0
  273. package/src/shared/lib/code/outline/rows.test.ts +96 -0
  274. package/src/shared/lib/code/outline/rows.ts +94 -0
  275. package/src/shared/lib/code/syntax-highlight.test.ts +1 -79
  276. package/src/shared/lib/code/syntax-highlight.ts +2 -3
  277. package/src/shared/lib/fs/file-kind-classify.test.ts +25 -11
  278. package/src/shared/lib/fs/file-kind.ts +47 -13
  279. package/src/shared/lib/fs/file-tree.test.ts +183 -0
  280. package/src/shared/lib/fs/file-tree.ts +159 -0
  281. package/src/shared/lib/fs/file-url.test.ts +52 -0
  282. package/src/shared/lib/fs/file-url.ts +37 -0
  283. package/src/shared/lib/fs/patch-sides.test.ts +114 -0
  284. package/src/shared/lib/fs/patch-sides.ts +139 -0
  285. package/src/shared/lib/fs/paths.test.ts +24 -1
  286. package/src/shared/lib/fs/paths.ts +17 -0
  287. package/src/shared/lib/fs/worktree/settings-keys.ts +18 -0
  288. package/src/shared/lib/omp/session/prewarm.ts +7 -0
  289. package/src/shared/lib/realtime/protocol.ts +16 -0
  290. package/src/shared/lib/ui/toolbar-overflow.test.ts +83 -0
  291. package/src/shared/lib/ui/toolbar-overflow.ts +86 -0
  292. package/src/shared/lib/updates/shell-arch.ts +54 -0
  293. package/src/shared/lib/updates/shell-ua.ts +24 -4
  294. package/src/shared/lib/workspace/active-project.ts +23 -0
  295. package/src/shared/lib/workspace/editor-view-mode.test.ts +70 -0
  296. package/src/shared/lib/workspace/editor-view-mode.ts +48 -0
  297. package/src/shared/lib/workspace/panel-widths.test.ts +30 -0
  298. package/src/shared/lib/workspace/panel-widths.ts +25 -5
  299. package/src/shared/lib/workspace/session-state/cache.ts +159 -0
  300. package/src/shared/lib/workspace/session-state/harness.test-util.ts +55 -0
  301. package/src/shared/lib/workspace/session-state/merge.ts +100 -0
  302. package/src/shared/lib/workspace/session-state/store-load.test.ts +206 -0
  303. package/src/shared/lib/workspace/session-state/store.test.ts +17 -139
  304. package/src/shared/lib/workspace/session-state/store.ts +92 -130
  305. package/src/shared/lib/workspace/workspace-modules.test.ts +49 -1
  306. package/src/shared/lib/workspace/workspace-panel-widths.ts +124 -0
  307. package/src/shared/lib/workspace/worktree/branch-name.test.ts +95 -0
  308. package/src/shared/lib/workspace/worktree/branch-name.ts +115 -0
  309. package/src/shared/lib/workspace/worktree/types.ts +104 -0
  310. package/src/shared/types/fs.ts +10 -0
  311. package/src/shared/types/realtime.ts +29 -0
  312. package/src/shared/types/settings/state.ts +10 -0
  313. package/src/shared/types/workspace.ts +14 -0
  314. package/src/test-support/file-explorer-disk.test-util.ts +76 -0
  315. package/src/test-support/file-explorer.test-util.ts +71 -40
  316. package/src/test-support/loopback.ts +183 -0
  317. package/dist/client/static/js/index-nzyjf92m.css +0 -1
  318. package/src/client/components/common/code-editor/handler.test.ts +0 -274
  319. package/src/client/components/common/code-editor/handler.ts +0 -175
  320. package/src/client/components/common/code-editor/index.tsx +0 -314
  321. package/src/client/components/common/code-surface/measure.ts +0 -148
  322. package/src/client/components/workspace/diff-panel/SplitView.tsx +0 -209
  323. package/src/client/components/workspace/diff-panel/UnifiedView.tsx +0 -150
  324. package/src/client/hooks/editor/scroll-container.test.ts +0 -102
  325. package/src/client/hooks/editor/scroll-container.ts +0 -26
  326. package/src/client/hooks/editor/use-code-window.ts +0 -100
  327. package/src/client/hooks/editor/use-editor-history.test.ts +0 -216
  328. package/src/client/hooks/editor/use-editor-history.ts +0 -134
  329. package/src/client/hooks/editor/use-occurrences.test.ts +0 -236
  330. package/src/client/hooks/editor/use-occurrences.ts +0 -98
  331. package/src/shared/lib/code/editor/commands.test.ts +0 -155
  332. package/src/shared/lib/code/editor/commands.ts +0 -260
  333. package/src/shared/lib/code/editor/edits.test.ts +0 -100
  334. package/src/shared/lib/code/editor/edits.ts +0 -122
  335. package/src/shared/lib/code/editor/history.test.ts +0 -118
  336. package/src/shared/lib/code/editor/history.ts +0 -101
  337. package/src/shared/lib/code/editor/layers.test.ts +0 -77
  338. package/src/shared/lib/code/editor/layers.ts +0 -67
  339. package/src/shared/lib/code/editor/line-edits.test.ts +0 -131
  340. package/src/shared/lib/code/editor/line-edits.ts +0 -159
  341. package/src/shared/lib/code/editor/occurrences.test.ts +0 -117
  342. package/src/shared/lib/code/editor/occurrences.ts +0 -111
  343. package/src/shared/lib/code/editor/reset-css.test.ts +0 -35
  344. package/src/shared/lib/code/editor/reset-css.ts +0 -32
  345. package/src/shared/lib/code/editor/text-commands.test.ts +0 -178
  346. package/src/shared/lib/code/editor/text-commands.ts +0 -154
  347. package/src/shared/lib/code/lazy-window.test.ts +0 -161
  348. package/src/shared/lib/code/lazy-window.ts +0 -156
  349. package/src/shared/lib/code/windowed-highlight.ts +0 -114
  350. package/src/shared/lib/fs/diff-parser.test.ts +0 -116
  351. package/src/shared/lib/fs/diff-parser.ts +0 -213
@@ -0,0 +1,124 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * The desktop workspace group's three panel widths, resolved from one place.
8
+ *
9
+ * `WorkspacePanels` used to compute these inline, which was correct but made the
10
+ * component's own contract hard to read and impossible to unit-test: every rule
11
+ * here — the two-call order, the pair caps, the leftover width a collapsed chat
12
+ * hands the editor — is arithmetic over values the caller already has.
13
+ *
14
+ * Pure: no hooks, no DOM. The caller measures the group and reads the stored
15
+ * widths; this module decides what those mean.
16
+ */
17
+
18
+ import {
19
+ DEFAULT_EDITOR_FRACTIONS,
20
+ DEFAULT_EDITOR_WIDTHS,
21
+ HANDLE_WIDTH,
22
+ MIN_EDITOR_PANEL_WIDTH,
23
+ panelFraction,
24
+ pairMaxSize,
25
+ resolvePanelWidth,
26
+ type EditorWidthMode,
27
+ type PanelWidths,
28
+ } from '@/shared/lib/workspace/panel-widths';
29
+ import { DEFAULT_RIGHT_PANEL_FRACTION } from '@/shared/lib/workspace/right-panels';
30
+
31
+ export interface WorkspacePanelWidthsArgs {
32
+ /** Measured area of the group, or null before the observer reports. */
33
+ available: number | null;
34
+ /** This session's remembered widths. */
35
+ panelWidths: PanelWidths;
36
+ /** Which slot the editor column is using (a diff wants more room). */
37
+ editorWidthMode: EditorWidthMode;
38
+ /** The right panel's active view, whose slot and defaults apply. */
39
+ activeRightPanel: string;
40
+ /** The active view's own defaults and floor, when it has them. */
41
+ activePanel?: { defaultFraction?: number; defaultWidth?: number; minWidth?: number };
42
+ showEditor: boolean;
43
+ showRightPanel: boolean;
44
+ /** The chat column is collapsed, so it holds no floor and no pair share. */
45
+ chatHidden: boolean;
46
+ }
47
+
48
+ export interface WorkspacePanelWidths {
49
+ right: number;
50
+ editor: number;
51
+ /** Drag ceilings; `undefined` before the group is measured. */
52
+ editorMax: number | undefined;
53
+ rightMax: number | undefined;
54
+ /** Separators actually rendered, which also consume width. */
55
+ handleCount: number;
56
+ /** The right panel's floor, which the drag ceiling also reserves. */
57
+ rightMin: number;
58
+ }
59
+
60
+ export function resolveWorkspacePanelWidths(args: WorkspacePanelWidthsArgs): WorkspacePanelWidths {
61
+ const { available, panelWidths, editorWidthMode, activeRightPanel, activePanel, showEditor, showRightPanel, chatHidden } = args;
62
+
63
+ // The separator before the editor is gone while the chat is collapsed: there
64
+ // is nothing on its left to transfer width to, and the drag's neighbour walk
65
+ // skips a collapsed panel, so it would render a handle that does nothing.
66
+ const handleCount = (showEditor && !chatHidden ? 1 : 0) + (showRightPanel ? 1 : 0);
67
+ const rightMin = activePanel?.minWidth ?? MIN_EDITOR_PANEL_WIDTH;
68
+
69
+ // The editor and the right panel are a COUPLED pair: the right panel is
70
+ // resolved first, reserving the editor's WANTED share (the editor has no
71
+ // result yet), and the editor then reserves the right panel's ACTUAL width.
72
+ // That asymmetry is deliberate — an editor asking for more than its default
73
+ // makes the right panel give way first, and the pair still sums to the total.
74
+ const editorFraction = panelFraction(panelWidths[editorWidthMode], DEFAULT_EDITOR_FRACTIONS[editorWidthMode], available);
75
+ const editorReservePx = available != null && available > 0 ? Math.round(editorFraction * available) : 0;
76
+
77
+ const right = resolvePanelWidth({
78
+ stored: panelWidths.right?.[activeRightPanel],
79
+ defaultFraction: activePanel?.defaultFraction ?? DEFAULT_RIGHT_PANEL_FRACTION,
80
+ defaultPx: activePanel?.defaultWidth ?? MIN_EDITOR_PANEL_WIDTH,
81
+ available,
82
+ min: rightMin,
83
+ pairReservePx: showEditor ? editorReservePx : 0,
84
+ // The editor has not been resolved yet, so only its floor can be reserved.
85
+ siblingWidth: showEditor ? MIN_EDITOR_PANEL_WIDTH : 0,
86
+ handleCount,
87
+ chatHidden,
88
+ });
89
+
90
+ // With the chat collapsed the editor takes everything the right panel and the
91
+ // separators do not. Resolving from its remembered FRACTION would leave the
92
+ // freed width as an empty gap, which is the opposite of what the mode
93
+ // promises; the remembered width is untouched, so leaving restores it.
94
+ const editor = chatHidden
95
+ ? Math.max(
96
+ MIN_EDITOR_PANEL_WIDTH,
97
+ available != null && available > 0
98
+ ? Math.floor(available - (showRightPanel ? right : 0) - handleCount * HANDLE_WIDTH)
99
+ : DEFAULT_EDITOR_WIDTHS[editorWidthMode],
100
+ )
101
+ : resolvePanelWidth({
102
+ stored: panelWidths[editorWidthMode],
103
+ defaultFraction: DEFAULT_EDITOR_FRACTIONS[editorWidthMode],
104
+ defaultPx: DEFAULT_EDITOR_WIDTHS[editorWidthMode],
105
+ available,
106
+ min: MIN_EDITOR_PANEL_WIDTH,
107
+ pairReservePx: showRightPanel ? right : 0,
108
+ siblingWidth: showRightPanel ? right : 0,
109
+ handleCount,
110
+ });
111
+
112
+ return {
113
+ right,
114
+ editor,
115
+ // `resolvePanelWidth` caps what it HANDS to `defaultSize`, but a separator
116
+ // drag bypasses it — the resizer transfers pixels between two panels clamped
117
+ // only by their own `maxSize`. Each max is its own cap and the pair total
118
+ // minus the sibling's FLOOR (it gives way down to its minimum mid-drag).
119
+ editorMax: pairMaxSize({ available, siblingMin: rightMin, siblingOpen: showRightPanel, chatHidden }),
120
+ rightMax: pairMaxSize({ available, siblingMin: MIN_EDITOR_PANEL_WIDTH, siblingOpen: showEditor, chatHidden }),
121
+ handleCount,
122
+ rightMin,
123
+ };
124
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * Branch naming and slugging.
8
+ *
9
+ * Two rules here are load-bearing and each was a defect first:
10
+ *
11
+ * - `slug('..')` is `..`, and `path.join(root, '..')` escapes the root — so a
12
+ * dot-only result is REJECTED, not passed through.
13
+ * - our own `wt/` namespace is stripped from the folder (the folder does not
14
+ * repeat it) while a repository's own namespace stays visible (`feat/x` →
15
+ * `feat-x`), so a folder never reads as a second branch name.
16
+ */
17
+
18
+ import { describe, expect, test } from 'bun:test';
19
+ import {
20
+ defaultWorktreeBranch,
21
+ folderNameForBranch,
22
+ generateBranchSlug,
23
+ isPlausibleBranchName,
24
+ normalizeBranchName,
25
+ slugWorktreeName,
26
+ WORKTREE_BRANCH_PREFIX,
27
+ } from '@/shared/lib/workspace/worktree/branch-name';
28
+
29
+ describe('worktree branch names', () => {
30
+ test('the generated branch carries our namespace', () => {
31
+ const branch = defaultWorktreeBranch();
32
+ expect(branch.startsWith(`${WORKTREE_BRANCH_PREFIX}/`)).toBe(true);
33
+ expect(branch.slice(WORKTREE_BRANCH_PREFIX.length + 1)).toMatch(/^[a-z]+-[a-z]+$/);
34
+ });
35
+
36
+ test('the ref namespace is stripped and only that', () => {
37
+ expect(normalizeBranchName('refs/heads/wt/x')).toBe('wt/x');
38
+ expect(normalizeBranchName('heads/wt/x')).toBe('wt/x');
39
+ expect(normalizeBranchName(' wt/x ')).toBe('wt/x');
40
+ });
41
+ });
42
+
43
+ describe('folder naming', () => {
44
+ test('our namespace is stripped, a repository namespace is kept', () => {
45
+ expect(folderNameForBranch('wt/cosmic-dolphin')).toBe('cosmic-dolphin');
46
+ expect(folderNameForBranch('feat/payments')).toBe('feat-payments');
47
+ expect(folderNameForBranch('refs/heads/wt/yak')).toBe('yak');
48
+ expect(folderNameForBranch('plain')).toBe('plain');
49
+ });
50
+
51
+ test('a folder is one path segment and cannot escape its root', () => {
52
+ expect(slugWorktreeName('../../etc/passwd')).toBe('etc-passwd');
53
+ expect(slugWorktreeName('a b/c')).toBe('a-b-c');
54
+ expect(slugWorktreeName('-weird-')).toBe('weird');
55
+ // The trap: these two ARE valid path segments and would leave the root.
56
+ expect(slugWorktreeName('..')).toBe('');
57
+ expect(slugWorktreeName('.')).toBe('');
58
+ expect(slugWorktreeName('...')).toBe('');
59
+ });
60
+
61
+ test('a folder name is bounded', () => {
62
+ expect(slugWorktreeName('x'.repeat(200)).length).toBeLessThanOrEqual(80);
63
+ });
64
+
65
+ test('a non-Latin branch leaves no slug, so the caller generates a name', () => {
66
+ expect(folderNameForBranch('测试分支')).toBe('');
67
+ });
68
+ });
69
+
70
+ describe('generated slugs', () => {
71
+ test('stay inside the word lists and vary', () => {
72
+ const seen = new Set<string>();
73
+ for (let index = 0; index < 60; index += 1) seen.add(generateBranchSlug());
74
+ expect(seen.size).toBeGreaterThan(10);
75
+ for (const slug of seen) expect(slug).toMatch(/^[a-z]+-[a-z]+$/);
76
+ });
77
+ });
78
+
79
+ describe('branch name plausibility', () => {
80
+ test('accepts what git accepts', () => {
81
+ expect(isPlausibleBranchName('wt/ok')).toBe(true);
82
+ expect(isPlausibleBranchName('feat/payments-v2')).toBe(true);
83
+ });
84
+
85
+ test('refuses what git refuses, before git has to say so', () => {
86
+ expect(isPlausibleBranchName('')).toBe(false);
87
+ expect(isPlausibleBranchName('has space')).toBe(false);
88
+ expect(isPlausibleBranchName('a..b')).toBe(false);
89
+ expect(isPlausibleBranchName('-leading')).toBe(false);
90
+ expect(isPlausibleBranchName('trailing/')).toBe(false);
91
+ expect(isPlausibleBranchName('tilde~1')).toBe(false);
92
+ expect(isPlausibleBranchName('at@{1}')).toBe(false);
93
+ expect(isPlausibleBranchName('a//b')).toBe(false);
94
+ });
95
+ });
@@ -0,0 +1,115 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * Branch naming and slugging for worktrees — ONE module, because the dialog and
8
+ * the create route must agree on what a name means. The dialog pre-fills the
9
+ * branch and derives the folder; the server re-derives both from what arrives.
10
+ *
11
+ * The word lists are OpenChamber's Ubuntu-style pairs (their
12
+ * `lib/git/branchNameGenerator.ts`), and the pick is seeded from
13
+ * `crypto.getRandomValues` rather than `Math.random`, which is available in the
14
+ * browser and in Bun without an import.
15
+ */
16
+
17
+ /**
18
+ * Kabmo's namespace for a generated worktree branch. Deliberately `wt`, not
19
+ * OpenChamber's `openchamber`: omp's own `/wt` already creates `wt/*`, so a
20
+ * repository that has used either tool reads one convention instead of two.
21
+ */
22
+ export const WORKTREE_BRANCH_PREFIX = 'wt';
23
+
24
+ const ADJECTIVES = [
25
+ 'artful', 'bionic', 'cosmic', 'disco', 'focal', 'groovy', 'jammy', 'kinetic',
26
+ 'lunar', 'noble', 'bold', 'brave', 'calm', 'eager', 'gentle', 'happy', 'keen',
27
+ 'lively', 'merry', 'swift', 'warm', 'wise', 'bright', 'clever', 'daring',
28
+ 'agile', 'crisp', 'fresh', 'lucid', 'quick', 'sharp', 'vivid', 'zealous',
29
+ ];
30
+
31
+ const NOUNS = [
32
+ 'aardvark', 'beaver', 'chipmunk', 'dolphin', 'falcon', 'gopher', 'hedgehog',
33
+ 'jackal', 'koala', 'lemur', 'mongoose', 'narwhal', 'otter', 'pangolin',
34
+ 'quokka', 'raccoon', 'salamander', 'toucan', 'walrus', 'yak', 'zebra',
35
+ 'badger', 'condor', 'dingo', 'egret', 'ferret', 'gecko', 'heron', 'iguana',
36
+ ];
37
+
38
+ /** A random index from the platform CSPRNG, so two sessions rarely collide. */
39
+ function pick(values: readonly string[]): string {
40
+ const bytes = new Uint8Array(2);
41
+ crypto.getRandomValues(bytes);
42
+ return values[((bytes[0] ?? 0) << 8 | (bytes[1] ?? 0)) % values.length] as string;
43
+ }
44
+
45
+ /** One adjective-noun pair, e.g. `cosmic-dolphin`. */
46
+ export function generateBranchSlug(): string {
47
+ return `${pick(ADJECTIVES)}-${pick(NOUNS)}`;
48
+ }
49
+
50
+ /** The default branch of a worktree: our namespace plus a generated slug. */
51
+ export function defaultWorktreeBranch(): string {
52
+ return `${WORKTREE_BRANCH_PREFIX}/${generateBranchSlug()}`;
53
+ }
54
+
55
+ /** The branch as git names it, without the ref namespace. */
56
+ export function normalizeBranchName(value: string): string {
57
+ return String(value ?? '')
58
+ .trim()
59
+ .replace(/^refs\/heads\//, '')
60
+ .replace(/^heads\//, '')
61
+ .replace(/\s+/g, '-')
62
+ .replace(/^\/+|\/+$/g, '');
63
+ }
64
+
65
+ /**
66
+ * One path segment, ≤80 chars, safe on every filesystem we target.
67
+ *
68
+ * A leading `.` is stripped and the dot-only results are rejected: `slug('.')`
69
+ * is `.` and `slug('..')` is `..`, and `path.join(root, name)` resolves those to
70
+ * the root itself and to its parent. A folder name that escapes its root is not
71
+ * a name.
72
+ */
73
+ export function slugWorktreeName(value: string): string {
74
+ const slug = String(value ?? '')
75
+ .trim()
76
+ .replace(/^refs\/heads\//, '')
77
+ .replace(/^heads\//, '')
78
+ .replace(/\s+/g, '-')
79
+ .replace(/^\/+|\/+$/g, '')
80
+ .split('/')
81
+ .join('-')
82
+ .replace(/[^A-Za-z0-9._-]+/g, '-')
83
+ .replace(/-+/g, '-')
84
+ .replace(/^[.-]+|[.-]+$/g, '')
85
+ .slice(0, 80);
86
+ return slug === '.' || slug === '..' ? '' : slug;
87
+ }
88
+
89
+ /**
90
+ * The folder a branch gets. Our own `wt/` namespace is stripped — the folder
91
+ * does not repeat it — while a repository's own namespaces stay visible
92
+ * (`feat/x` → `feat-x`). A non-Latin branch (`测试分支`) leaves no slug, and the
93
+ * caller falls back to a generated name rather than an empty folder.
94
+ */
95
+ export function folderNameForBranch(branch: string): string {
96
+ const withoutRef = normalizeBranchName(branch);
97
+ const namespaced = `${WORKTREE_BRANCH_PREFIX}/`;
98
+ const withoutOwnPrefix = withoutRef.startsWith(namespaced)
99
+ ? withoutRef.slice(namespaced.length)
100
+ : withoutRef;
101
+ return slugWorktreeName(withoutOwnPrefix);
102
+ }
103
+
104
+ /**
105
+ * Whether git would plausibly accept this as a branch name. Rejects only what
106
+ * `git check-ref-format` rejects in every case — the server still asks git
107
+ * itself, because a plausible name can still collide with an existing ref.
108
+ */
109
+ export function isPlausibleBranchName(value: string): boolean {
110
+ if (!value) return false;
111
+ if (/^[.\-/]/.test(value) || /[/.]$/.test(value)) return false;
112
+ if (value.includes('..') || value.includes('//') || value.includes('@{')) return false;
113
+ if (/[\s~^:?*[\\]/.test(value)) return false;
114
+ return true;
115
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * Worktree shapes shared by the server and the client.
8
+ *
9
+ * A worktree session is a normal session whose cwd is a linked checkout, so the
10
+ * only thing that crosses the wire is where it is and whether it is still there
11
+ * — everything else is read from git.
12
+ */
13
+
14
+ /** One row of `GET /api/worktrees`, and what a create answers with. */
15
+ export interface WorktreeInfo {
16
+ /** Absolute, canonical path of the checkout. */
17
+ path: string;
18
+ /** Branch checked out there (`refs/heads/` stripped); `''` when detached. */
19
+ branch: string;
20
+ /** The commit HEAD points at — what a detached row names instead of a branch. */
21
+ head: string;
22
+ detached: boolean;
23
+ /** git reports it prunable, or the directory is gone. */
24
+ missing: boolean;
25
+ /** The main checkout — listed, but never removable. */
26
+ isPrimary: boolean;
27
+ /** Changed + untracked count. */
28
+ dirty: number;
29
+ /**
30
+ * Commits on this branch that are not on the ref it was created from. Gates
31
+ * the delete dialog's branch checkbox, because `git branch -d` refuses an
32
+ * unmerged branch and a checkbox that cannot succeed is worse than none.
33
+ */
34
+ aheadOfStart: number;
35
+ /** The ref recorded when the worktree was created; `''` for an existing branch. */
36
+ startRef: string;
37
+ /** Chats whose cwd is inside this worktree. */
38
+ sessions: number;
39
+ }
40
+
41
+ /**
42
+ * The per-session view, carried on `SessionItemData` so every layout and every
43
+ * plugin reads one field instead of asking git.
44
+ */
45
+ export interface SessionWorktreeInfo {
46
+ branch: string;
47
+ /** Canonical worktree root. */
48
+ path: string;
49
+ /** The checkout this worktree belongs to — the folder's `project_path`. */
50
+ primaryRoot: string;
51
+ /** `missing` when git still tracks it but the directory is gone. */
52
+ status: 'ready' | 'missing';
53
+ detached: boolean;
54
+ }
55
+
56
+ /** Where a chat that has not started will run. */
57
+ export interface SpawnTarget {
58
+ folderId: number | null;
59
+ /** Absolute worktree path, or null for the folder's own checkout. */
60
+ worktreePath: string | null;
61
+ }
62
+
63
+ /** Why a worktree action was refused. Codes are stable; messages are not. */
64
+ export type WorktreeErrorCode =
65
+ | 'not_a_repo'
66
+ | 'no_commits'
67
+ | 'invalid_name'
68
+ | 'branch_exists'
69
+ | 'branch_in_use'
70
+ | 'path_exists'
71
+ | 'create_failed'
72
+ | 'not_a_worktree'
73
+ | 'primary_checkout'
74
+ | 'dirty'
75
+ | 'dirty_carryover'
76
+ | 'remove_failed'
77
+ | 'mock_mode';
78
+
79
+ /** One refusal, with the field it belongs to so a dialog can mark the input. */
80
+ export interface WorktreeFailure {
81
+ code: WorktreeErrorCode;
82
+ message: string;
83
+ field?: 'branch' | 'folder' | 'location';
84
+ /** Present for `dirty`: how many files would be lost. */
85
+ count?: number;
86
+ }
87
+
88
+ /** `GET /api/worktrees`. */
89
+ export interface WorktreeListPayload {
90
+ worktrees: WorktreeInfo[];
91
+ /** The resolved parent directory new worktrees are created in. */
92
+ base: string;
93
+ isRepo: boolean;
94
+ /** Present when the listing failed — never substituted with an empty list. */
95
+ error?: string;
96
+ }
97
+
98
+ /** `POST /api/worktrees/validate`. */
99
+ export interface WorktreeValidation {
100
+ ok: boolean;
101
+ failures: WorktreeFailure[];
102
+ /** The name and directory the create would use, so the dialog can show them. */
103
+ resolved?: { name: string; directory: string; branch: string };
104
+ }
@@ -74,4 +74,14 @@ export interface FsActionResponse {
74
74
  error?: string;
75
75
  /** The path a create or a transfer landed at, relative to the listed root. */
76
76
  path?: string;
77
+ /** What a create made, so the tree can draw the row it just added. */
78
+ kind?: 'file' | 'folder';
79
+ /**
80
+ * The path a rename moved FROM, relative to the listed root.
81
+ *
82
+ * A rename answers with both ends so the panel can move the row in place:
83
+ * re-reading every open folder to discover a path it was just told is the
84
+ * reload this field exists to avoid.
85
+ */
86
+ previousPath?: string;
77
87
  }
@@ -32,3 +32,32 @@ export interface SidebarStatusEntry {
32
32
 
33
33
  /** `sidebar:status` — keyed by session id. Absent id means "no volatile fields". */
34
34
  export type SidebarStatusPayload = Record<string, SidebarStatusEntry>;
35
+
36
+ /**
37
+ * One live session's row fields that can still move between turn boundaries.
38
+ *
39
+ * Exactly two, and the list is closed on purpose: everything else about a row
40
+ * (title, folder, cwd, worktree, archived) only changes on a STRUCTURAL event
41
+ * that publishes the structure for its own reason. These two are the ones a
42
+ * run advances mid-turn — the transcript appends, and a subagent's transcript
43
+ * appears beside it (which is what lights the row's roster chevron).
44
+ */
45
+ export interface SidebarActivityEntry {
46
+ /** Newest transcript entry's ISO timestamp — the row's age and its
47
+ * `LATEST_SESSION` position. */
48
+ updatedAt?: string;
49
+ /** Whether the session has any subagent transcript on disk. */
50
+ hasSubagents?: boolean;
51
+ }
52
+
53
+ /**
54
+ * `sidebar:activity` — session id → the live fields above, for the sessions
55
+ * with a run in flight.
56
+ *
57
+ * Only a running session can move between turn boundaries, so an id that is
58
+ * absent here simply keeps what the structure gave it. Both sides read the same
59
+ * sources (the newest JSONL entry's own `timestamp`, and the subagent
60
+ * transcript directory), so the client merges field by field and lets the
61
+ * newer stamp win.
62
+ */
63
+ export type SidebarActivityPayload = Record<string, SidebarActivityEntry>;
@@ -48,6 +48,16 @@ export interface SettingsState {
48
48
  * cannot render a broken editor, only a different one.
49
49
  */
50
50
  editorFont: string;
51
+ /**
52
+ * Colour theme for the code editor, INDEPENDENT of `theme` above.
53
+ *
54
+ * `auto` (the default) keeps the editor on the app palette. Any other value
55
+ * is a Shiki theme id from `EDITOR_THEMES` — the editor then paints its own
56
+ * surface, gutter, selection and tokens while the console around it keeps the
57
+ * app palette. `resolveEditorTheme` treats an id this build no longer ships
58
+ * as `auto`, so a downgrade renders the app palette rather than half a theme.
59
+ */
60
+ editorTheme: string;
51
61
  /** Generate a session title from the first run. omp suppresses its
52
62
  * own auto-titling under `--mode rpc-ui` (PI_NO_TITLE), so without this a
53
63
  * Kabmo session keeps its `New Session - <timestamp>` placeholder. Asked
@@ -3,6 +3,8 @@
3
3
  * JSONL discovery and use the omp session UUID (string) as id — hence the
4
4
  * union with string ids. Mock/demo mode keeps numeric SQLite ids.
5
5
  */
6
+ import type { SessionWorktreeInfo } from '@/shared/lib/workspace/worktree/types';
7
+
6
8
  export interface SessionItemData {
7
9
  id: number | string;
8
10
  folder_id: number | string;
@@ -38,6 +40,18 @@ export interface SessionItemData {
38
40
  * saying so. Absent means the row is not waiting (the normal case).
39
41
  */
40
42
  waitingFor?: { waitId: string; sessionId: string; title: string | null };
43
+ /**
44
+ * The directory the session actually runs in. A worktree session's cwd is the
45
+ * linked checkout, not the folder's `project_path` — the difference the panels
46
+ * have to honour and the sidebar badge names.
47
+ */
48
+ cwd?: string;
49
+ /**
50
+ * The worktree this session runs in, when its cwd is a linked checkout.
51
+ * Absent for an ordinary session (the common case) and for a session whose cwd
52
+ * is outside every worktree of its project, which the row draws the same way.
53
+ */
54
+ worktree?: SessionWorktreeInfo;
41
55
  }
42
56
 
43
57
  /**
@@ -0,0 +1,76 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * The Files panel harness's fake disk, and the two writes a spec drives it with.
8
+ *
9
+ * A separate module because the harness file is at the repository's line
10
+ * ceiling, and because the disk is the part with its own rules: a create and a
11
+ * rename MUTATE it the way the real route mutates the filesystem, so the very
12
+ * next read agrees with the row the panel just drew. A stub that answered
13
+ * "created" while leaving the folder empty would make every local-edit spec pass
14
+ * against a tree the server would contradict.
15
+ */
16
+
17
+ /** The fake disk: `path -> child -> (null for a folder, 'file' for a file)`. */
18
+ export type Disk = Record<string, Record<string, null | 'file'> | undefined>;
19
+
20
+ /** One directory's own listing, created on demand. */
21
+ function listingOf(disk: Disk, path: string): Record<string, null | 'file'> {
22
+ disk[path] = disk[path] ?? {};
23
+ return disk[path]!;
24
+ }
25
+
26
+ /**
27
+ * Apply a create: every directory on the way exists afterwards, and the entry
28
+ * sits in its parent's listing. Returns the entry's path relative to the root.
29
+ */
30
+ export function writeToDisk(disk: Disk, folder: string, name: string, kind: 'file' | 'folder'): string {
31
+ const segments = name.split('/');
32
+ let parent = folder === '.' ? '' : folder;
33
+ for (const segment of segments.slice(0, -1)) {
34
+ listingOf(disk, parent || '.')[segment] = null;
35
+ parent = parent ? `${parent}/${segment}` : segment;
36
+ listingOf(disk, parent);
37
+ }
38
+ const leaf = segments[segments.length - 1];
39
+ listingOf(disk, parent || '.')[leaf] = kind === 'folder' ? null : 'file';
40
+ const created = parent ? `${parent}/${leaf}` : leaf;
41
+ if (kind === 'folder') listingOf(disk, created);
42
+ return created;
43
+ }
44
+
45
+ /** Split a relative path into its directory (`.` at the root) and its name. */
46
+ function splitPath(path: string): { dir: string; name: string } {
47
+ const cut = path.lastIndexOf('/');
48
+ return cut === -1 ? { dir: '.', name: path } : { dir: path.slice(0, cut), name: path.slice(cut + 1) };
49
+ }
50
+
51
+ /** Apply a rename: remove the old entry, write the new one, kind preserved. */
52
+ export function moveOnDisk(disk: Disk, fromPath: string, toPath: string): void {
53
+ const from = splitPath(fromPath);
54
+ const source = disk[from.dir];
55
+ const kind = source?.[from.name] === null ? 'folder' : 'file';
56
+ if (source) delete source[from.name];
57
+ const to = splitPath(toPath);
58
+ writeToDisk(disk, to.dir, to.name, kind);
59
+ }
60
+
61
+ /** Whether `path` already names an entry of the fake disk. */
62
+ export function diskHas(disk: Disk, path: string): boolean {
63
+ const { dir, name } = splitPath(path);
64
+ return (disk[dir] ?? {})[name] !== undefined;
65
+ }
66
+
67
+ /** One directory's entries as the server emits them: folders with `children: null`. */
68
+ export function entriesOf(disk: Disk, path: string): unknown[] {
69
+ const dir = disk[path] ?? {};
70
+ return Object.keys(dir).map((child) => {
71
+ const rel = path === '.' ? child : `${path}/${child}`;
72
+ return dir[child] === null
73
+ ? { id: rel, name: child, type: 'folder', path: rel, children: null, ignored: false }
74
+ : { id: rel, name: child, type: 'file', path: rel, ignored: false };
75
+ });
76
+ }