@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,53 @@
1
+ /** The App renders in a light virtual terminal: passing appearance="light" threads the light branded theme through the provider without breaking the mount (the char frame is color-blind, so this covers propagation and wiring, not the pixel colors). */
2
+
3
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
4
+ import { mkdtempSync, rmSync } from "node:fs";
5
+ import { tmpdir } from "node:os";
6
+ import { join } from "node:path";
7
+ import React from "react";
8
+ import { DaemonServer } from "@cueloop/daemon";
9
+ import type { Thread } from "@cueloop/schema";
10
+ import { App } from "./App";
11
+ import { isolateUserConfig, settle, waitForText, renderReadyApp } from "../testing/test-support";
12
+
13
+ const PLAN = "# Sync Plan\n\n## Steps\n\n- flush the queue\n";
14
+
15
+ let home: string;
16
+ let server: DaemonServer;
17
+ let session: Thread;
18
+ let restoreUserConfig: () => void;
19
+
20
+ beforeEach(() => {
21
+ home = mkdtempSync(join(tmpdir(), "cueloop-appearance-"));
22
+ restoreUserConfig = isolateUserConfig(home);
23
+ server = new DaemonServer({ home, idleExitMs: 0 });
24
+ server.start();
25
+ session = server.core.sessionCreate({
26
+ workspace: { repoRoot: "/repo", branch: "main" },
27
+ artifact: { type: "plan", content: PLAN, meta: { title: "Sync Plan", planPath: "plan.md" } },
28
+ });
29
+ });
30
+ afterEach(() => {
31
+ restoreUserConfig();
32
+ server.stop();
33
+ rmSync(home, { recursive: true, force: true });
34
+ });
35
+
36
+ describe("appearance", () => {
37
+ test("renders the review on a light terminal", async () => {
38
+ // Arrange + Act
39
+ const setup = await renderReadyApp(
40
+ <App home={home} sessionId={session.id} appearance="light" />,
41
+ {
42
+ width: 120,
43
+ height: 32,
44
+ },
45
+ );
46
+
47
+ // Assert
48
+ await waitForText(setup, "cueloop");
49
+ await waitForText(setup, "Sync Plan");
50
+ await settle(setup);
51
+ expect(setup.captureCharFrame()).toContain("flush the queue");
52
+ });
53
+ });
@@ -0,0 +1,144 @@
1
+ /** Post-submit completion overlay: prompt, opt-in persistence, countdown, dismissal. Countdown seconds tick on an injected ManualClock, so tests advance time instead of waiting it out. */
2
+
3
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
4
+ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
5
+ import { tmpdir } from "node:os";
6
+ import { join } from "node:path";
7
+ import React from "react";
8
+ import { testRender } from "@opentui/react/test-utils";
9
+ import { ManualClock } from "@opentui/core/testing";
10
+ import { DaemonServer } from "@cueloop/daemon";
11
+ import type { Thread } from "@cueloop/schema";
12
+ import { App } from "./App";
13
+ import { isolateUserConfig, press, settle, waitForText, pressKey } from "../testing/test-support";
14
+
15
+ const PLAN = "# Plan\n\nShip the thing.\n";
16
+
17
+ let home: string;
18
+ let server: DaemonServer;
19
+ let session: Thread;
20
+ let configPath: string;
21
+ let restoreUserConfig: () => void;
22
+ let clock: ManualClock;
23
+
24
+ beforeEach(() => {
25
+ home = mkdtempSync(join(tmpdir(), "cueloop-close-"));
26
+ configPath = join(home, "config.toml");
27
+ restoreUserConfig = isolateUserConfig(home, "config.toml");
28
+ server = new DaemonServer({ home, idleExitMs: 0 });
29
+ server.start();
30
+ session = server.core.sessionCreate({
31
+ workspace: { repoRoot: "/repo", branch: "main" },
32
+ artifact: { type: "plan", content: PLAN, meta: { title: "Plan" } },
33
+ });
34
+ clock = new ManualClock();
35
+ });
36
+ afterEach(() => {
37
+ restoreUserConfig();
38
+ server.stop();
39
+ rmSync(home, { recursive: true, force: true });
40
+ });
41
+
42
+ async function renderApp(onExit?: (code: number) => void) {
43
+ const setup = await testRender(
44
+ <App home={home} sessionId={session.id} onExit={onExit} clock={clock} />,
45
+ {
46
+ width: 120,
47
+ height: 30,
48
+ },
49
+ );
50
+
51
+ await waitForText(setup, "cueloop");
52
+
53
+ return setup;
54
+ }
55
+
56
+ type Setup = Awaited<ReturnType<typeof renderApp>>;
57
+
58
+ async function submitApprove(setup: Setup): Promise<void> {
59
+ await pressKey(setup, "RETURN", { meta: true }); // open submit (approve default: no pending items)
60
+ await pressKey(setup, "RETURN", { meta: true }); // confirm
61
+ await waitForText(setup, "review approved");
62
+ }
63
+
64
+ describe("completion overlay", () => {
65
+ test("submit with auto-close off counts down from 5; ⏎ exits now", async () => {
66
+ // Arrange
67
+ let exited = -1;
68
+ const setup = await renderApp((code) => (exited = code));
69
+
70
+ // Act
71
+ await submitApprove(setup);
72
+
73
+ // Assert
74
+ const frame = await waitForText(setup, "closing in 5s");
75
+
76
+ expect(frame).toContain("review approved");
77
+ expect(frame).toContain("close");
78
+ expect(frame).toContain("back to plan");
79
+
80
+ // Act
81
+ await press(setup, "enter");
82
+
83
+ // Assert
84
+ expect(exited).toBe(0);
85
+ });
86
+
87
+ test("a remembers the countdown as the persisted default", async () => {
88
+ // Arrange
89
+ const setup = await renderApp();
90
+
91
+ await submitApprove(setup);
92
+ await waitForText(setup, "closing in 5s");
93
+
94
+ // Act
95
+ await press(setup, "a");
96
+
97
+ // Assert
98
+ expect(existsSync(configPath)).toBe(true);
99
+ expect(readFileSync(configPath, "utf8")).toContain("auto_close = 5");
100
+
101
+ // Act
102
+ // the countdown keeps running; each second is one manual-clock tick
103
+ clock.advance(1000);
104
+
105
+ // Assert
106
+ await waitForText(setup, "closing in 4s");
107
+ });
108
+
109
+ test("esc dismisses to the resolved read-only view", async () => {
110
+ // Arrange
111
+ const setup = await renderApp();
112
+
113
+ await submitApprove(setup);
114
+ await waitForText(setup, "closing in 5s");
115
+
116
+ // Act
117
+ // a bare ESC sits in the input parser's escape-sequence disambiguation
118
+ // window before it is delivered - the frame wait absorbs it
119
+ await press(setup, "escape");
120
+
121
+ // Assert - the countdown is cancelled and we are back in the (resolved) thread view
122
+ const frame = await waitForText(setup, "Ship the thing");
123
+
124
+ expect(frame).not.toContain("closing in");
125
+ });
126
+
127
+ test("configured auto_close counts down and exits without interaction", async () => {
128
+ // Arrange
129
+ writeFileSync(configPath, "[ui]\nauto_close = 1\n");
130
+ let exited = -1;
131
+ const setup = await renderApp((code) => (exited = code));
132
+
133
+ await submitApprove(setup);
134
+ await waitForText(setup, "closing in 1s");
135
+ expect(exited).toBe(-1);
136
+
137
+ // Act
138
+ clock.advance(1000);
139
+ await settle(setup);
140
+
141
+ // Assert
142
+ expect(exited).toBe(0);
143
+ });
144
+ });
@@ -0,0 +1,187 @@
1
+ /** Keyboard scrolling in a changed-file tab uses caret navigation, even when the file was deleted. */
2
+
3
+ import { expect, test } from "bun:test";
4
+ import { mkdtempSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
5
+ import { tmpdir } from "node:os";
6
+ import { join } from "node:path";
7
+ import React from "react";
8
+ import { ScrollBoxRenderable, type Renderable } from "@opentui/core";
9
+ import { DaemonServer } from "@cueloop/daemon";
10
+ import { App } from "./App";
11
+ import { annotationPaletteFor } from "../annotations/annotation-palette";
12
+ import { DARK } from "../appearance/theme";
13
+ import {
14
+ isolateUserConfig,
15
+ locateText,
16
+ press,
17
+ renderReadyApp,
18
+ settle,
19
+ waitForText,
20
+ } from "../testing/test-support";
21
+
22
+ function findScrollbox(node: Renderable): ScrollBoxRenderable | null {
23
+ if (node.id === "diff-scroll" && node instanceof ScrollBoxRenderable) return node;
24
+
25
+ for (const child of node.getChildren()) {
26
+ const found = findScrollbox(child);
27
+
28
+ if (found) return found;
29
+ }
30
+
31
+ return null;
32
+ }
33
+
34
+ function hex(color: { toInts(): [number, number, number, number] }): string {
35
+ const [red, green, blue] = color.toInts();
36
+
37
+ return "#" + [red, green, blue].map((part) => part.toString(16).padStart(2, "0")).join("");
38
+ }
39
+
40
+ test("deleted file caret scrolls one row and stays at the file boundaries", async () => {
41
+ const home = mkdtempSync(join(tmpdir(), "cueloop-scroll-home-"));
42
+ const repo = mkdtempSync(join(tmpdir(), "cueloop-scroll-repo-"));
43
+ const restoreConfig = isolateUserConfig(home);
44
+ const server = new DaemonServer({ home, idleExitMs: 0 });
45
+ let setup: Awaited<ReturnType<typeof renderReadyApp>> | undefined;
46
+
47
+ try {
48
+ const file = join(repo, "long.md");
49
+ const lines = Array.from({ length: 180 }, (_, index) =>
50
+ index === 113
51
+ ? "[nodejs/node#36005](https://github.com/nodejs/node/issues/36005)."
52
+ : `row ${String(index).padStart(3, "0")} ${"a paragraph with wrapped text and a link [source](https://example.test/page) ".repeat((index % 3) + 1)}`,
53
+ );
54
+ const git = (args: string[]) => Bun.spawnSync(["git", "-C", repo, ...args]);
55
+
56
+ writeFileSync(file, lines.join("\n") + "\n");
57
+ git(["init", "-q"]);
58
+ git(["config", "user.email", "fixture@example.com"]);
59
+ git(["config", "user.name", "Fixture"]);
60
+ git(["add", "-A"]);
61
+ git(["commit", "-q", "-m", "seed"]);
62
+ unlinkSync(file);
63
+ server.start();
64
+ setup = await renderReadyApp(<App home={home} sessionId={undefined} cwd={repo} />, {
65
+ width: 180,
66
+ height: 48,
67
+ });
68
+ await waitForText(setup, "long.md");
69
+ const treeFile = locateText(setup, "long.md");
70
+
71
+ await setup.mockMouse.click(treeFile.column, treeFile.row);
72
+ await waitForText(setup, "row 00");
73
+ const box = findScrollbox(setup.renderer.root);
74
+
75
+ if (!box) throw new Error("file diff scrollbox missing");
76
+ for (let offset = 0; offset < 1000; offset += 20) {
77
+ box.scrollTo({ x: 0, y: offset });
78
+ await settle(setup);
79
+ if (setup.captureCharFrame().includes("row 130")) break;
80
+ }
81
+ const target = locateText(setup, "row 130");
82
+
83
+ await setup.mockMouse.click(target.column, target.row);
84
+ await press(setup, "escape");
85
+
86
+ const steps: number[] = [];
87
+ const caretCell = annotationPaletteFor(DARK).caretCell;
88
+ const frameTrace: Array<{ markers: number[]; caret: number }> = [];
89
+ const captureFrame = () => {
90
+ const lines = setup!.captureCharFrame().split("\n");
91
+ const markers = lines.flatMap((line, row) => (line.includes("▎") ? [row] : []));
92
+ const caret = setup!
93
+ .captureSpans()
94
+ .lines.findIndex((line) => line.spans.some((span) => hex(span.bg) === caretCell));
95
+
96
+ frameTrace.push({ markers, caret });
97
+ };
98
+
99
+ setup.renderer.on("frame", captureFrame);
100
+
101
+ for (let index = 0; index < 12; index++) {
102
+ const before = box.scrollTop;
103
+
104
+ await press(setup, "down");
105
+ steps.push(box.scrollTop - before);
106
+ const caretRow = setup
107
+ .captureSpans()
108
+ .lines.findIndex((line) => line.spans.some((span) => hex(span.bg) === caretCell));
109
+ const markerRows = setup
110
+ .captureCharFrame()
111
+ .split("\n")
112
+ .flatMap((line, row) => (line.includes("▎") ? [row] : []));
113
+
114
+ expect(caretRow).toBeGreaterThanOrEqual(0);
115
+ expect(markerRows).toEqual([caretRow]);
116
+ }
117
+
118
+ expect(Math.max(...steps)).toBeLessThanOrEqual(1);
119
+ expect(Math.min(...steps)).toBeGreaterThanOrEqual(0);
120
+ let anchored = false;
121
+
122
+ for (let index = 0; index < 80; index++) {
123
+ await press(setup, "up");
124
+ const marker = setup
125
+ .captureCharFrame()
126
+ .split("\n")
127
+ .findIndex((line) => line.includes("▎"));
128
+
129
+ if (marker === box.viewport.screenY) anchored = true;
130
+ if (anchored && box.scrollTop > 0) expect(marker).toBe(box.viewport.screenY);
131
+ }
132
+ expect(anchored).toBe(true);
133
+ box.scrollTo({ x: 0, y: 100000 });
134
+ await settle(setup);
135
+ const last = locateText(setup, "row 179");
136
+
137
+ await setup.mockMouse.click(last.column, last.row);
138
+ await press(setup, "escape");
139
+ let lastMarker = -1;
140
+
141
+ for (let index = 0; index < 12; index++) {
142
+ const traceStart = frameTrace.length;
143
+ const before = box.scrollTop;
144
+
145
+ await press(setup, "down");
146
+ const states = frameTrace.slice(traceStart);
147
+
148
+ expect(box.scrollTop).toBe(before);
149
+ expect(states.length).toBeGreaterThan(0);
150
+ for (const state of states) {
151
+ expect(state.markers).toEqual([state.caret]);
152
+ expect(state.caret).toBeGreaterThanOrEqual(lastMarker);
153
+ lastMarker = state.caret;
154
+ }
155
+ }
156
+ box.scrollTo({ x: 0, y: 0 });
157
+ await settle(setup);
158
+ const first = locateText(setup, "row 000");
159
+
160
+ await setup.mockMouse.click(first.column, first.row);
161
+ await press(setup, "escape");
162
+ let firstMarker = Number.POSITIVE_INFINITY;
163
+
164
+ for (let index = 0; index < 12; index++) {
165
+ const traceStart = frameTrace.length;
166
+ const before = box.scrollTop;
167
+
168
+ await press(setup, "up");
169
+ const states = frameTrace.slice(traceStart);
170
+
171
+ expect(box.scrollTop).toBe(before);
172
+ expect(states.length).toBeGreaterThan(0);
173
+ for (const state of states) {
174
+ expect(state.markers).toEqual([state.caret]);
175
+ expect(state.caret).toBeLessThanOrEqual(firstMarker);
176
+ firstMarker = state.caret;
177
+ }
178
+ }
179
+ setup.renderer.off("frame", captureFrame);
180
+ } finally {
181
+ setup?.renderer.destroy();
182
+ server.stop();
183
+ restoreConfig();
184
+ rmSync(home, { recursive: true, force: true });
185
+ rmSync(repo, { recursive: true, force: true });
186
+ }
187
+ }, 30000);
@@ -0,0 +1,313 @@
1
+ /** Diff-review flow in the virtual terminal: the diff sheet annotates like the thread view. */
2
+
3
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
4
+ import { mkdtempSync, rmSync } from "node:fs";
5
+ import { tmpdir } from "node:os";
6
+ import { join } from "node:path";
7
+ import React from "react";
8
+ import { DaemonServer } from "@cueloop/daemon";
9
+ import type { Thread } from "@cueloop/schema";
10
+ import { App } from "./App";
11
+ import { pullRequestReviewLayout } from "./launch-layout";
12
+ import { DARK } from "../appearance/theme";
13
+ import {
14
+ clickText,
15
+ dragText,
16
+ isolateUserConfig,
17
+ navCommand,
18
+ press,
19
+ pressKey,
20
+ renderReadyApp,
21
+ typeText,
22
+ waitForState,
23
+ waitForText,
24
+ } from "../testing/test-support";
25
+
26
+ const PATCH = `diff --git a/src/store.ts b/src/store.ts
27
+ index 111..222 100644
28
+ --- a/src/store.ts
29
+ +++ b/src/store.ts
30
+ @@ -1,4 +1,4 @@
31
+ export class Store {
32
+ - private items = [];
33
+ + private items = new Map();
34
+ }
35
+ `;
36
+
37
+ let home: string;
38
+ let restoreUserConfig: () => void;
39
+ let server: DaemonServer;
40
+ let session: Thread;
41
+
42
+ beforeEach(() => {
43
+ home = mkdtempSync(join(tmpdir(), "cueloop-diff-"));
44
+ restoreUserConfig = isolateUserConfig(home);
45
+ server = new DaemonServer({ home, idleExitMs: 0 });
46
+ server.start();
47
+ session = server.core.sessionCreate({
48
+ workspace: { repoRoot: "/repo", branch: "main" },
49
+ artifact: { type: "diff", content: PATCH, meta: { title: "working tree" } },
50
+ });
51
+ });
52
+ afterEach(() => {
53
+ restoreUserConfig();
54
+ server.stop();
55
+ rmSync(home, { recursive: true, force: true });
56
+ });
57
+
58
+ async function renderApp(sessionId = session.id) {
59
+ const setup = await renderReadyApp(<App home={home} sessionId={sessionId} />, {
60
+ width: 120,
61
+ height: 30,
62
+ });
63
+
64
+ await waitForText(setup, "new Map()");
65
+
66
+ return setup;
67
+ }
68
+
69
+ describe("diff review", () => {
70
+ test("a submitted JJ review uses the existing refresh action and shows its source", async () => {
71
+ const jjReview = server.core.sessionCreate({
72
+ workspace: { repoRoot: "/repo", branch: "jj" },
73
+ artifact: {
74
+ type: "diff",
75
+ content: PATCH,
76
+ meta: { title: "JJ change", vcs: "jj", vcsChangeId: "abcdefghijklmnopqrstuvwxyzaaaaaa" },
77
+ },
78
+ });
79
+ const setup = await renderReadyApp(<App home={home} sessionId={jjReview.id} />, {
80
+ width: 120,
81
+ height: 30,
82
+ });
83
+
84
+ await waitForText(setup, "new Map()");
85
+ expect(setup.captureCharFrame()).toContain("vcs: jj");
86
+ expect(setup.captureCharFrame()).toContain("refresh");
87
+ });
88
+
89
+ test("pull request review shows its brief beside Changes and signals a moved head", async () => {
90
+ const pullRequest = server.core.sessionCreate({
91
+ workspace: { repoRoot: "/repo", branch: "detached" },
92
+ artifact: {
93
+ type: "diff",
94
+ content: PATCH,
95
+ meta: {
96
+ title: "Fix store",
97
+ pr: "org/repo#42",
98
+ prBrief: "# Fix store\n\n## PR description\n\nUse a map.",
99
+ prHeadSha: "head-1",
100
+ prRefreshHeadSha: "head-2",
101
+ },
102
+ },
103
+ });
104
+ const setup = await renderReadyApp(
105
+ <App home={home} sessionId={pullRequest.id} layout={pullRequestReviewLayout()} />,
106
+ { width: 160, height: 30 },
107
+ );
108
+
109
+ await waitForText(setup, "Use a map.");
110
+ await waitForText(setup, "new Map()");
111
+ await waitForText(setup, "refresh");
112
+ const refreshSpan = setup
113
+ .captureSpans()
114
+ .lines.flatMap((line) => line.spans)
115
+ .find((span) => span.text.includes("refresh"));
116
+ const refreshColor = refreshSpan?.fg
117
+ .toInts()
118
+ .slice(0, 3)
119
+ .map((part) => part.toString(16).padStart(2, "0"))
120
+ .join("");
121
+
122
+ expect(`#${refreshColor}`).toBe(DARK.warning);
123
+ });
124
+
125
+ test("a resolved pull request review does not offer refresh", async () => {
126
+ const pullRequest = server.core.sessionCreate({
127
+ workspace: { repoRoot: "/repo", branch: "detached" },
128
+ artifact: {
129
+ type: "diff",
130
+ content: PATCH,
131
+ meta: {
132
+ title: "Fix store",
133
+ pr: "org/repo#42",
134
+ prHeadSha: "head-1",
135
+ prRefreshHeadSha: "head-2",
136
+ },
137
+ },
138
+ });
139
+
140
+ server.core.sessionSendMessage(pullRequest.id, "approved", "Ready to merge.");
141
+ const setup = await renderReadyApp(
142
+ <App home={home} sessionId={pullRequest.id} layout={pullRequestReviewLayout()} />,
143
+ { width: 160, height: 30 },
144
+ );
145
+
146
+ await waitForText(setup, "new Map()");
147
+ expect(setup.captureCharFrame()).not.toContain("refresh");
148
+ });
149
+
150
+ test("renders file header, hunks, and signed lines", async () => {
151
+ // Arrange
152
+ const setup = await renderApp();
153
+
154
+ // Assert
155
+ const frame = setup.captureCharFrame();
156
+
157
+ expect(frame).toContain("src/store.ts");
158
+ expect(frame).toContain("@@ -1,4 +1,4 @@");
159
+ expect(frame).toMatch(/- {2,}private items = \[\];/);
160
+ expect(frame).toMatch(/\+ {2,}private items = new Map\(\);/);
161
+ });
162
+
163
+ test("a drag marks the code, typing comments on it, and the comment lands in feedback", async () => {
164
+ // Arrange
165
+ const setup = await renderApp();
166
+
167
+ // Act - mark "new Map()" on the added line and just start typing, as in the thread view
168
+ await dragText(setup, "new Map()", "new Map()", "new Map()".length);
169
+ await typeText(setup, "Map needs an eviction story.");
170
+ await waitForText(setup, "● Map needs an eviction story.");
171
+ await pressKey(setup, "RETURN", { meta: true });
172
+
173
+ // Assert - the discussion card hangs under the line; the anchor is the marked words
174
+ await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
175
+ await waitForText(setup, "● Map needs an eviction story.");
176
+ expect(server.core.sessionGet(session.id).annotations[0]!.anchor.quote).toBe("new Map()");
177
+
178
+ // Act - submit with the session chord (cmd+enter, no composer open), cycle to request changes
179
+ await pressKey(setup, "RETURN", { meta: true });
180
+ await waitForText(setup, "[approve]");
181
+ await press(setup, "right");
182
+ await waitForText(setup, "[changes]");
183
+ await pressKey(setup, "RETURN", { meta: true });
184
+
185
+ // Assert
186
+ await waitForText(setup, "feedback sent");
187
+ const resolved = server.core.sessionGet(session.id);
188
+
189
+ expect(resolved.message!.body).toContain("new Map()");
190
+ expect(resolved.message!.body).toContain("Map needs an eviction story.");
191
+ });
192
+
193
+ test("pasting an image into a diff comment drops in an [Image #n] placeholder", async () => {
194
+ // Arrange - mark the added line and open the inline comment composer
195
+ const setup = await renderApp();
196
+
197
+ await dragText(setup, "new Map()", "new Map()", "new Map()".length);
198
+ await typeText(setup, "see ");
199
+
200
+ // Act - a burst of control bytes stands in for the binary an image paste delivers
201
+ await setup.mockInput.pasteBracketedText("");
202
+
203
+ // Assert - the same placeholder the summary composer shows
204
+ await waitForText(setup, "see [Image #1]");
205
+ });
206
+
207
+ test("a comment reopens with its mark painted on the code", async () => {
208
+ // Arrange - a stored comment anchored to the added line's words
209
+ server.core.sessionAnnotate(session.id, {
210
+ id: "a_existing",
211
+ kind: "comment",
212
+ anchor: { quote: "new Map()", prefix: " private items = ", suffix: ";" },
213
+ body: "Existing note.",
214
+ });
215
+
216
+ // Act
217
+ const setup = await renderApp();
218
+
219
+ // Assert
220
+ await waitForText(setup, "● Existing note.");
221
+ });
222
+
223
+ test("option+c folds the caret's file to its band; its title restores the body", async () => {
224
+ // Arrange - the caret opens on the file's first code line
225
+ const setup = await renderApp();
226
+
227
+ // Act
228
+ await navCommand(setup, "c");
229
+
230
+ // Assert - the body is gone but the band (with its counts) remains
231
+ await waitForState(setup, () => !setup.captureCharFrame().includes("new Map()"));
232
+ const collapsed = setup.captureCharFrame();
233
+
234
+ expect(collapsed).toContain("src/store.ts");
235
+ expect(collapsed).toContain("+1");
236
+
237
+ // Act - the title unfolds it again
238
+ await clickText(setup, "src/store.ts");
239
+
240
+ // Assert
241
+ await waitForText(setup, "new Map()");
242
+ });
243
+
244
+ test("curation needs full file contents; a legacy diff answers", async () => {
245
+ // Arrange - the session carries no artifact.files
246
+ const setup = await renderApp();
247
+
248
+ // Act
249
+ await navCommand(setup, "x");
250
+
251
+ // Assert
252
+ await waitForText(setup, "hunk curation needs full file contents");
253
+ });
254
+
255
+ test("rejecting the only change empties the curated working copy", async () => {
256
+ // Arrange - a diff session that carries full file contents
257
+ const withFiles = server.core.sessionCreate({
258
+ workspace: { repoRoot: "/repo", branch: "main" },
259
+ artifact: {
260
+ type: "diff",
261
+ content: PATCH,
262
+ meta: { title: "working tree" },
263
+ files: [
264
+ {
265
+ path: "src/store.ts",
266
+ oldContents: "export class Store {\n private items = [];\n}\n",
267
+ newContents: "export class Store {\n private items = new Map();\n}\n",
268
+ status: "modified",
269
+ },
270
+ ],
271
+ },
272
+ });
273
+ const setup = await renderApp(withFiles.id);
274
+
275
+ // Act - place the caret on the added line and reject its change
276
+ await clickText(setup, "new Map()");
277
+ await navCommand(setup, "x");
278
+
279
+ // Assert - the single change is gone, so the curated working copy is empty
280
+ await waitForState(setup, () => server.core.sessionGet(withFiles.id).workingCopy === "");
281
+ const stored = server.core.sessionGet(withFiles.id);
282
+
283
+ expect(stored.workingCopy).toBe("");
284
+ expect(setup.captureCharFrame()).not.toContain("change rejected");
285
+ });
286
+
287
+ test("a diff opens with the Changes column listing every changed file", async () => {
288
+ // Arrange - two changed files; config.ts is not in the patch text, so it can
289
+ // only appear here by way of the Changes column reading artifact.files
290
+ const withFiles = server.core.sessionCreate({
291
+ workspace: { repoRoot: "/repo", branch: "main" },
292
+ artifact: {
293
+ type: "diff",
294
+ content: PATCH,
295
+ meta: { title: "working tree" },
296
+ files: [
297
+ { path: "src/store.ts", oldContents: "a", newContents: "b", status: "modified" },
298
+ { path: "src/config.ts", oldContents: "", newContents: "c", status: "added" },
299
+ ],
300
+ },
301
+ });
302
+ const setup = await renderReadyApp(<App home={home} sessionId={withFiles.id} />, {
303
+ width: 120,
304
+ height: 30,
305
+ });
306
+
307
+ await waitForText(setup, "cueloop");
308
+
309
+ // Assert - the context default opens the column for a diff, so both files show
310
+ await waitForText(setup, "config.ts");
311
+ expect(setup.captureCharFrame()).toContain("store.ts");
312
+ });
313
+ });