@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,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
+ });
@@ -1,4 +1,4 @@
1
- /** Post-submit completion overlay: prompt, opt-in persistence, countdown, dismissal. */
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
2
 
3
3
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
4
4
  import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
@@ -6,113 +6,139 @@ import { tmpdir } from "node:os";
6
6
  import { join } from "node:path";
7
7
  import React from "react";
8
8
  import { testRender } from "@opentui/react/test-utils";
9
+ import { ManualClock } from "@opentui/core/testing";
9
10
  import { DaemonServer } from "@cueloop/daemon";
10
- import type { ReviewSession } from "@cueloop/schema";
11
+ import type { Thread } from "@cueloop/schema";
11
12
  import { App } from "./App";
13
+ import { isolateUserConfig, press, settle, waitForText, pressKey } from "../testing/test-support";
12
14
 
13
15
  const PLAN = "# Plan\n\nShip the thing.\n";
14
16
 
15
17
  let home: string;
16
18
  let server: DaemonServer;
17
- let session: ReviewSession;
19
+ let session: Thread;
18
20
  let configPath: string;
21
+ let restoreUserConfig: () => void;
22
+ let clock: ManualClock;
19
23
 
20
24
  beforeEach(() => {
21
25
  home = mkdtempSync(join(tmpdir(), "cueloop-close-"));
22
26
  configPath = join(home, "config.toml");
23
- process.env.CUELOOP_CONFIG = configPath;
27
+ restoreUserConfig = isolateUserConfig(home, "config.toml");
24
28
  server = new DaemonServer({ home, idleExitMs: 0 });
25
29
  server.start();
26
30
  session = server.core.sessionCreate({
27
31
  workspace: { repoRoot: "/repo", branch: "main" },
28
32
  artifact: { type: "plan", content: PLAN, meta: { title: "Plan" } },
29
33
  });
34
+ clock = new ManualClock();
30
35
  });
31
36
  afterEach(() => {
32
- delete process.env.CUELOOP_CONFIG;
37
+ restoreUserConfig();
33
38
  server.stop();
34
39
  rmSync(home, { recursive: true, force: true });
35
40
  });
36
41
 
37
42
  async function renderApp(onExit?: (code: number) => void) {
38
- const setup = await testRender(<App home={home} sessionId={session.id} onExit={onExit} />, {
39
- width: 120,
40
- height: 30,
41
- });
42
- for (let i = 0; i < 40 && !setup.captureCharFrame().includes("cueloop"); i++) {
43
- await Bun.sleep(25);
44
- await setup.renderOnce();
45
- }
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
+
46
53
  return setup;
47
54
  }
48
55
 
49
56
  type Setup = Awaited<ReturnType<typeof renderApp>>;
50
- async function press(setup: Setup, k: string): Promise<void> {
51
- if (k === "enter") setup.mockInput.pressKey("RETURN");
52
- else if (k === "escape") setup.mockInput.pressKey("ESCAPE");
53
- else await setup.mockInput.typeText(k);
54
- await Bun.sleep(15);
55
- await setup.renderOnce();
56
- }
57
57
 
58
58
  async function submitApprove(setup: Setup): Promise<void> {
59
- await press(setup, "enter"); // open submit (approve default: no pending items)
60
- await press(setup, "enter"); // confirm
61
- const deadline = Date.now() + 10_000;
62
- while (Date.now() < deadline && !setup.captureCharFrame().includes("review approved")) {
63
- await Bun.sleep(25);
64
- await setup.renderOnce();
65
- }
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");
66
62
  }
67
63
 
68
64
  describe("completion overlay", () => {
69
- test("submit with auto-close off shows the prompt; ⏎ exits", async () => {
65
+ test("submit with auto-close off counts down from 5; ⏎ exits now", async () => {
66
+ // Arrange
70
67
  let exited = -1;
71
68
  const setup = await renderApp((code) => (exited = code));
69
+
70
+ // Act
72
71
  await submitApprove(setup);
73
- const frame = setup.captureCharFrame();
74
- expect(frame).toContain("✓ review approved");
75
- expect(frame).toContain("a always close after submit");
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
76
81
  await press(setup, "enter");
82
+
83
+ // Assert
77
84
  expect(exited).toBe(0);
78
85
  });
79
86
 
80
- test("a opts in: persists the config and starts the countdown", async () => {
87
+ test("a remembers the countdown as the persisted default", async () => {
88
+ // Arrange
81
89
  const setup = await renderApp();
90
+
82
91
  await submitApprove(setup);
92
+ await waitForText(setup, "closing in 5s");
93
+
94
+ // Act
83
95
  await press(setup, "a");
84
- expect(setup.captureCharFrame()).toContain("closing in 3s");
96
+
97
+ // Assert
85
98
  expect(existsSync(configPath)).toBe(true);
86
- expect(readFileSync(configPath, "utf8")).toContain("auto_close = 3");
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");
87
107
  });
88
108
 
89
109
  test("esc dismisses to the resolved read-only view", async () => {
110
+ // Arrange
90
111
  const setup = await renderApp();
112
+
91
113
  await submitApprove(setup);
92
- await press(setup, "escape");
114
+ await waitForText(setup, "closing in 5s");
115
+
116
+ // Act
93
117
  // a bare ESC sits in the input parser's escape-sequence disambiguation
94
- // window before it is delivered - poll for the dismissal
95
- const deadline = Date.now() + 5_000;
96
- while (Date.now() < deadline && !setup.captureCharFrame().includes("resolved: approve")) {
97
- await Bun.sleep(50);
98
- await setup.renderOnce();
99
- }
100
- const frame = setup.captureCharFrame();
101
- expect(frame).toContain("resolved: approve");
102
- expect(frame).not.toContain("always close after submit");
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");
103
125
  });
104
126
 
105
127
  test("configured auto_close counts down and exits without interaction", async () => {
128
+ // Arrange
106
129
  writeFileSync(configPath, "[ui]\nauto_close = 1\n");
107
130
  let exited = -1;
108
131
  const setup = await renderApp((code) => (exited = code));
132
+
109
133
  await submitApprove(setup);
110
- expect(setup.captureCharFrame()).toContain("closing in 1s");
111
- const deadline = Date.now() + 10_000;
112
- while (Date.now() < deadline && exited === -1) {
113
- await Bun.sleep(100);
114
- await setup.renderOnce();
115
- }
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
116
142
  expect(exited).toBe(0);
117
- }, 20_000);
143
+ });
118
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);