@cueloop/client 0.1.0-alpha.10 → 0.1.0-alpha.101

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 (356) 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 +42 -34
  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 +369 -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 +1243 -0
  18. package/src/app/App.appearance.test.tsx +53 -0
  19. package/src/{App.autoclose.test.tsx → app/App.autoclose.test.tsx} +77 -51
  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 +20 -0
  36. package/src/app/App.toast.test.tsx +124 -0
  37. package/src/app/App.tsx +1667 -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.agent.test.tsx +50 -0
  119. package/src/integrations/components/ShareDialog.stories.tsx +121 -0
  120. package/src/integrations/components/ShareDialog.tsx +574 -0
  121. package/src/integrations/components/share-dialog-store.ts +59 -0
  122. package/src/integrations/editor.test.ts +149 -0
  123. package/src/integrations/editor.ts +160 -0
  124. package/src/integrations/github-identity.test.ts +58 -0
  125. package/src/integrations/github-identity.ts +41 -0
  126. package/src/integrations/herdr.test.ts +43 -0
  127. package/src/integrations/herdr.ts +38 -0
  128. package/src/integrations/integrations.test.ts +41 -0
  129. package/src/integrations/integrations.ts +32 -0
  130. package/src/integrations/pasted-image.test.ts +67 -0
  131. package/src/integrations/pasted-image.ts +60 -0
  132. package/src/integrations/share.ts +238 -0
  133. package/src/keyboard/components/CompletionOverlay.stories.tsx +37 -0
  134. package/src/keyboard/components/CompletionOverlay.tsx +75 -0
  135. package/src/keyboard/components/HandleChipsInput.stories.tsx +24 -0
  136. package/src/keyboard/components/HandleChipsInput.test.tsx +35 -0
  137. package/src/keyboard/components/HandleChipsInput.tsx +143 -0
  138. package/src/keyboard/components/KeybindsDialog.stories.tsx +31 -0
  139. package/src/keyboard/components/KeybindsDialog.tsx +103 -0
  140. package/src/keyboard/components/NavModeHint.stories.tsx +40 -0
  141. package/src/keyboard/components/NavModeHint.tsx +31 -0
  142. package/src/keyboard/components/SlashComposer.stories.tsx +37 -0
  143. package/src/keyboard/components/SlashComposer.tsx +87 -0
  144. package/src/keyboard/key-bindings.test.ts +156 -0
  145. package/src/keyboard/key-bindings.ts +459 -0
  146. package/src/keyboard/keymap.test.ts +611 -0
  147. package/src/keyboard/keymap.ts +395 -0
  148. package/src/keyboard/skills.test.ts +74 -0
  149. package/src/keyboard/skills.ts +75 -0
  150. package/src/keyboard/slash-palette.test.ts +103 -0
  151. package/src/keyboard/slash-palette.ts +129 -0
  152. package/src/keyboard/thread-chords.test.ts +79 -0
  153. package/src/keyboard/thread-chords.ts +163 -0
  154. package/src/keyboard/use-shared-keyboard.ts +55 -0
  155. package/src/markdown/__snapshots__/view-plan.regression.test.ts.snap +100 -0
  156. package/src/markdown/components/MarkdownGridBlock.stories.tsx +33 -0
  157. package/src/markdown/components/MarkdownGridBlock.tsx +72 -0
  158. package/src/markdown/components/MarkdownThreadEditor.stories.tsx +15 -0
  159. package/src/markdown/components/MarkdownThreadEditor.test.tsx +98 -0
  160. package/src/markdown/components/MarkdownThreadEditor.tsx +160 -0
  161. package/src/markdown/components/ThreadView.selection.test.tsx +951 -0
  162. package/src/markdown/components/ThreadView.slash.test.tsx +146 -0
  163. package/src/markdown/components/ThreadView.stories.tsx +26 -0
  164. package/src/markdown/components/ThreadView.tsx +560 -0
  165. package/src/markdown/mark-runs.ts +121 -0
  166. package/src/markdown/markdown-editor-syntax-style.ts +50 -0
  167. package/src/markdown/markdown-grid.test.ts +101 -0
  168. package/src/markdown/markdown-grid.ts +203 -0
  169. package/src/markdown/markdown-highlight.test.ts +61 -0
  170. package/src/markdown/markdown-highlight.ts +124 -0
  171. package/src/markdown/thread-code-syntax.ts +45 -0
  172. package/src/markdown/use-thread-body-editing.tsx +54 -0
  173. package/src/markdown/view-plan.regression.test.ts +60 -0
  174. package/src/markdown/view-plan.test.ts +545 -0
  175. package/src/markdown/view-plan.ts +1009 -0
  176. package/src/performance/otlp-trace.test.ts +119 -0
  177. package/src/performance/otlp-trace.ts +134 -0
  178. package/src/performance/perf-report.ts +18 -0
  179. package/src/performance/perf-timings.test.ts +18 -0
  180. package/src/performance/perf-timings.ts +39 -0
  181. package/src/prototype/components/PrototypeContentView.stories.tsx +19 -0
  182. package/src/prototype/components/PrototypeContentView.tsx +442 -0
  183. package/src/prototype/prototype-browser.test.ts +81 -0
  184. package/src/prototype/prototype-browser.ts +321 -0
  185. package/src/prototype/prototype-pixels.tsx +35 -0
  186. package/src/settings/components/SettingsDialog.stories.tsx +117 -0
  187. package/src/settings/components/SettingsDialog.tsx +183 -0
  188. package/src/settings/components/SettingsRows.stories.tsx +18 -0
  189. package/src/settings/components/SettingsRows.tsx +112 -0
  190. package/src/settings/components/quick-actions-editor.stories.tsx +58 -0
  191. package/src/settings/components/quick-actions-editor.test.tsx +120 -0
  192. package/src/settings/components/quick-actions-editor.tsx +222 -0
  193. package/src/settings/config-identity.test.ts +78 -0
  194. package/src/settings/config.test.ts +676 -0
  195. package/src/settings/config.ts +628 -0
  196. package/src/settings/use-settings-dialog.tsx +351 -0
  197. package/src/stories/__snapshots__/stories.test.tsx.snap +2358 -0
  198. package/src/stories/stories-app.tsx +196 -0
  199. package/src/stories/stories.test.tsx +74 -0
  200. package/src/stories/story-fixtures.ts +102 -0
  201. package/src/stories/story-tree.test.ts +108 -0
  202. package/src/stories/story.ts +154 -0
  203. package/src/terminal/ansi-screen.test.ts +28 -0
  204. package/src/terminal/ansi-screen.ts +72 -0
  205. package/src/terminal/components/AnsiScreen.stories.tsx +12 -0
  206. package/src/terminal/components/AnsiScreen.tsx +28 -0
  207. package/src/terminal/components/terminal-pane.test.tsx +40 -0
  208. package/src/terminal/components/terminal-pane.ts +196 -0
  209. package/src/terminal/ghostty-terminal.test.ts +85 -0
  210. package/src/terminal/ghostty-terminal.ts +197 -0
  211. package/src/terminal/kitty-image.test.ts +66 -0
  212. package/src/terminal/kitty-image.ts +137 -0
  213. package/src/terminal/pty.test.ts +81 -0
  214. package/src/terminal/pty.ts +246 -0
  215. package/src/testing/test-support.test.ts +264 -0
  216. package/src/testing/test-support.ts +367 -0
  217. package/src/thread/agent-continuation.test.ts +32 -0
  218. package/src/thread/agent-continuation.ts +13 -0
  219. package/src/thread/agent-transcript.test.ts +161 -0
  220. package/src/thread/agent-transcript.ts +336 -0
  221. package/src/thread/components/AgentConfigControls.stories.tsx +39 -0
  222. package/src/thread/components/AgentConfigControls.test.tsx +112 -0
  223. package/src/thread/components/AgentConfigControls.tsx +114 -0
  224. package/src/thread/components/AgentThreadPane.stories.tsx +105 -0
  225. package/src/thread/components/AgentThreadPane.test.tsx +935 -0
  226. package/src/thread/components/AgentThreadPane.tsx +377 -0
  227. package/src/thread/components/ThreadFooter.stories.tsx +30 -0
  228. package/src/thread/components/ThreadFooter.test.tsx +49 -0
  229. package/src/thread/components/ThreadFooter.tsx +77 -0
  230. package/src/thread/components/ThreadTree.stories.tsx +46 -0
  231. package/src/thread/components/ThreadTree.test.tsx +41 -0
  232. package/src/thread/components/ThreadTree.tsx +267 -0
  233. package/src/thread/components/thread-tree.test.ts +137 -0
  234. package/src/thread/components/thread-tree.ts +112 -0
  235. package/src/thread/thread-controller.curate.test.ts +502 -0
  236. package/src/thread/thread-controller.derivation.test.ts +62 -0
  237. package/src/thread/thread-controller.live-diff.test.ts +357 -0
  238. package/src/thread/thread-controller.optimistic.test.ts +69 -0
  239. package/src/thread/thread-controller.share.test.ts +573 -0
  240. package/src/thread/thread-controller.switch-cache.test.ts +174 -0
  241. package/src/thread/thread-controller.tree.test.ts +263 -0
  242. package/src/thread/thread-controller.ts +2026 -0
  243. package/src/thread/tree-view.test.ts +128 -0
  244. package/src/thread/tree-view.ts +146 -0
  245. package/src/thread/use-thread-agent.test.tsx +208 -0
  246. package/src/thread/use-thread-agent.ts +117 -0
  247. package/src/ui/components/CodeBlock.stories.tsx +42 -0
  248. package/src/ui/components/CodeBlock.tsx +80 -0
  249. package/src/ui/components/ConfirmCard.stories.tsx +37 -0
  250. package/src/ui/components/ConfirmCard.tsx +151 -0
  251. package/src/ui/components/ConfirmDialog.stories.tsx +21 -0
  252. package/src/ui/components/ConfirmDialog.tsx +72 -0
  253. package/src/ui/components/OverlayScrollbar.stories.tsx +33 -0
  254. package/src/ui/components/OverlayScrollbar.test.tsx +139 -0
  255. package/src/ui/components/OverlayScrollbar.tsx +129 -0
  256. package/src/ui/components/PromptDialog.stories.tsx +36 -0
  257. package/src/ui/components/PromptDialog.test.tsx +65 -0
  258. package/src/ui/components/PromptDialog.tsx +99 -0
  259. package/src/ui/components/RootOverlay.stories.tsx +45 -0
  260. package/src/ui/components/RootOverlay.tsx +49 -0
  261. package/src/ui/components/ScrollArea.stories.tsx +20 -0
  262. package/src/ui/components/ScrollArea.tsx +61 -0
  263. package/src/ui/components/Toast.stories.tsx +19 -0
  264. package/src/ui/components/Toast.tsx +54 -0
  265. package/src/ui/components/Tooltip.stories.tsx +26 -0
  266. package/src/ui/components/Tooltip.tsx +72 -0
  267. package/src/ui/components/menu-control.stories.tsx +35 -0
  268. package/src/ui/components/menu-control.tsx +43 -0
  269. package/src/ui/components/primitives/Button.stories.tsx +32 -0
  270. package/src/ui/components/primitives/Button.tsx +57 -0
  271. package/src/ui/components/primitives/Card.stories.tsx +25 -0
  272. package/src/ui/components/primitives/Card.tsx +62 -0
  273. package/src/ui/components/primitives/Dialog.stories.tsx +24 -0
  274. package/src/ui/components/primitives/Dialog.test.tsx +38 -0
  275. package/src/ui/components/primitives/Dialog.tsx +69 -0
  276. package/src/ui/components/primitives/DialogActions.stories.tsx +20 -0
  277. package/src/ui/components/primitives/DialogActions.tsx +39 -0
  278. package/src/ui/components/primitives/IconButton.stories.tsx +36 -0
  279. package/src/ui/components/primitives/IconButton.tsx +58 -0
  280. package/src/ui/components/primitives/StatusBar.stories.tsx +11 -0
  281. package/src/ui/components/primitives/StatusBar.tsx +23 -0
  282. package/src/ui/components/primitives/Switch.stories.tsx +17 -0
  283. package/src/ui/components/primitives/Switch.tsx +32 -0
  284. package/src/ui/components/primitives/Tabs.stories.tsx +29 -0
  285. package/src/ui/components/primitives/Tabs.tsx +101 -0
  286. package/src/ui/components/primitives/Toolbar.stories.tsx +17 -0
  287. package/src/ui/components/primitives/Toolbar.tsx +14 -0
  288. package/src/ui/components/primitives/Tree.stories.tsx +124 -0
  289. package/src/ui/components/primitives/Tree.tsx +133 -0
  290. package/src/ui/components/primitives/frame.test.ts +12 -0
  291. package/src/ui/components/primitives/frame.ts +17 -0
  292. package/src/ui/components/primitives/icons.ts +61 -0
  293. package/src/ui/components/primitives/tree-model.test.ts +99 -0
  294. package/src/ui/components/primitives/tree-model.ts +137 -0
  295. package/src/ui/components/syntax-highlight.ts +133 -0
  296. package/src/ui/components/truncate-text.ts +7 -0
  297. package/src/ui/components/truncate-title.test.ts +31 -0
  298. package/src/ui/components/truncate-title.ts +15 -0
  299. package/src/ui/use-frame-measure.ts +74 -0
  300. package/src/ui/use-terminal-virtualizer.ts +259 -0
  301. package/src/workbench/components/ChangesColumn.stories.tsx +44 -0
  302. package/src/workbench/components/ChangesColumn.tsx +203 -0
  303. package/src/workbench/components/ChangesFileTree.test.tsx +181 -0
  304. package/src/workbench/components/EditorGrid.split.test.tsx +118 -0
  305. package/src/workbench/components/EditorGrid.stories.tsx +86 -0
  306. package/src/workbench/components/EditorGrid.tabs.test.tsx +118 -0
  307. package/src/workbench/components/EditorGrid.tsx +544 -0
  308. package/src/workbench/components/EditorGrid.zoom.test.tsx +132 -0
  309. package/src/workbench/components/FileContentsView.stories.tsx +16 -0
  310. package/src/workbench/components/FileContentsView.test.tsx +29 -0
  311. package/src/workbench/components/FileContentsView.tsx +100 -0
  312. package/src/workbench/components/GridTabContent.actions.test.tsx +151 -0
  313. package/src/workbench/components/GridTabContent.stories.tsx +59 -0
  314. package/src/workbench/components/GridTabContent.tsx +193 -0
  315. package/src/workbench/components/ProjectTreeView.stories.tsx +25 -0
  316. package/src/workbench/components/ProjectTreeView.test.tsx +236 -0
  317. package/src/workbench/components/ProjectTreeView.tsx +151 -0
  318. package/src/workbench/components/TreePane.stories.tsx +106 -0
  319. package/src/workbench/components/TreePane.tsx +155 -0
  320. package/src/workbench/components/WelcomePlayground.stories.tsx +12 -0
  321. package/src/workbench/components/WelcomePlayground.test.tsx +83 -0
  322. package/src/workbench/components/WelcomePlayground.tsx +138 -0
  323. package/src/workbench/components/editor-grid.test.ts +184 -0
  324. package/src/workbench/components/editor-grid.ts +281 -0
  325. package/src/workbench/components/file-tree.test.ts +60 -0
  326. package/src/workbench/components/file-tree.ts +112 -0
  327. package/src/workbench/use-changes-workbench.ts +205 -0
  328. package/src/workbench/use-remember-layout.ts +31 -0
  329. package/src/workbench/use-repo-changes.ts +91 -0
  330. package/src/workbench/workbench-snapshot.test.ts +99 -0
  331. package/src/workbench/workbench-snapshot.ts +59 -0
  332. package/src/App.diff.test.tsx +0 -99
  333. package/src/App.export.test.tsx +0 -88
  334. package/src/App.plan-review.test.tsx +0 -259
  335. package/src/App.readonly.test.tsx +0 -148
  336. package/src/App.rendering.test.tsx +0 -108
  337. package/src/App.submit-confirm.test.tsx +0 -183
  338. package/src/App.test.tsx +0 -218
  339. package/src/App.tsx +0 -1249
  340. package/src/config.test.ts +0 -116
  341. package/src/config.ts +0 -150
  342. package/src/editor.ts +0 -34
  343. package/src/herdr.test.ts +0 -68
  344. package/src/herdr.ts +0 -34
  345. package/src/keymap.test.ts +0 -270
  346. package/src/keymap.ts +0 -173
  347. package/src/run.ts +0 -30
  348. package/src/serve.ts +0 -75
  349. package/src/session-controller.ts +0 -398
  350. package/src/syntax.test.ts +0 -41
  351. package/src/syntax.ts +0 -88
  352. package/src/theme.ts +0 -46
  353. package/src/view-diff.test.ts +0 -55
  354. package/src/view-diff.ts +0 -86
  355. package/src/view.test.ts +0 -157
  356. package/src/view.ts +0 -364
@@ -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
+ });
@@ -0,0 +1,105 @@
1
+ /** Tier-2 wiring for the notes-vault export: with [integrations.obsidian] set to export on resolve, submitting a review writes the note into the vault and surfaces the path in the status line. */
2
+
3
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
4
+ import {
5
+ existsSync,
6
+ mkdirSync,
7
+ mkdtempSync,
8
+ readFileSync,
9
+ readdirSync,
10
+ rmSync,
11
+ writeFileSync,
12
+ } from "node:fs";
13
+ import { tmpdir } from "node:os";
14
+ import { join } from "node:path";
15
+ import React from "react";
16
+ import { DaemonServer } from "@cueloop/daemon";
17
+ import type { Thread } from "@cueloop/schema";
18
+ import { App } from "./App";
19
+ import {
20
+ isolateUserConfig,
21
+ waitForState,
22
+ waitForText,
23
+ pressKey,
24
+ renderReadyApp,
25
+ } from "../testing/test-support";
26
+
27
+ const PLAN = `# Migration Plan
28
+
29
+ Move the store atomically.
30
+ `;
31
+
32
+ let home: string;
33
+ let vault: string;
34
+ let server: DaemonServer;
35
+ let session: Thread;
36
+ let restoreUserConfig: () => void;
37
+
38
+ beforeEach(() => {
39
+ home = mkdtempSync(join(tmpdir(), "cueloop-app-export-"));
40
+ vault = join(home, "vault");
41
+ mkdirSync(vault);
42
+ const configPath = join(home, "config.toml");
43
+
44
+ writeFileSync(
45
+ configPath,
46
+ `[integrations.obsidian]\nvault = ${JSON.stringify(vault)}\nexportOn = "message"\n`,
47
+ );
48
+ restoreUserConfig = isolateUserConfig(home, "config.toml");
49
+ server = new DaemonServer({ home, idleExitMs: 0 });
50
+ server.start();
51
+ session = server.core.sessionCreate({
52
+ workspace: { repoRoot: "/repo", branch: "main" },
53
+ artifact: {
54
+ type: "plan",
55
+ content: PLAN,
56
+ meta: { title: "Migration Plan", planPath: "plan.md" },
57
+ },
58
+ });
59
+ });
60
+ afterEach(() => {
61
+ restoreUserConfig();
62
+ server.stop();
63
+ rmSync(home, { recursive: true, force: true });
64
+ });
65
+
66
+ describe("obsidian export on resolve", () => {
67
+ test("submitting a review writes the plan into the vault and shows the path", async () => {
68
+ // Arrange
69
+ const setup = await renderReadyApp(<App home={home} sessionId={session.id} />, {
70
+ width: 120,
71
+ height: 32,
72
+ });
73
+
74
+ await waitForText(setup, "cueloop");
75
+
76
+ // Act
77
+ await pressKey(setup, "RETURN", { meta: true }); // open the submit overlay, keep the default message (approve)
78
+
79
+ // Assert
80
+ await waitForText(setup, "[approve]");
81
+
82
+ // Act
83
+ await pressKey(setup, "RETURN", { meta: true }); // submit
84
+
85
+ // Assert
86
+ // the export is an async round-trip after resolve; the status line lands
87
+ // on a later render than the file write
88
+ const dir = join(vault, "cueloop");
89
+
90
+ await waitForState(setup, () => existsSync(dir));
91
+ await waitForText(setup, "exported to");
92
+
93
+ const notes = readdirSync(dir);
94
+
95
+ expect(notes.length).toBe(1);
96
+ expect(notes[0]).toMatch(/^\d{4}-\d{2}-\d{2} - Migration Plan\.md$/);
97
+ const written = readFileSync(join(dir, notes[0]!), "utf8");
98
+
99
+ expect(written).toContain("source: cueloop");
100
+ expect(written).toContain(`session: ${session.id}`);
101
+ expect(written).toContain("message: approved");
102
+ expect(written).toContain("Move the store atomically.");
103
+ expect(setup.captureCharFrame()).toContain("exported to");
104
+ });
105
+ });
@@ -0,0 +1,89 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { nextFocusPane, reconciledFocus, threadsNavHandled, visiblePanes } from "./App";
3
+
4
+ describe("threads keyboard nav", () => {
5
+ const base = () => {
6
+ let cursor = 1;
7
+ let opened = 0;
8
+
9
+ return {
10
+ setInboxCursor: (update: (cursor: number) => number) => (cursor = update(cursor)),
11
+ openSession: () => (opened += 1),
12
+ read: () => ({
13
+ get cursor() {
14
+ return cursor;
15
+ },
16
+ get opened() {
17
+ return opened;
18
+ },
19
+ }),
20
+ };
21
+ };
22
+
23
+ test("moves the cursor and opens only when the Threads pane is quiet and focused", () => {
24
+ const spy = base();
25
+ const call = (name: string) =>
26
+ threadsNavHandled({
27
+ focusedPane: "threads",
28
+ quiet: true,
29
+ key: { name },
30
+ count: 3,
31
+ setInboxCursor: spy.setInboxCursor,
32
+ openSession: spy.openSession,
33
+ });
34
+
35
+ expect(call("down")).toBe(true);
36
+ expect(spy.read().cursor).toBe(2);
37
+ expect(call("down")).toBe(true); // clamps at the last row
38
+ expect(spy.read().cursor).toBe(2);
39
+ expect(call("k")).toBe(true);
40
+ expect(spy.read().cursor).toBe(1);
41
+ expect(call("return")).toBe(true);
42
+ expect(spy.read().opened).toBe(1);
43
+ });
44
+
45
+ test("defers when another pane is focused, keys are owned elsewhere, or the inbox is empty", () => {
46
+ const spy = base();
47
+ const params = {
48
+ quiet: true,
49
+ key: { name: "down" },
50
+ count: 3,
51
+ setInboxCursor: spy.setInboxCursor,
52
+ openSession: spy.openSession,
53
+ };
54
+
55
+ expect(threadsNavHandled({ ...params, focusedPane: "changes" })).toBe(false);
56
+ expect(threadsNavHandled({ ...params, focusedPane: "threads", quiet: false })).toBe(false);
57
+ expect(threadsNavHandled({ ...params, focusedPane: "threads", count: 0 })).toBe(false);
58
+ expect(spy.read().cursor).toBe(1);
59
+ });
60
+ });
61
+
62
+ describe("pane focus cycle", () => {
63
+ test("visiblePanes lists only the open panes, in reading order", () => {
64
+ expect(visiblePanes(true, true, true, true)).toEqual([
65
+ "threads",
66
+ "thread",
67
+ "changes",
68
+ "project",
69
+ ]);
70
+ expect(visiblePanes(true, false, true, true)).toEqual(["threads", "changes", "project"]);
71
+ expect(visiblePanes(false, false, true, false)).toEqual(["changes"]);
72
+ });
73
+
74
+ test("nextFocusPane wraps forward and back over the visible panes", () => {
75
+ const panes = visiblePanes(true, false, true, true);
76
+
77
+ expect(nextFocusPane("threads", panes, false)).toBe("changes");
78
+ expect(nextFocusPane("project", panes, false)).toBe("threads");
79
+ expect(nextFocusPane("threads", panes, true)).toBe("project");
80
+ expect(nextFocusPane("changes", [], false)).toBe("changes");
81
+ });
82
+
83
+ test("reconciledFocus falls back only when the focused pane is not navigable", () => {
84
+ expect(reconciledFocus(["threads", "changes"], "threads")).toBeNull();
85
+ expect(reconciledFocus(["threads", "changes"], "thread")).toBe("threads");
86
+ expect(reconciledFocus(["changes", "project"], "thread")).toBe("changes");
87
+ expect(reconciledFocus([], "thread")).toBeNull();
88
+ });
89
+ });
@@ -0,0 +1,84 @@
1
+ /** Reviewer identity surfaces: the Account settings tab and the author-name tooltip on a comment dot. */
2
+
3
+ import { afterEach, beforeEach, 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 { App } from "./App";
10
+ import {
11
+ isolateUserConfig,
12
+ locateText,
13
+ renderReadyApp,
14
+ waitForText,
15
+ } from "../testing/test-support";
16
+
17
+ const PLAN = "# Migration Plan\n\nThe daemon persists sessions to disk atomically.\n";
18
+
19
+ let home: string;
20
+ let restoreUserConfig: () => void;
21
+ let server: DaemonServer;
22
+ let sessionId: string;
23
+
24
+ beforeEach(() => {
25
+ home = mkdtempSync(join(tmpdir(), "cueloop-identity-"));
26
+ restoreUserConfig = isolateUserConfig(home);
27
+ server = new DaemonServer({ home, idleExitMs: 0 });
28
+ server.start();
29
+ const session = server.core.sessionCreate({
30
+ workspace: { repoRoot: "/repo", branch: "main" },
31
+ artifact: {
32
+ type: "plan",
33
+ content: PLAN,
34
+ meta: { title: "Migration Plan", planPath: "plan.md" },
35
+ },
36
+ });
37
+
38
+ sessionId = session.id;
39
+ });
40
+ afterEach(() => {
41
+ restoreUserConfig();
42
+ server.stop();
43
+ rmSync(home, { recursive: true, force: true });
44
+ });
45
+
46
+ async function renderApp() {
47
+ return renderReadyApp(<App home={home} sessionId={sessionId} />, { width: 120, height: 32 });
48
+ }
49
+
50
+ test("the Account settings tab shows the display name row and the GitHub sync action", async () => {
51
+ const setup = await renderApp();
52
+
53
+ await setup.mockMouse.click(1, 0);
54
+ await waitForText(setup, "settings");
55
+ const menu = setup.captureCharFrame().split("\n");
56
+ const settingsRow = menu.findIndex((line) => line.includes("settings"));
57
+
58
+ await setup.mockMouse.click(menu[settingsRow]!.indexOf("settings") + 1, settingsRow);
59
+ await waitForText(setup, "Account");
60
+ const dialog = setup.captureCharFrame().split("\n");
61
+ const accountRow = dialog.findIndex((line) => line.includes("Account"));
62
+
63
+ await setup.mockMouse.click(dialog[accountRow]!.indexOf("Account") + 1, accountRow);
64
+ await waitForText(setup, "Display name");
65
+ expect(setup.captureCharFrame()).toContain("Sync from GitHub");
66
+ });
67
+
68
+ test("hovering an own comment's dot surfaces the author tooltip", async () => {
69
+ server.core.sessionAnnotate(sessionId, {
70
+ id: "a_own",
71
+ kind: "comment",
72
+ anchor: { quote: "atomically", prefix: "sessions to disk ", suffix: "." },
73
+ body: "My note.",
74
+ });
75
+ const setup = await renderApp();
76
+
77
+ await waitForText(setup, "● My note.");
78
+ // the tooltip is not painted until the pointer is over the dot
79
+ expect(setup.captureCharFrame()).not.toContain("you");
80
+ const dot = locateText(setup, "● My note.");
81
+
82
+ await setup.mockMouse.moveTo(dot.column, dot.row);
83
+ await waitForText(setup, "you");
84
+ });