@cueloop/client 0.1.0-alpha.0 → 0.1.0-alpha.100

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 (345) hide show
  1. package/native/build-pty.sh +51 -0
  2. package/native/build.sh +82 -0
  3. package/native/darwin-arm64/libcuelooppty.dylib +0 -0
  4. package/native/darwin-arm64/libcueloopvt.dylib +0 -0
  5. package/native/src/pty.zig +193 -0
  6. package/native/src/shim.zig +114 -0
  7. package/package.json +41 -27
  8. package/src/annotations/annotation-palette.ts +62 -0
  9. package/src/annotations/components/AnnotationCards.stories.tsx +86 -0
  10. package/src/annotations/components/AnnotationCards.test.tsx +42 -0
  11. package/src/annotations/components/AnnotationCards.tsx +364 -0
  12. package/src/annotations/components/DiscussionMarkerRail.stories.tsx +54 -0
  13. package/src/annotations/components/DiscussionMarkerRail.tsx +186 -0
  14. package/src/annotations/discussions.ts +77 -0
  15. package/src/annotations/thread-selection.test.ts +210 -0
  16. package/src/annotations/thread-selection.ts +242 -0
  17. package/src/annotations/use-annotation-surface.tsx +1238 -0
  18. package/src/app/App.appearance.test.tsx +53 -0
  19. package/src/app/App.autoclose.test.tsx +144 -0
  20. package/src/app/App.deleted-scroll.test.tsx +187 -0
  21. package/src/app/App.diff.test.tsx +313 -0
  22. package/src/app/App.export.test.tsx +105 -0
  23. package/src/app/App.focus.test.ts +89 -0
  24. package/src/app/App.identity.test.tsx +84 -0
  25. package/src/app/App.plan-render.test.tsx +209 -0
  26. package/src/app/App.plan-review.test.tsx +428 -0
  27. package/src/app/App.prototype-markdown.test.tsx +128 -0
  28. package/src/app/App.prototype.test.tsx +176 -0
  29. package/src/app/App.readonly.test.tsx +210 -0
  30. package/src/app/App.regression.test.tsx +99 -0
  31. package/src/app/App.rendering.test.tsx +134 -0
  32. package/src/app/App.share-dialog.test.tsx +177 -0
  33. package/src/app/App.submit-confirm.test.tsx +267 -0
  34. package/src/app/App.test.tsx +496 -0
  35. package/src/app/App.thread-agent.test.ts +12 -0
  36. package/src/app/App.toast.test.tsx +124 -0
  37. package/src/app/App.tsx +1650 -0
  38. package/src/app/App.visual.test.tsx +59 -0
  39. package/src/app/App.walk.test.tsx +299 -0
  40. package/src/app/App.workbench.test.tsx +1149 -0
  41. package/src/app/app-key-state.ts +61 -0
  42. package/src/app/app-screens.tsx +606 -0
  43. package/src/app/app-view-model.ts +168 -0
  44. package/src/app/components/AppHeader.stories.tsx +50 -0
  45. package/src/app/components/AppHeader.tsx +81 -0
  46. package/src/app/components/AppShell.brand.test.tsx +58 -0
  47. package/src/app/components/AppShell.extensions.test.tsx +191 -0
  48. package/src/app/components/AppShell.stories.tsx +89 -0
  49. package/src/app/components/AppShell.tsx +358 -0
  50. package/src/app/components/AppShell.zoom.test.tsx +149 -0
  51. package/src/app/components/Breadcrumb.stories.tsx +19 -0
  52. package/src/app/components/Breadcrumb.tsx +45 -0
  53. package/src/app/components/MenuBar.stories.tsx +28 -0
  54. package/src/app/components/MenuBar.tsx +44 -0
  55. package/src/app/components/PaneGrid.stories.tsx +84 -0
  56. package/src/app/components/PaneGrid.tsx +128 -0
  57. package/src/app/components/PanelColumn.stories.tsx +25 -0
  58. package/src/app/components/PanelColumn.tsx +93 -0
  59. package/src/app/components/ShellHeader.stories.tsx +50 -0
  60. package/src/app/components/ShellHeader.tsx +137 -0
  61. package/src/app/components/WorkspacePanels.stories.tsx +36 -0
  62. package/src/app/components/WorkspacePanels.tsx +120 -0
  63. package/src/app/components/pages/Workspace.stories.tsx +181 -0
  64. package/src/app/intent-dispatch.test.ts +728 -0
  65. package/src/app/intent-dispatch.ts +687 -0
  66. package/src/app/launch-layout.test.ts +78 -0
  67. package/src/app/launch-layout.ts +57 -0
  68. package/src/app/ready-signal.test.ts +85 -0
  69. package/src/app/ready-signal.ts +52 -0
  70. package/src/app/run.ts +109 -0
  71. package/src/{serve.test.ts → app/serve.test.ts} +24 -12
  72. package/src/app/serve.ts +111 -0
  73. package/src/app/version.ts +5 -0
  74. package/src/appearance/components/theme-context.stories.tsx +46 -0
  75. package/src/appearance/components/theme-context.tsx +37 -0
  76. package/src/appearance/theme-presets.test.ts +51 -0
  77. package/src/appearance/theme-presets.ts +219 -0
  78. package/src/appearance/theme.ts +112 -0
  79. package/src/appearance/visual/contrast.test.ts +35 -0
  80. package/src/appearance/visual/contrast.ts +33 -0
  81. package/src/diff/components/AnnotatableFileView.stories.tsx +42 -0
  82. package/src/diff/components/AnnotatableFileView.test.tsx +157 -0
  83. package/src/diff/components/AnnotatableFileView.tsx +141 -0
  84. package/src/diff/components/BareWorkbenchFileView.stories.tsx +27 -0
  85. package/src/diff/components/BareWorkbenchFileView.tsx +63 -0
  86. package/src/diff/components/DiffContentView.scroll.test.tsx +434 -0
  87. package/src/diff/components/DiffContentView.split.test.tsx +140 -0
  88. package/src/diff/components/DiffContentView.stories.tsx +80 -0
  89. package/src/diff/components/DiffContentView.syntax.test.tsx +82 -0
  90. package/src/diff/components/DiffContentView.tsx +1055 -0
  91. package/src/diff/components/WalkWizard.stories.tsx +100 -0
  92. package/src/diff/components/WalkWizard.tsx +146 -0
  93. package/src/diff/components/diff-content-view-layout.test.ts +198 -0
  94. package/src/diff/components/diff-content-view-layout.ts +84 -0
  95. package/src/diff/diff-fold.test.ts +78 -0
  96. package/src/diff/diff-fold.ts +126 -0
  97. package/src/diff/diff-hunk-curate.test.ts +315 -0
  98. package/src/diff/diff-hunk-curate.ts +152 -0
  99. package/src/diff/diff-intraline.test.ts +239 -0
  100. package/src/diff/diff-intraline.ts +238 -0
  101. package/src/diff/diff-syntax.test.ts +125 -0
  102. package/src/diff/diff-syntax.ts +186 -0
  103. package/src/diff/split-diff.test.ts +110 -0
  104. package/src/diff/split-diff.ts +144 -0
  105. package/src/diff/view-diff.test.ts +325 -0
  106. package/src/diff/view-diff.ts +522 -0
  107. package/src/diff/walk.test.ts +121 -0
  108. package/src/diff/walk.ts +73 -0
  109. package/src/index.ts +31 -2
  110. package/src/integrations/attribution.test.ts +31 -0
  111. package/src/integrations/attribution.ts +42 -0
  112. package/src/integrations/client-extension-registry.test.tsx +61 -0
  113. package/src/integrations/client-extension-registry.ts +159 -0
  114. package/src/integrations/clipboard.test.ts +23 -0
  115. package/src/integrations/clipboard.ts +33 -0
  116. package/src/integrations/components/ExtensionZone.stories.tsx +28 -0
  117. package/src/integrations/components/ExtensionZone.tsx +145 -0
  118. package/src/integrations/components/ShareDialog.stories.tsx +121 -0
  119. package/src/integrations/components/ShareDialog.tsx +550 -0
  120. package/src/integrations/components/share-dialog-store.ts +55 -0
  121. package/src/integrations/editor.test.ts +149 -0
  122. package/src/integrations/editor.ts +160 -0
  123. package/src/integrations/github-identity.test.ts +58 -0
  124. package/src/integrations/github-identity.ts +41 -0
  125. package/src/integrations/herdr.test.ts +43 -0
  126. package/src/integrations/herdr.ts +38 -0
  127. package/src/integrations/integrations.test.ts +41 -0
  128. package/src/integrations/integrations.ts +32 -0
  129. package/src/integrations/pasted-image.test.ts +67 -0
  130. package/src/integrations/pasted-image.ts +60 -0
  131. package/src/integrations/share.ts +238 -0
  132. package/src/keyboard/components/CompletionOverlay.stories.tsx +37 -0
  133. package/src/keyboard/components/CompletionOverlay.tsx +75 -0
  134. package/src/keyboard/components/HandleChipsInput.stories.tsx +24 -0
  135. package/src/keyboard/components/HandleChipsInput.test.tsx +35 -0
  136. package/src/keyboard/components/HandleChipsInput.tsx +143 -0
  137. package/src/keyboard/components/KeybindsDialog.stories.tsx +31 -0
  138. package/src/keyboard/components/KeybindsDialog.tsx +103 -0
  139. package/src/keyboard/components/NavModeHint.stories.tsx +40 -0
  140. package/src/keyboard/components/NavModeHint.tsx +31 -0
  141. package/src/keyboard/components/SlashComposer.stories.tsx +37 -0
  142. package/src/keyboard/components/SlashComposer.tsx +87 -0
  143. package/src/keyboard/key-bindings.test.ts +156 -0
  144. package/src/keyboard/key-bindings.ts +459 -0
  145. package/src/keyboard/keymap.test.ts +611 -0
  146. package/src/keyboard/keymap.ts +395 -0
  147. package/src/keyboard/skills.test.ts +74 -0
  148. package/src/keyboard/skills.ts +75 -0
  149. package/src/keyboard/slash-palette.test.ts +103 -0
  150. package/src/keyboard/slash-palette.ts +129 -0
  151. package/src/keyboard/thread-chords.test.ts +79 -0
  152. package/src/keyboard/thread-chords.ts +163 -0
  153. package/src/keyboard/use-shared-keyboard.ts +55 -0
  154. package/src/markdown/__snapshots__/view-plan.regression.test.ts.snap +100 -0
  155. package/src/markdown/components/MarkdownGridBlock.stories.tsx +33 -0
  156. package/src/markdown/components/MarkdownGridBlock.tsx +72 -0
  157. package/src/markdown/components/MarkdownThreadEditor.stories.tsx +15 -0
  158. package/src/markdown/components/MarkdownThreadEditor.test.tsx +98 -0
  159. package/src/markdown/components/MarkdownThreadEditor.tsx +160 -0
  160. package/src/markdown/components/ThreadView.selection.test.tsx +951 -0
  161. package/src/markdown/components/ThreadView.slash.test.tsx +146 -0
  162. package/src/markdown/components/ThreadView.stories.tsx +26 -0
  163. package/src/markdown/components/ThreadView.tsx +560 -0
  164. package/src/markdown/mark-runs.ts +121 -0
  165. package/src/markdown/markdown-editor-syntax-style.ts +50 -0
  166. package/src/markdown/markdown-grid.test.ts +101 -0
  167. package/src/markdown/markdown-grid.ts +203 -0
  168. package/src/markdown/markdown-highlight.test.ts +61 -0
  169. package/src/markdown/markdown-highlight.ts +124 -0
  170. package/src/markdown/thread-code-syntax.ts +45 -0
  171. package/src/markdown/use-thread-body-editing.tsx +54 -0
  172. package/src/markdown/view-plan.regression.test.ts +60 -0
  173. package/src/markdown/view-plan.test.ts +545 -0
  174. package/src/markdown/view-plan.ts +1009 -0
  175. package/src/performance/otlp-trace.test.ts +119 -0
  176. package/src/performance/otlp-trace.ts +134 -0
  177. package/src/performance/perf-report.ts +18 -0
  178. package/src/performance/perf-timings.test.ts +18 -0
  179. package/src/performance/perf-timings.ts +39 -0
  180. package/src/prototype/components/PrototypeContentView.stories.tsx +19 -0
  181. package/src/prototype/components/PrototypeContentView.tsx +442 -0
  182. package/src/prototype/prototype-browser.test.ts +81 -0
  183. package/src/prototype/prototype-browser.ts +321 -0
  184. package/src/prototype/prototype-pixels.tsx +35 -0
  185. package/src/settings/components/SettingsDialog.stories.tsx +117 -0
  186. package/src/settings/components/SettingsDialog.tsx +183 -0
  187. package/src/settings/components/SettingsRows.stories.tsx +18 -0
  188. package/src/settings/components/SettingsRows.tsx +112 -0
  189. package/src/settings/components/quick-actions-editor.stories.tsx +58 -0
  190. package/src/settings/components/quick-actions-editor.test.tsx +120 -0
  191. package/src/settings/components/quick-actions-editor.tsx +222 -0
  192. package/src/settings/config-identity.test.ts +78 -0
  193. package/src/settings/config.test.ts +676 -0
  194. package/src/settings/config.ts +628 -0
  195. package/src/settings/use-settings-dialog.tsx +351 -0
  196. package/src/stories/__snapshots__/stories.test.tsx.snap +2358 -0
  197. package/src/stories/stories-app.tsx +196 -0
  198. package/src/stories/stories.test.tsx +74 -0
  199. package/src/stories/story-fixtures.ts +102 -0
  200. package/src/stories/story-tree.test.ts +108 -0
  201. package/src/stories/story.ts +154 -0
  202. package/src/terminal/ansi-screen.test.ts +28 -0
  203. package/src/terminal/ansi-screen.ts +72 -0
  204. package/src/terminal/components/AnsiScreen.stories.tsx +12 -0
  205. package/src/terminal/components/AnsiScreen.tsx +28 -0
  206. package/src/terminal/components/terminal-pane.test.tsx +40 -0
  207. package/src/terminal/components/terminal-pane.ts +196 -0
  208. package/src/terminal/ghostty-terminal.test.ts +85 -0
  209. package/src/terminal/ghostty-terminal.ts +197 -0
  210. package/src/terminal/kitty-image.test.ts +66 -0
  211. package/src/terminal/kitty-image.ts +137 -0
  212. package/src/terminal/pty.test.ts +81 -0
  213. package/src/terminal/pty.ts +246 -0
  214. package/src/testing/test-support.test.ts +264 -0
  215. package/src/testing/test-support.ts +367 -0
  216. package/src/thread/agent-continuation.test.ts +32 -0
  217. package/src/thread/agent-continuation.ts +13 -0
  218. package/src/thread/agent-transcript.test.ts +161 -0
  219. package/src/thread/agent-transcript.ts +336 -0
  220. package/src/thread/components/AgentConfigControls.stories.tsx +39 -0
  221. package/src/thread/components/AgentConfigControls.test.tsx +112 -0
  222. package/src/thread/components/AgentConfigControls.tsx +114 -0
  223. package/src/thread/components/AgentThreadPane.stories.tsx +105 -0
  224. package/src/thread/components/AgentThreadPane.test.tsx +829 -0
  225. package/src/thread/components/AgentThreadPane.tsx +367 -0
  226. package/src/thread/components/ThreadFooter.stories.tsx +30 -0
  227. package/src/thread/components/ThreadFooter.test.tsx +49 -0
  228. package/src/thread/components/ThreadFooter.tsx +77 -0
  229. package/src/thread/components/ThreadTree.stories.tsx +46 -0
  230. package/src/thread/components/ThreadTree.test.tsx +41 -0
  231. package/src/thread/components/ThreadTree.tsx +267 -0
  232. package/src/thread/components/thread-tree.test.ts +137 -0
  233. package/src/thread/components/thread-tree.ts +112 -0
  234. package/src/thread/thread-controller.curate.test.ts +502 -0
  235. package/src/thread/thread-controller.derivation.test.ts +62 -0
  236. package/src/thread/thread-controller.live-diff.test.ts +357 -0
  237. package/src/thread/thread-controller.optimistic.test.ts +69 -0
  238. package/src/thread/thread-controller.share.test.ts +573 -0
  239. package/src/thread/thread-controller.switch-cache.test.ts +174 -0
  240. package/src/thread/thread-controller.tree.test.ts +263 -0
  241. package/src/thread/thread-controller.ts +2018 -0
  242. package/src/thread/tree-view.test.ts +128 -0
  243. package/src/thread/tree-view.ts +146 -0
  244. package/src/thread/use-thread-agent.test.tsx +208 -0
  245. package/src/thread/use-thread-agent.ts +111 -0
  246. package/src/ui/components/CodeBlock.stories.tsx +42 -0
  247. package/src/ui/components/CodeBlock.tsx +80 -0
  248. package/src/ui/components/ConfirmCard.stories.tsx +37 -0
  249. package/src/ui/components/ConfirmCard.tsx +151 -0
  250. package/src/ui/components/ConfirmDialog.stories.tsx +21 -0
  251. package/src/ui/components/ConfirmDialog.tsx +72 -0
  252. package/src/ui/components/OverlayScrollbar.stories.tsx +33 -0
  253. package/src/ui/components/OverlayScrollbar.test.tsx +139 -0
  254. package/src/ui/components/OverlayScrollbar.tsx +129 -0
  255. package/src/ui/components/PromptDialog.stories.tsx +36 -0
  256. package/src/ui/components/PromptDialog.test.tsx +65 -0
  257. package/src/ui/components/PromptDialog.tsx +99 -0
  258. package/src/ui/components/RootOverlay.stories.tsx +45 -0
  259. package/src/ui/components/RootOverlay.tsx +49 -0
  260. package/src/ui/components/ScrollArea.stories.tsx +20 -0
  261. package/src/ui/components/ScrollArea.tsx +61 -0
  262. package/src/ui/components/Toast.stories.tsx +19 -0
  263. package/src/ui/components/Toast.tsx +54 -0
  264. package/src/ui/components/Tooltip.stories.tsx +26 -0
  265. package/src/ui/components/Tooltip.tsx +72 -0
  266. package/src/ui/components/menu-control.stories.tsx +35 -0
  267. package/src/ui/components/menu-control.tsx +43 -0
  268. package/src/ui/components/primitives/Button.stories.tsx +32 -0
  269. package/src/ui/components/primitives/Button.tsx +57 -0
  270. package/src/ui/components/primitives/Card.stories.tsx +25 -0
  271. package/src/ui/components/primitives/Card.tsx +62 -0
  272. package/src/ui/components/primitives/Dialog.stories.tsx +24 -0
  273. package/src/ui/components/primitives/Dialog.test.tsx +38 -0
  274. package/src/ui/components/primitives/Dialog.tsx +69 -0
  275. package/src/ui/components/primitives/DialogActions.stories.tsx +20 -0
  276. package/src/ui/components/primitives/DialogActions.tsx +39 -0
  277. package/src/ui/components/primitives/IconButton.stories.tsx +36 -0
  278. package/src/ui/components/primitives/IconButton.tsx +58 -0
  279. package/src/ui/components/primitives/StatusBar.stories.tsx +11 -0
  280. package/src/ui/components/primitives/StatusBar.tsx +23 -0
  281. package/src/ui/components/primitives/Switch.stories.tsx +17 -0
  282. package/src/ui/components/primitives/Switch.tsx +32 -0
  283. package/src/ui/components/primitives/Tabs.stories.tsx +29 -0
  284. package/src/ui/components/primitives/Tabs.tsx +101 -0
  285. package/src/ui/components/primitives/Toolbar.stories.tsx +17 -0
  286. package/src/ui/components/primitives/Toolbar.tsx +14 -0
  287. package/src/ui/components/primitives/Tree.stories.tsx +124 -0
  288. package/src/ui/components/primitives/Tree.tsx +133 -0
  289. package/src/ui/components/primitives/frame.test.ts +12 -0
  290. package/src/ui/components/primitives/frame.ts +17 -0
  291. package/src/ui/components/primitives/icons.ts +61 -0
  292. package/src/ui/components/primitives/tree-model.test.ts +99 -0
  293. package/src/ui/components/primitives/tree-model.ts +137 -0
  294. package/src/ui/components/syntax-highlight.ts +133 -0
  295. package/src/ui/components/truncate-text.ts +7 -0
  296. package/src/ui/components/truncate-title.test.ts +31 -0
  297. package/src/ui/components/truncate-title.ts +15 -0
  298. package/src/ui/use-frame-measure.ts +74 -0
  299. package/src/ui/use-terminal-virtualizer.ts +259 -0
  300. package/src/workbench/components/ChangesColumn.stories.tsx +44 -0
  301. package/src/workbench/components/ChangesColumn.tsx +203 -0
  302. package/src/workbench/components/ChangesFileTree.test.tsx +181 -0
  303. package/src/workbench/components/EditorGrid.split.test.tsx +118 -0
  304. package/src/workbench/components/EditorGrid.stories.tsx +86 -0
  305. package/src/workbench/components/EditorGrid.tabs.test.tsx +118 -0
  306. package/src/workbench/components/EditorGrid.tsx +544 -0
  307. package/src/workbench/components/EditorGrid.zoom.test.tsx +132 -0
  308. package/src/workbench/components/FileContentsView.stories.tsx +16 -0
  309. package/src/workbench/components/FileContentsView.test.tsx +29 -0
  310. package/src/workbench/components/FileContentsView.tsx +100 -0
  311. package/src/workbench/components/GridTabContent.actions.test.tsx +151 -0
  312. package/src/workbench/components/GridTabContent.stories.tsx +59 -0
  313. package/src/workbench/components/GridTabContent.tsx +193 -0
  314. package/src/workbench/components/ProjectTreeView.stories.tsx +25 -0
  315. package/src/workbench/components/ProjectTreeView.test.tsx +236 -0
  316. package/src/workbench/components/ProjectTreeView.tsx +151 -0
  317. package/src/workbench/components/TreePane.stories.tsx +106 -0
  318. package/src/workbench/components/TreePane.tsx +155 -0
  319. package/src/workbench/components/WelcomePlayground.stories.tsx +12 -0
  320. package/src/workbench/components/WelcomePlayground.test.tsx +83 -0
  321. package/src/workbench/components/WelcomePlayground.tsx +138 -0
  322. package/src/workbench/components/editor-grid.test.ts +184 -0
  323. package/src/workbench/components/editor-grid.ts +281 -0
  324. package/src/workbench/components/file-tree.test.ts +60 -0
  325. package/src/workbench/components/file-tree.ts +112 -0
  326. package/src/workbench/use-changes-workbench.ts +205 -0
  327. package/src/workbench/use-remember-layout.ts +31 -0
  328. package/src/workbench/use-repo-changes.ts +91 -0
  329. package/src/workbench/workbench-snapshot.test.ts +99 -0
  330. package/src/workbench/workbench-snapshot.ts +59 -0
  331. package/src/App.diff.test.tsx +0 -99
  332. package/src/App.export.test.tsx +0 -80
  333. package/src/App.readonly.test.tsx +0 -148
  334. package/src/App.test.tsx +0 -217
  335. package/src/App.tsx +0 -0
  336. package/src/config.test.ts +0 -116
  337. package/src/config.ts +0 -113
  338. package/src/editor.ts +0 -34
  339. package/src/run.ts +0 -28
  340. package/src/serve.ts +0 -75
  341. package/src/theme.ts +0 -46
  342. package/src/view-diff.test.ts +0 -55
  343. package/src/view-diff.ts +0 -86
  344. package/src/view.test.ts +0 -157
  345. package/src/view.ts +0 -0
@@ -0,0 +1,60 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { DiffFileContents } from "@cueloop/schema";
3
+ import { buildFileTree } from "./file-tree";
4
+
5
+ function file(path: string, status: DiffFileContents["status"] = "modified"): DiffFileContents {
6
+ return { path, oldContents: "a", newContents: "b", status };
7
+ }
8
+
9
+ describe("buildFileTree", () => {
10
+ test("nests files under their directories and keeps the full path as the leaf id", () => {
11
+ // Arrange
12
+ const files = [file("src/app/main.ts"), file("src/app/util.ts"), file("README.md")];
13
+
14
+ // Act
15
+ const tree = buildFileTree(files);
16
+
17
+ // Assert - one src folder over one app folder over two leaves, plus a root file
18
+ expect(tree.map((node) => node.label)).toEqual(["src", "README.md"]);
19
+ const app = tree[0]!.children![0]!;
20
+
21
+ expect(app.label).toBe("app");
22
+ expect(app.children!.map((leaf) => leaf.id)).toEqual(["src/app/main.ts", "src/app/util.ts"]);
23
+ });
24
+
25
+ test("carries the git status onto each leaf so the tree can tint it", () => {
26
+ // Act
27
+ const tree = buildFileTree([file("a.ts", "added"), file("b.ts", "deleted")]);
28
+
29
+ // Assert
30
+ expect(tree.map((leaf) => leaf.status)).toEqual(["added", "deleted"]);
31
+ });
32
+
33
+ test("merges files that share a directory into one folder node", () => {
34
+ // Act
35
+ const tree = buildFileTree([file("pkg/one.ts"), file("pkg/two.ts")]);
36
+
37
+ // Assert
38
+ expect(tree).toHaveLength(1);
39
+ expect(tree[0]!.children).toHaveLength(2);
40
+ });
41
+
42
+ test("returns an empty tree for no changes", () => {
43
+ expect(buildFileTree([])).toEqual([]);
44
+ });
45
+
46
+ test("badges a leaf with its comment count, and leaves an uncommented file bare", () => {
47
+ // Act
48
+ const tree = buildFileTree(
49
+ [file("a.ts"), file("b.ts")],
50
+ new Map([
51
+ ["a.ts", 3],
52
+ ["b.ts", 0],
53
+ ]),
54
+ );
55
+
56
+ // Assert - a.ts shows the dot-and-count, b.ts (0) shows nothing
57
+ expect(tree[0]!.badge).toBe("● 3");
58
+ expect(tree[1]!.badge).toBeUndefined();
59
+ });
60
+ });
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Builds a nested file tree from a diff's changed files, so the Changes column
3
+ * can browse them by directory. Each leaf keeps its repo-relative path as the id
4
+ * (what the diff rows are keyed by) and carries the git status for its tint.
5
+ */
6
+
7
+ import type { DiffFileContents, DiffFileStatus } from "@cueloop/schema";
8
+ import type { GitStatus, TreeNode } from "../../ui/components/primitives/tree-model";
9
+
10
+ /** The diff file statuses are a subset of the tree's git statuses. */
11
+ function gitStatusOf(status: DiffFileStatus): GitStatus {
12
+ return status;
13
+ }
14
+
15
+ interface MutableTreeNode {
16
+ id: string;
17
+ label: string;
18
+ status?: GitStatus;
19
+ badge?: string;
20
+ children?: Map<string, MutableTreeNode>;
21
+ }
22
+
23
+ function freeze(node: MutableTreeNode): TreeNode {
24
+ if (node.children === undefined) {
25
+ const leaf: TreeNode = { id: node.id, label: node.label, status: node.status };
26
+
27
+ if (node.badge !== undefined) leaf.badge = node.badge;
28
+
29
+ return leaf;
30
+ }
31
+
32
+ return {
33
+ id: node.id,
34
+ label: node.label,
35
+ children: [...node.children.values()].map(freeze),
36
+ };
37
+ }
38
+
39
+ /** A file's comment count as a leaf badge, or undefined when it carries none. */
40
+ function commentBadge(
41
+ counts: ReadonlyMap<string, number> | undefined,
42
+ path: string,
43
+ ): string | undefined {
44
+ const count = counts?.get(path);
45
+
46
+ return count ? `● ${count}` : undefined;
47
+ }
48
+
49
+ /**
50
+ * Group changed files into a directory tree; leaves are keyed by their full path. A leaf whose
51
+ * file carries comments shows a dot-and-count badge, so feedback is visible even with its tab closed.
52
+ */
53
+ export function buildFileTree(
54
+ files: readonly DiffFileContents[],
55
+ commentCounts?: ReadonlyMap<string, number>,
56
+ ): TreeNode[] {
57
+ const roots = new Map<string, MutableTreeNode>();
58
+
59
+ for (const file of files) {
60
+ const segments = file.path.split("/").filter((segment) => segment.length > 0);
61
+ let level = roots;
62
+ let prefix = "";
63
+
64
+ segments.forEach((segment, index) => {
65
+ prefix = prefix.length > 0 ? `${prefix}/${segment}` : segment;
66
+ const isLeaf = index === segments.length - 1;
67
+ let node = level.get(segment);
68
+
69
+ if (node === undefined) {
70
+ node = isLeaf
71
+ ? {
72
+ id: file.path,
73
+ label: segment,
74
+ status: gitStatusOf(file.status),
75
+ badge: commentBadge(commentCounts, file.path),
76
+ }
77
+ : { id: prefix, label: segment, children: new Map() };
78
+ level.set(segment, node);
79
+ }
80
+ if (!isLeaf) level = node.children!;
81
+ });
82
+ }
83
+
84
+ return [...roots.values()].map(freeze);
85
+ }
86
+
87
+ /** Group plain repo-relative paths (git ls-files) into a directory tree; leaves keep their full path. */
88
+ export function buildPathTree(paths: readonly string[]): TreeNode[] {
89
+ const roots = new Map<string, MutableTreeNode>();
90
+
91
+ for (const path of paths) {
92
+ const segments = path.split("/").filter((segment) => segment.length > 0);
93
+ let level = roots;
94
+ let prefix = "";
95
+
96
+ segments.forEach((segment, index) => {
97
+ prefix = prefix.length > 0 ? `${prefix}/${segment}` : segment;
98
+ const isLeaf = index === segments.length - 1;
99
+ let node = level.get(segment);
100
+
101
+ if (node === undefined) {
102
+ node = isLeaf
103
+ ? { id: path, label: segment }
104
+ : { id: prefix, label: segment, children: new Map() };
105
+ level.set(segment, node);
106
+ }
107
+ if (!isLeaf) level = node.children!;
108
+ });
109
+ }
110
+
111
+ return [...roots.values()].map(freeze);
112
+ }
@@ -0,0 +1,205 @@
1
+ // The Changes/Project right-region state machine: the Project pane is the right sidebar and is always
2
+ // present when the region is on, the Changes editor rides on it and never stands alone, and the two
3
+ // toggles are mutually-exclusive file-tree modes. Owns the editor grid (tabs and splits) too.
4
+
5
+ import { useRef, useState } from "react";
6
+ import type { ProjectPanelMode } from "../app/components/AppShell";
7
+ import type { LaunchLayout } from "../app/launch-layout";
8
+ import {
9
+ activateTab,
10
+ changesTab,
11
+ closeTab,
12
+ containsGroup,
13
+ firstGroupId,
14
+ keepFileTab,
15
+ makeGroup,
16
+ openChangesTab,
17
+ openFileTab,
18
+ splitGroup,
19
+ welcomeTab,
20
+ type EditorNode,
21
+ type SplitDirection,
22
+ } from "./components/editor-grid";
23
+
24
+ /** Seed the workbench for a bare launch: the region opens on a disposable Welcome tab. */
25
+ export interface ChangesWorkbenchOptions {
26
+ seed?: "changes" | "welcome";
27
+ layout?: LaunchLayout;
28
+ }
29
+
30
+ interface InitialPanes {
31
+ projectOpen: boolean;
32
+ changesOpen: boolean;
33
+ projectMode: ProjectPanelMode;
34
+ zoomed: boolean;
35
+ }
36
+
37
+ /** The right region's opening state: from the launch layout when given, else the welcome-seed defaults. */
38
+ function initialPanes(options?: ChangesWorkbenchOptions): InitialPanes {
39
+ const layout = options?.layout;
40
+
41
+ if (layout) {
42
+ const changesOpen = layout.rightSidebar === "changes";
43
+
44
+ return {
45
+ projectOpen: layout.rightSidebar !== "off",
46
+ changesOpen,
47
+ projectMode: layout.projectMode ?? (layout.rightSidebar === "project" ? "tree" : "changes"),
48
+ // zoom fills the middle with the Changes editor, so a hand-edited layout that zooms without one
49
+ // would strand a blank center with the Thread pane hidden; only honor zoom when Changes is open
50
+ zoomed: layout.zoomChanges && changesOpen,
51
+ };
52
+ }
53
+ const welcomeSeed = options?.seed === "welcome";
54
+
55
+ return {
56
+ projectOpen: welcomeSeed,
57
+ changesOpen: welcomeSeed,
58
+ projectMode: "changes",
59
+ zoomed: false,
60
+ };
61
+ }
62
+
63
+ export interface ChangesWorkbench {
64
+ projectOpen: boolean;
65
+ changesOpen: boolean;
66
+ projectMode: ProjectPanelMode;
67
+ zoomed: boolean;
68
+ grid: EditorNode;
69
+ activeGroup: string;
70
+ toggleRight: () => void;
71
+ toggleChanges: () => void;
72
+ toggleProject: () => void;
73
+ toggleZoom: () => void;
74
+ focusGroup: (groupId: string) => void;
75
+ activate: (groupId: string, tabId: string) => void;
76
+ keepTab: (groupId: string, tabId: string) => void;
77
+ close: (groupId: string, tabId: string) => void;
78
+ split: (groupId: string, direction: SplitDirection) => void;
79
+ openFile: (path: string, fileView: "diff" | "contents", persistent?: boolean) => void;
80
+ syncSession: (sessionId: string | undefined, isDiff: boolean) => void;
81
+ }
82
+
83
+ export function useChangesWorkbench(options?: ChangesWorkbenchOptions): ChangesWorkbench {
84
+ const welcomeSeed = options?.seed === "welcome";
85
+ const initial = initialPanes(options);
86
+ const [projectOpen, setProjectOpen] = useState(initial.projectOpen);
87
+ const [changesOpen, setChangesOpen] = useState(initial.changesOpen);
88
+ const [projectMode, setProjectMode] = useState<ProjectPanelMode>(initial.projectMode);
89
+ const [zoomed, setZoomed] = useState(initial.zoomed);
90
+ const [grid, setGrid] = useState<EditorNode>(() =>
91
+ makeGroup([welcomeSeed ? welcomeTab() : changesTab()]),
92
+ );
93
+ const [focusedGroup, setFocusedGroup] = useState<string | null>(null);
94
+ const rememberedChanges = useRef(welcomeSeed);
95
+ const seenSession = useRef<string | undefined>(undefined);
96
+
97
+ const syncSession = (sessionId: string | undefined, isDiff: boolean): void => {
98
+ if (sessionId === undefined || sessionId === seenSession.current) return;
99
+ seenSession.current = sessionId;
100
+ setProjectOpen(isDiff);
101
+ setChangesOpen(isDiff);
102
+ setProjectMode(isDiff ? (options?.layout?.projectMode ?? "changes") : "tree");
103
+ rememberedChanges.current = isDiff;
104
+ // a non-diff thread has no Changes editor; leaving zoom on would strand the hidden Thread pane
105
+ if (!isDiff) setZoomed(false);
106
+ setGrid(makeGroup([changesTab()]));
107
+ setFocusedGroup(null);
108
+ };
109
+
110
+ const toggleRight = (): void => {
111
+ if (projectOpen) {
112
+ rememberedChanges.current = changesOpen;
113
+ setChangesOpen(false);
114
+ setProjectOpen(false);
115
+ // zoom focuses the Changes editor; collapsing the region returns the Thread pane full width
116
+ setZoomed(false);
117
+ } else {
118
+ setProjectOpen(true);
119
+ setChangesOpen(rememberedChanges.current);
120
+ setProjectMode(rememberedChanges.current ? "changes" : "tree");
121
+ }
122
+ };
123
+ const toggleChanges = (): void => {
124
+ setChangesOpen(true);
125
+ setProjectOpen(true);
126
+ setProjectMode("changes");
127
+ setGrid((tree) => {
128
+ const result = openChangesTab(tree, focusedGroup ?? firstGroupId(tree));
129
+
130
+ setFocusedGroup(result.focusGroupId);
131
+
132
+ return result.tree;
133
+ });
134
+ };
135
+ const toggleProject = (): void => {
136
+ setProjectOpen(true);
137
+ setProjectMode("tree");
138
+ };
139
+ const close = (groupId: string, tabId: string): void => {
140
+ setGrid((tree) => {
141
+ const pruned = closeTab(tree, groupId, tabId);
142
+
143
+ if (pruned === null) {
144
+ setChangesOpen(false);
145
+ setProjectMode("tree");
146
+ // leaving zoom on would hide the Thread pane with no editor to fill the gap
147
+ setZoomed(false);
148
+
149
+ return makeGroup([changesTab()]);
150
+ }
151
+ // re-home focus when the closed group was pruned away, so the next open has a live target
152
+ setFocusedGroup((current) =>
153
+ current !== null && containsGroup(pruned, current) ? current : firstGroupId(pruned),
154
+ );
155
+
156
+ return pruned;
157
+ });
158
+ };
159
+ const split = (groupId: string, direction: SplitDirection): void => {
160
+ setGrid((tree) => {
161
+ const result = splitGroup(tree, groupId, direction);
162
+
163
+ setFocusedGroup(result.focusGroupId);
164
+
165
+ return result.tree;
166
+ });
167
+ };
168
+ const openFile = (path: string, fileView: "diff" | "contents", persistent = false): void => {
169
+ setProjectOpen(true);
170
+ setChangesOpen(true);
171
+ setGrid((tree) => {
172
+ const result = openFileTab(
173
+ tree,
174
+ focusedGroup ?? firstGroupId(tree),
175
+ path,
176
+ fileView,
177
+ persistent,
178
+ );
179
+
180
+ setFocusedGroup(result.focusGroupId);
181
+
182
+ return result.tree;
183
+ });
184
+ };
185
+
186
+ return {
187
+ projectOpen,
188
+ changesOpen,
189
+ projectMode,
190
+ zoomed,
191
+ grid,
192
+ activeGroup: focusedGroup ?? firstGroupId(grid),
193
+ toggleRight,
194
+ toggleChanges,
195
+ toggleProject,
196
+ toggleZoom: () => setZoomed((value) => !value),
197
+ focusGroup: setFocusedGroup,
198
+ activate: (groupId, tabId) => setGrid((tree) => activateTab(tree, groupId, tabId)),
199
+ keepTab: (groupId, tabId) => setGrid((tree) => keepFileTab(tree, groupId, tabId)),
200
+ close,
201
+ split,
202
+ openFile,
203
+ syncSession,
204
+ };
205
+ }
@@ -0,0 +1,31 @@
1
+ import { useEffect, useRef } from "react";
2
+ import { persistLayout } from "../settings/config";
3
+ import { layoutFromPanes, type LaunchLayout } from "../app/launch-layout";
4
+
5
+ /**
6
+ * Persist the pane composition whenever the user changes it, so the next bare launch restores it.
7
+ * Only a launch that carried an intended layout (a create-command or the vanilla inbox) remembers;
8
+ * a bare thread open collapses the sidebar and must not clobber the remembered composition. The
9
+ * mount write is skipped: the opening composition is the one just restored.
10
+ */
11
+ export function useRememberLayout(
12
+ layout: LaunchLayout | undefined,
13
+ active: boolean,
14
+ threads: boolean,
15
+ changesOpen: boolean,
16
+ projectOpen: boolean,
17
+ zoomChanges: boolean,
18
+ ): void {
19
+ const settled = useRef(false);
20
+
21
+ useEffect(() => {
22
+ if (!settled.current) {
23
+ settled.current = true;
24
+
25
+ return;
26
+ }
27
+ // only the active workbench persists: on a bare launch App's unused panes must not clobber the shell's
28
+ if (!active || layout === undefined) return;
29
+ persistLayout(layoutFromPanes({ threads, changesOpen, projectOpen, zoomChanges }));
30
+ }, [active, layout, threads, changesOpen, projectOpen, zoomChanges]);
31
+ }
@@ -0,0 +1,91 @@
1
+ import { useEffect, useEffectEvent, useState } from "react";
2
+ import type { DiffFileContents } from "@cueloop/schema";
3
+
4
+ const CHANGES_REFRESH_MS = 2000;
5
+ const EMPTY_CHANGED_FILES: readonly DiffFileContents[] = [];
6
+
7
+ interface RepoChangesOptions {
8
+ loadChanges: () => Promise<readonly DiffFileContents[]>;
9
+ visible: boolean;
10
+ diffSourceKey: string;
11
+ initialFiles?: readonly DiffFileContents[];
12
+ refreshAutomatically?: boolean;
13
+ }
14
+
15
+ interface RepoChangesSnapshot {
16
+ diffSourceKey: string;
17
+ files: readonly DiffFileContents[];
18
+ }
19
+
20
+ function sameChangedFiles(a: readonly DiffFileContents[], b: readonly DiffFileContents[]): boolean {
21
+ return (
22
+ a.length === b.length &&
23
+ a.every(
24
+ (file, index) =>
25
+ file.path === b[index]?.path &&
26
+ file.status === b[index]?.status &&
27
+ file.oldContents === b[index]?.oldContents &&
28
+ file.newContents === b[index]?.newContents,
29
+ )
30
+ );
31
+ }
32
+
33
+ /** Refresh the visible Changes tree from the working repo; keep captured diff reviews fixed. */
34
+ export function useRepoChanges({
35
+ loadChanges,
36
+ visible,
37
+ diffSourceKey,
38
+ initialFiles,
39
+ refreshAutomatically = true,
40
+ }: RepoChangesOptions): readonly DiffFileContents[] {
41
+ const [snapshot, setSnapshot] = useState<RepoChangesSnapshot | null>(() =>
42
+ initialFiles ? { diffSourceKey, files: initialFiles } : null,
43
+ );
44
+ const loadLatestChanges = useEffectEvent(loadChanges);
45
+
46
+ useEffect(() => {
47
+ if (!visible) return;
48
+
49
+ let cancelled = false;
50
+ let requestInFlight = false;
51
+
52
+ const refreshChanges = async (): Promise<void> => {
53
+ if (requestInFlight) return;
54
+ requestInFlight = true;
55
+
56
+ try {
57
+ const files = await loadLatestChanges();
58
+
59
+ if (!cancelled) {
60
+ setSnapshot((previous) =>
61
+ previous?.diffSourceKey === diffSourceKey && sameChangedFiles(previous.files, files)
62
+ ? previous
63
+ : { diffSourceKey, files },
64
+ );
65
+ }
66
+ } catch {
67
+ if (!cancelled) {
68
+ setSnapshot((previous) =>
69
+ previous?.diffSourceKey === diffSourceKey && previous.files.length === 0
70
+ ? previous
71
+ : { diffSourceKey, files: EMPTY_CHANGED_FILES },
72
+ );
73
+ }
74
+ } finally {
75
+ requestInFlight = false;
76
+ }
77
+ };
78
+
79
+ void refreshChanges();
80
+ const timer = refreshAutomatically
81
+ ? setInterval(() => void refreshChanges(), CHANGES_REFRESH_MS)
82
+ : null;
83
+
84
+ return () => {
85
+ cancelled = true;
86
+ if (timer !== null) clearInterval(timer);
87
+ };
88
+ }, [visible, diffSourceKey, refreshAutomatically]);
89
+
90
+ return snapshot?.diffSourceKey === diffSourceKey ? snapshot.files : EMPTY_CHANGED_FILES;
91
+ }
@@ -0,0 +1,99 @@
1
+ /** Sharing/serving a workbench thread freezes its live working-tree diff into the artifact so a remote
2
+ * reviewer, who cannot see the owner's tree, reads a stable snapshot; any other thread passes through. */
3
+
4
+ import { describe, expect, mock, test } from "bun:test";
5
+ import { historyFromLinear, viewFollowing, type Thread } from "@cueloop/schema";
6
+ import { snapshotWorkbench } from "./workbench-snapshot";
7
+
8
+ const AT = "2026-01-01T00:00:00.000Z";
9
+
10
+ const STALE_FILES = [
11
+ { path: "src/a.ts", oldContents: "a\n", newContents: "b\n", status: "modified" as const },
12
+ ];
13
+ const LIVE = {
14
+ patch: "FRESH PATCH",
15
+ files: [
16
+ { path: "src/a.ts", oldContents: "a\n", newContents: "c\n", status: "modified" as const },
17
+ ],
18
+ };
19
+
20
+ function diffThread(meta: Thread["artifact"]["meta"]): Thread {
21
+ return {
22
+ schemaVersion: "1",
23
+ id: "ses_wb",
24
+ workspace: { repoRoot: "/repo", branch: "main" },
25
+ artifact: { type: "diff", content: "STALE PATCH", files: STALE_FILES, meta },
26
+ revisions: [{ revision: 1, content: "STALE PATCH", submittedAt: AT }],
27
+ annotations: [
28
+ {
29
+ id: "n1",
30
+ kind: "comment",
31
+ anchor: { quote: "b", prefix: "", suffix: "" },
32
+ body: "why?",
33
+ createdAt: AT,
34
+ },
35
+ ],
36
+ message: null,
37
+ status: "pending",
38
+ createdAt: AT,
39
+ };
40
+ }
41
+
42
+ describe("snapshotWorkbench", () => {
43
+ test("freezes a workbench thread's live diff and drops the workbench flag", async () => {
44
+ const frozen = await snapshotWorkbench(
45
+ diffThread({ workbench: true, title: "Workbench" }),
46
+ async () => LIVE,
47
+ );
48
+
49
+ // the live diff is pinned into the artifact
50
+ expect(frozen.artifact.content).toBe("FRESH PATCH");
51
+ expect(frozen.artifact.files).toEqual(LIVE.files);
52
+ // the snapshot marker pins the render; the workbench marker stays so file-targeted feedback still paints
53
+ expect(frozen.artifact.meta.snapshot).toBe(true);
54
+ expect(frozen.artifact.meta.workbench).toBe(true);
55
+ expect(frozen.artifact.meta.title).toBe("Workbench");
56
+ // the thread is otherwise intact - same id, workspace, and annotations
57
+ expect(frozen.id).toBe("ses_wb");
58
+ expect(frozen.annotations).toHaveLength(1);
59
+ });
60
+
61
+ test("recaptures history so the share path rebuilds the fresh content, not the stale head", async () => {
62
+ // a workbench thread carries branch history whose head is the creation-time capture
63
+ const base = diffThread({ workbench: true });
64
+ const withHistory: Thread = { ...base, history: historyFromLinear(base) };
65
+
66
+ const frozen = await snapshotWorkbench(withHistory, async () => LIVE);
67
+
68
+ // viewFollowing (the share path) rebuilds content from history - it must yield the fresh patch
69
+ expect(viewFollowing(frozen).artifact.content).toBe("FRESH PATCH");
70
+ });
71
+
72
+ test("pins the captured source and revision in a shared workbench", async () => {
73
+ const frozen = await snapshotWorkbench(
74
+ diffThread({ workbench: true, vcs: "jj", vcsRevisionId: "old" }),
75
+ async () => ({
76
+ ...LIVE,
77
+ vcs: "jj",
78
+ source: { changeId: "change", revisionId: "new" },
79
+ }),
80
+ );
81
+
82
+ expect(frozen.artifact.meta.vcsRevisionId).toBe("new");
83
+ expect(frozen.revisions[0]).toMatchObject({
84
+ content: LIVE.patch,
85
+ files: LIVE.files,
86
+ source: { vcs: "jj", changeId: "change", revisionId: "new" },
87
+ });
88
+ });
89
+
90
+ test("passes a plain diff review through untouched and never queries the tree", async () => {
91
+ const session = diffThread({ title: "PR #7" });
92
+ const repoDiff = mock(async () => LIVE);
93
+
94
+ const result = await snapshotWorkbench(session, repoDiff);
95
+
96
+ expect(result).toBe(session);
97
+ expect(repoDiff).not.toHaveBeenCalled();
98
+ });
99
+ });
@@ -0,0 +1,59 @@
1
+ import {
2
+ recaptureMainHead,
3
+ type DiffFileContents,
4
+ type DiffSource,
5
+ type Thread,
6
+ } from "@cueloop/schema";
7
+
8
+ /** Reads the live working-tree diff for a repo root (the daemon's `repo.diff` RPC). */
9
+ export type RepoDiff = (
10
+ repoRoot: string,
11
+ vcs?: string,
12
+ ) => Promise<{
13
+ patch: string;
14
+ files: DiffFileContents[];
15
+ source?: Omit<DiffSource, "vcs">;
16
+ vcs?: string;
17
+ }>;
18
+
19
+ /**
20
+ * Freeze a workbench thread's live working-tree diff into its artifact for a remote reviewer, who cannot
21
+ * see the owner's tree. The `snapshot` marker pins the render to this captured diff while keeping the
22
+ * workbench marker, so the reviewer still sees the file-targeted feedback already on the thread. Any other
23
+ * thread is already stable and passes through; the owner's own session keeps the original live thread.
24
+ */
25
+ export async function snapshotWorkbench(session: Thread, repoDiff: RepoDiff): Promise<Thread> {
26
+ if (session.artifact.type !== "diff" || session.artifact.meta.workbench !== true) return session;
27
+
28
+ const { patch, files, source, vcs } = await repoDiff(
29
+ session.workspace.repoRoot,
30
+ session.artifact.meta.vcs,
31
+ );
32
+ const provider = vcs ?? session.artifact.meta.vcs;
33
+ const artifact = {
34
+ ...session.artifact,
35
+ content: patch,
36
+ files,
37
+ meta: {
38
+ ...session.artifact.meta,
39
+ snapshot: true,
40
+ vcs: provider,
41
+ vcsChangeId: source?.changeId,
42
+ vcsRevisionId: source?.revisionId,
43
+ },
44
+ };
45
+ // the share path rebuilds content from history; recapture the branch head so it matches the fresh patch
46
+ const history = session.history ? recaptureMainHead(session.history, patch) : session.history;
47
+ const revisions = session.revisions.map((revision, index) =>
48
+ index === session.revisions.length - 1
49
+ ? {
50
+ ...revision,
51
+ content: patch,
52
+ files,
53
+ source: provider && source ? { vcs: provider, ...source } : undefined,
54
+ }
55
+ : revision,
56
+ );
57
+
58
+ return { ...session, artifact, history, revisions };
59
+ }