@cueloop/client 0.1.0-alpha.11 → 0.1.0-alpha.111

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 (360) 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 +63 -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 +383 -0
  12. package/src/annotations/components/DiscussionMarkerRail.stories.tsx +54 -0
  13. package/src/annotations/components/DiscussionMarkerRail.tsx +189 -0
  14. package/src/annotations/discussions.ts +78 -0
  15. package/src/annotations/thread-selection.test.ts +210 -0
  16. package/src/annotations/thread-selection.ts +248 -0
  17. package/src/annotations/use-annotation-surface.tsx +1378 -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 +190 -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.harness-settings.test.tsx +77 -0
  25. package/src/app/App.identity.test.tsx +84 -0
  26. package/src/app/App.plan-render.test.tsx +211 -0
  27. package/src/app/App.plan-review.test.tsx +429 -0
  28. package/src/app/App.prototype-markdown.test.tsx +129 -0
  29. package/src/app/App.prototype.test.tsx +176 -0
  30. package/src/app/App.readonly.test.tsx +274 -0
  31. package/src/app/App.regression.test.tsx +99 -0
  32. package/src/app/App.rendering.test.tsx +134 -0
  33. package/src/app/App.share-dialog.test.tsx +177 -0
  34. package/src/app/App.submit-confirm.test.tsx +267 -0
  35. package/src/app/App.test.tsx +497 -0
  36. package/src/app/App.thread-agent.test.ts +20 -0
  37. package/src/app/App.toast.test.tsx +124 -0
  38. package/src/app/App.tsx +1758 -0
  39. package/src/app/App.visual.test.tsx +60 -0
  40. package/src/app/App.walk.test.tsx +300 -0
  41. package/src/app/App.workbench.test.tsx +1153 -0
  42. package/src/app/app-key-state.ts +61 -0
  43. package/src/app/app-screens.tsx +607 -0
  44. package/src/app/app-view-model.ts +178 -0
  45. package/src/app/components/AppHeader.stories.tsx +50 -0
  46. package/src/app/components/AppHeader.tsx +81 -0
  47. package/src/app/components/AppShell.brand.test.tsx +58 -0
  48. package/src/app/components/AppShell.extensions.test.tsx +191 -0
  49. package/src/app/components/AppShell.stories.tsx +89 -0
  50. package/src/app/components/AppShell.tsx +358 -0
  51. package/src/app/components/AppShell.zoom.test.tsx +149 -0
  52. package/src/app/components/Breadcrumb.stories.tsx +19 -0
  53. package/src/app/components/Breadcrumb.tsx +45 -0
  54. package/src/app/components/MenuBar.stories.tsx +28 -0
  55. package/src/app/components/MenuBar.tsx +44 -0
  56. package/src/app/components/PaneGrid.stories.tsx +84 -0
  57. package/src/app/components/PaneGrid.tsx +128 -0
  58. package/src/app/components/PanelColumn.stories.tsx +25 -0
  59. package/src/app/components/PanelColumn.tsx +93 -0
  60. package/src/app/components/ShellHeader.stories.tsx +50 -0
  61. package/src/app/components/ShellHeader.tsx +137 -0
  62. package/src/app/components/WorkspacePanels.stories.tsx +36 -0
  63. package/src/app/components/WorkspacePanels.tsx +121 -0
  64. package/src/app/components/pages/Workspace.stories.tsx +181 -0
  65. package/src/app/intent-dispatch.test.ts +729 -0
  66. package/src/app/intent-dispatch.ts +713 -0
  67. package/src/app/launch-layout.test.ts +78 -0
  68. package/src/app/launch-layout.ts +57 -0
  69. package/src/app/ready-signal.test.ts +85 -0
  70. package/src/app/ready-signal.ts +54 -0
  71. package/src/app/run.ts +111 -0
  72. package/src/{serve.test.ts → app/serve.test.ts} +28 -12
  73. package/src/app/serve.ts +111 -0
  74. package/src/app/version.ts +5 -0
  75. package/src/appearance/components/theme-context.stories.tsx +46 -0
  76. package/src/appearance/components/theme-context.tsx +37 -0
  77. package/src/appearance/theme-presets.test.ts +51 -0
  78. package/src/appearance/theme-presets.ts +219 -0
  79. package/src/appearance/theme.ts +112 -0
  80. package/src/appearance/visual/contrast.test.ts +35 -0
  81. package/src/appearance/visual/contrast.ts +33 -0
  82. package/src/diff/components/AnnotatableFileView.stories.tsx +42 -0
  83. package/src/diff/components/AnnotatableFileView.test.tsx +160 -0
  84. package/src/diff/components/AnnotatableFileView.tsx +142 -0
  85. package/src/diff/components/BareWorkbenchFileView.stories.tsx +27 -0
  86. package/src/diff/components/BareWorkbenchFileView.tsx +63 -0
  87. package/src/diff/components/DiffContentView.scroll.test.tsx +443 -0
  88. package/src/diff/components/DiffContentView.split.test.tsx +140 -0
  89. package/src/diff/components/DiffContentView.stories.tsx +80 -0
  90. package/src/diff/components/DiffContentView.syntax.test.tsx +85 -0
  91. package/src/diff/components/DiffContentView.tsx +1080 -0
  92. package/src/diff/components/WalkWizard.stories.tsx +100 -0
  93. package/src/diff/components/WalkWizard.tsx +146 -0
  94. package/src/diff/components/diff-content-view-layout.test.ts +198 -0
  95. package/src/diff/components/diff-content-view-layout.ts +87 -0
  96. package/src/diff/diff-fold.test.ts +78 -0
  97. package/src/diff/diff-fold.ts +130 -0
  98. package/src/diff/diff-hunk-curate.test.ts +315 -0
  99. package/src/diff/diff-hunk-curate.ts +155 -0
  100. package/src/diff/diff-intraline.test.ts +239 -0
  101. package/src/diff/diff-intraline.ts +244 -0
  102. package/src/diff/diff-syntax.test.ts +125 -0
  103. package/src/diff/diff-syntax.ts +194 -0
  104. package/src/diff/split-diff.test.ts +110 -0
  105. package/src/diff/split-diff.ts +150 -0
  106. package/src/diff/view-diff.test.ts +325 -0
  107. package/src/diff/view-diff.ts +540 -0
  108. package/src/diff/walk.test.ts +121 -0
  109. package/src/diff/walk.ts +76 -0
  110. package/src/index.ts +32 -2
  111. package/src/integrations/attribution.test.ts +31 -0
  112. package/src/integrations/attribution.ts +44 -0
  113. package/src/integrations/client-extension-registry.test.tsx +61 -0
  114. package/src/integrations/client-extension-registry.ts +163 -0
  115. package/src/integrations/clipboard.test.ts +23 -0
  116. package/src/integrations/clipboard.ts +34 -0
  117. package/src/integrations/components/ExtensionZone.stories.tsx +28 -0
  118. package/src/integrations/components/ExtensionZone.tsx +145 -0
  119. package/src/integrations/components/ShareDialog.agent.test.tsx +52 -0
  120. package/src/integrations/components/ShareDialog.stories.tsx +121 -0
  121. package/src/integrations/components/ShareDialog.tsx +602 -0
  122. package/src/integrations/components/share-dialog-store.ts +59 -0
  123. package/src/integrations/editor.test.ts +149 -0
  124. package/src/integrations/editor.ts +163 -0
  125. package/src/integrations/github-identity.test.ts +59 -0
  126. package/src/integrations/github-identity.ts +42 -0
  127. package/src/integrations/herdr.test.ts +43 -0
  128. package/src/integrations/herdr.ts +40 -0
  129. package/src/integrations/integrations.test.ts +41 -0
  130. package/src/integrations/integrations.ts +32 -0
  131. package/src/integrations/pasted-image.test.ts +67 -0
  132. package/src/integrations/pasted-image.ts +62 -0
  133. package/src/integrations/share.ts +283 -0
  134. package/src/keyboard/components/CompletionOverlay.stories.tsx +37 -0
  135. package/src/keyboard/components/CompletionOverlay.tsx +75 -0
  136. package/src/keyboard/components/HandleChipsInput.stories.tsx +24 -0
  137. package/src/keyboard/components/HandleChipsInput.test.tsx +35 -0
  138. package/src/keyboard/components/HandleChipsInput.tsx +147 -0
  139. package/src/keyboard/components/KeybindsDialog.stories.tsx +31 -0
  140. package/src/keyboard/components/KeybindsDialog.tsx +103 -0
  141. package/src/keyboard/components/NavModeHint.stories.tsx +40 -0
  142. package/src/keyboard/components/NavModeHint.tsx +31 -0
  143. package/src/keyboard/components/SlashComposer.stories.tsx +37 -0
  144. package/src/keyboard/components/SlashComposer.tsx +90 -0
  145. package/src/keyboard/key-bindings.test.ts +156 -0
  146. package/src/keyboard/key-bindings.ts +462 -0
  147. package/src/keyboard/keymap.test.ts +611 -0
  148. package/src/keyboard/keymap.ts +446 -0
  149. package/src/keyboard/skills.test.ts +74 -0
  150. package/src/keyboard/skills.ts +77 -0
  151. package/src/keyboard/slash-palette.test.ts +103 -0
  152. package/src/keyboard/slash-palette.ts +137 -0
  153. package/src/keyboard/thread-chords.test.ts +79 -0
  154. package/src/keyboard/thread-chords.ts +168 -0
  155. package/src/keyboard/use-shared-keyboard.ts +56 -0
  156. package/src/markdown/__snapshots__/view-plan.regression.test.ts.snap +100 -0
  157. package/src/markdown/components/MarkdownGridBlock.stories.tsx +33 -0
  158. package/src/markdown/components/MarkdownGridBlock.tsx +72 -0
  159. package/src/markdown/components/MarkdownThreadEditor.stories.tsx +15 -0
  160. package/src/markdown/components/MarkdownThreadEditor.test.tsx +98 -0
  161. package/src/markdown/components/MarkdownThreadEditor.tsx +165 -0
  162. package/src/markdown/components/ThreadView.selection.test.tsx +954 -0
  163. package/src/markdown/components/ThreadView.slash.test.tsx +146 -0
  164. package/src/markdown/components/ThreadView.stories.tsx +26 -0
  165. package/src/markdown/components/ThreadView.tsx +577 -0
  166. package/src/markdown/mark-runs.ts +125 -0
  167. package/src/markdown/markdown-editor-syntax-style.ts +66 -0
  168. package/src/markdown/markdown-grid.test.ts +101 -0
  169. package/src/markdown/markdown-grid.ts +209 -0
  170. package/src/markdown/markdown-highlight.test.ts +61 -0
  171. package/src/markdown/markdown-highlight.ts +125 -0
  172. package/src/markdown/thread-code-syntax.ts +48 -0
  173. package/src/markdown/use-thread-body-editing.tsx +54 -0
  174. package/src/markdown/view-plan.regression.test.ts +60 -0
  175. package/src/markdown/view-plan.test.ts +545 -0
  176. package/src/markdown/view-plan.ts +1061 -0
  177. package/src/performance/otlp-trace.test.ts +119 -0
  178. package/src/performance/otlp-trace.ts +134 -0
  179. package/src/performance/perf-report.ts +19 -0
  180. package/src/performance/perf-timings.test.ts +18 -0
  181. package/src/performance/perf-timings.ts +40 -0
  182. package/src/prototype/components/PrototypeContentView.stories.tsx +19 -0
  183. package/src/prototype/components/PrototypeContentView.tsx +459 -0
  184. package/src/prototype/prototype-browser.test.ts +81 -0
  185. package/src/prototype/prototype-browser.ts +332 -0
  186. package/src/prototype/prototype-pixels.tsx +35 -0
  187. package/src/settings/components/SettingsDialog.stories.tsx +117 -0
  188. package/src/settings/components/SettingsDialog.tsx +186 -0
  189. package/src/settings/components/SettingsRows.stories.tsx +18 -0
  190. package/src/settings/components/SettingsRows.tsx +112 -0
  191. package/src/settings/components/quick-actions-editor.stories.tsx +58 -0
  192. package/src/settings/components/quick-actions-editor.test.tsx +120 -0
  193. package/src/settings/components/quick-actions-editor.tsx +222 -0
  194. package/src/settings/config-identity.test.ts +78 -0
  195. package/src/settings/config.test.ts +771 -0
  196. package/src/settings/config.ts +722 -0
  197. package/src/settings/use-settings-dialog.test.tsx +172 -0
  198. package/src/settings/use-settings-dialog.tsx +465 -0
  199. package/src/settings/use-thread-harness-setting.test.tsx +87 -0
  200. package/src/settings/use-thread-harness-setting.ts +86 -0
  201. package/src/stories/__snapshots__/stories.test.tsx.snap +2358 -0
  202. package/src/stories/stories-app.tsx +196 -0
  203. package/src/stories/stories.test.tsx +78 -0
  204. package/src/stories/story-fixtures.ts +102 -0
  205. package/src/stories/story-tree.test.ts +108 -0
  206. package/src/stories/story.ts +160 -0
  207. package/src/terminal/ansi-screen.test.ts +28 -0
  208. package/src/terminal/ansi-screen.ts +77 -0
  209. package/src/terminal/components/AnsiScreen.stories.tsx +12 -0
  210. package/src/terminal/components/AnsiScreen.tsx +28 -0
  211. package/src/terminal/components/terminal-pane.test.tsx +40 -0
  212. package/src/terminal/components/terminal-pane.ts +208 -0
  213. package/src/terminal/ghostty-terminal.test.ts +85 -0
  214. package/src/terminal/ghostty-terminal.ts +207 -0
  215. package/src/terminal/kitty-image.test.ts +66 -0
  216. package/src/terminal/kitty-image.ts +140 -0
  217. package/src/terminal/pty.test.ts +81 -0
  218. package/src/terminal/pty.ts +255 -0
  219. package/src/testing/test-support.test.ts +266 -0
  220. package/src/testing/test-support.ts +449 -0
  221. package/src/thread/agent-continuation.test.ts +34 -0
  222. package/src/thread/agent-continuation.ts +13 -0
  223. package/src/thread/agent-transcript.test.ts +161 -0
  224. package/src/thread/agent-transcript.ts +344 -0
  225. package/src/thread/components/AgentConfigControls.stories.tsx +39 -0
  226. package/src/thread/components/AgentConfigControls.test.tsx +153 -0
  227. package/src/thread/components/AgentConfigControls.tsx +121 -0
  228. package/src/thread/components/AgentThreadPane.stories.tsx +105 -0
  229. package/src/thread/components/AgentThreadPane.test.tsx +1248 -0
  230. package/src/thread/components/AgentThreadPane.tsx +458 -0
  231. package/src/thread/components/ThreadFooter.stories.tsx +30 -0
  232. package/src/thread/components/ThreadFooter.test.tsx +49 -0
  233. package/src/thread/components/ThreadFooter.tsx +77 -0
  234. package/src/thread/components/ThreadTree.stories.tsx +46 -0
  235. package/src/thread/components/ThreadTree.test.tsx +41 -0
  236. package/src/thread/components/ThreadTree.tsx +268 -0
  237. package/src/thread/components/thread-tree.test.ts +138 -0
  238. package/src/thread/components/thread-tree.ts +112 -0
  239. package/src/thread/thread-controller.curate.test.ts +504 -0
  240. package/src/thread/thread-controller.derivation.test.ts +62 -0
  241. package/src/thread/thread-controller.live-diff.test.ts +357 -0
  242. package/src/thread/thread-controller.optimistic.test.ts +69 -0
  243. package/src/thread/thread-controller.share.test.ts +618 -0
  244. package/src/thread/thread-controller.switch-cache.test.ts +174 -0
  245. package/src/thread/thread-controller.tree.test.ts +263 -0
  246. package/src/thread/thread-controller.ts +2151 -0
  247. package/src/thread/tree-view.test.ts +128 -0
  248. package/src/thread/tree-view.ts +150 -0
  249. package/src/thread/use-thread-agent.test.tsx +209 -0
  250. package/src/thread/use-thread-agent.ts +122 -0
  251. package/src/ui/components/CodeBlock.stories.tsx +42 -0
  252. package/src/ui/components/CodeBlock.tsx +82 -0
  253. package/src/ui/components/ConfirmCard.stories.tsx +37 -0
  254. package/src/ui/components/ConfirmCard.tsx +151 -0
  255. package/src/ui/components/ConfirmDialog.stories.tsx +21 -0
  256. package/src/ui/components/ConfirmDialog.tsx +72 -0
  257. package/src/ui/components/OverlayScrollbar.stories.tsx +33 -0
  258. package/src/ui/components/OverlayScrollbar.test.tsx +142 -0
  259. package/src/ui/components/OverlayScrollbar.tsx +134 -0
  260. package/src/ui/components/PromptDialog.stories.tsx +36 -0
  261. package/src/ui/components/PromptDialog.test.tsx +65 -0
  262. package/src/ui/components/PromptDialog.tsx +101 -0
  263. package/src/ui/components/RootOverlay.stories.tsx +45 -0
  264. package/src/ui/components/RootOverlay.tsx +50 -0
  265. package/src/ui/components/ScrollArea.stories.tsx +20 -0
  266. package/src/ui/components/ScrollArea.tsx +63 -0
  267. package/src/ui/components/Toast.stories.tsx +19 -0
  268. package/src/ui/components/Toast.tsx +54 -0
  269. package/src/ui/components/Tooltip.stories.tsx +26 -0
  270. package/src/ui/components/Tooltip.tsx +93 -0
  271. package/src/ui/components/menu-control.stories.tsx +35 -0
  272. package/src/ui/components/menu-control.tsx +43 -0
  273. package/src/ui/components/primitives/Button.stories.tsx +32 -0
  274. package/src/ui/components/primitives/Button.tsx +58 -0
  275. package/src/ui/components/primitives/Card.stories.tsx +25 -0
  276. package/src/ui/components/primitives/Card.tsx +62 -0
  277. package/src/ui/components/primitives/Dialog.stories.tsx +24 -0
  278. package/src/ui/components/primitives/Dialog.test.tsx +38 -0
  279. package/src/ui/components/primitives/Dialog.tsx +69 -0
  280. package/src/ui/components/primitives/DialogActions.stories.tsx +20 -0
  281. package/src/ui/components/primitives/DialogActions.tsx +39 -0
  282. package/src/ui/components/primitives/IconButton.stories.tsx +36 -0
  283. package/src/ui/components/primitives/IconButton.tsx +58 -0
  284. package/src/ui/components/primitives/StatusBar.stories.tsx +11 -0
  285. package/src/ui/components/primitives/StatusBar.tsx +23 -0
  286. package/src/ui/components/primitives/Switch.stories.tsx +17 -0
  287. package/src/ui/components/primitives/Switch.tsx +32 -0
  288. package/src/ui/components/primitives/Tabs.stories.tsx +29 -0
  289. package/src/ui/components/primitives/Tabs.tsx +102 -0
  290. package/src/ui/components/primitives/Toolbar.stories.tsx +17 -0
  291. package/src/ui/components/primitives/Toolbar.tsx +14 -0
  292. package/src/ui/components/primitives/Tree.stories.tsx +124 -0
  293. package/src/ui/components/primitives/Tree.tsx +137 -0
  294. package/src/ui/components/primitives/frame.test.ts +12 -0
  295. package/src/ui/components/primitives/frame.ts +17 -0
  296. package/src/ui/components/primitives/icons.ts +61 -0
  297. package/src/ui/components/primitives/tree-model.test.ts +99 -0
  298. package/src/ui/components/primitives/tree-model.ts +144 -0
  299. package/src/ui/components/syntax-highlight.ts +154 -0
  300. package/src/ui/components/truncate-text.ts +7 -0
  301. package/src/ui/components/truncate-title.test.ts +31 -0
  302. package/src/ui/components/truncate-title.ts +16 -0
  303. package/src/ui/use-frame-measure.ts +88 -0
  304. package/src/ui/use-terminal-virtualizer.ts +286 -0
  305. package/src/workbench/components/ChangesColumn.stories.tsx +44 -0
  306. package/src/workbench/components/ChangesColumn.tsx +210 -0
  307. package/src/workbench/components/ChangesFileTree.test.tsx +184 -0
  308. package/src/workbench/components/EditorGrid.split.test.tsx +119 -0
  309. package/src/workbench/components/EditorGrid.stories.tsx +86 -0
  310. package/src/workbench/components/EditorGrid.tabs.test.tsx +118 -0
  311. package/src/workbench/components/EditorGrid.tsx +552 -0
  312. package/src/workbench/components/EditorGrid.zoom.test.tsx +132 -0
  313. package/src/workbench/components/FileContentsView.stories.tsx +16 -0
  314. package/src/workbench/components/FileContentsView.test.tsx +29 -0
  315. package/src/workbench/components/FileContentsView.tsx +104 -0
  316. package/src/workbench/components/GridTabContent.actions.test.tsx +151 -0
  317. package/src/workbench/components/GridTabContent.stories.tsx +59 -0
  318. package/src/workbench/components/GridTabContent.tsx +195 -0
  319. package/src/workbench/components/ProjectTreeView.stories.tsx +25 -0
  320. package/src/workbench/components/ProjectTreeView.test.tsx +239 -0
  321. package/src/workbench/components/ProjectTreeView.tsx +159 -0
  322. package/src/workbench/components/TreePane.stories.tsx +106 -0
  323. package/src/workbench/components/TreePane.tsx +157 -0
  324. package/src/workbench/components/WelcomePlayground.stories.tsx +12 -0
  325. package/src/workbench/components/WelcomePlayground.test.tsx +83 -0
  326. package/src/workbench/components/WelcomePlayground.tsx +138 -0
  327. package/src/workbench/components/editor-grid.test.ts +184 -0
  328. package/src/workbench/components/editor-grid.ts +284 -0
  329. package/src/workbench/components/file-tree.test.ts +60 -0
  330. package/src/workbench/components/file-tree.ts +114 -0
  331. package/src/workbench/use-changes-workbench.ts +210 -0
  332. package/src/workbench/use-remember-layout.ts +33 -0
  333. package/src/workbench/use-repo-changes.ts +93 -0
  334. package/src/workbench/workbench-snapshot.test.ts +99 -0
  335. package/src/workbench/workbench-snapshot.ts +59 -0
  336. package/src/App.diff.test.tsx +0 -99
  337. package/src/App.export.test.tsx +0 -88
  338. package/src/App.plan-review.test.tsx +0 -259
  339. package/src/App.readonly.test.tsx +0 -148
  340. package/src/App.rendering.test.tsx +0 -108
  341. package/src/App.submit-confirm.test.tsx +0 -183
  342. package/src/App.test.tsx +0 -218
  343. package/src/App.tsx +0 -1249
  344. package/src/config.test.ts +0 -116
  345. package/src/config.ts +0 -150
  346. package/src/editor.ts +0 -34
  347. package/src/herdr.test.ts +0 -68
  348. package/src/herdr.ts +0 -34
  349. package/src/keymap.test.ts +0 -270
  350. package/src/keymap.ts +0 -173
  351. package/src/run.ts +0 -30
  352. package/src/serve.ts +0 -75
  353. package/src/session-controller.ts +0 -398
  354. package/src/syntax.test.ts +0 -41
  355. package/src/syntax.ts +0 -88
  356. package/src/theme.ts +0 -46
  357. package/src/view-diff.test.ts +0 -55
  358. package/src/view-diff.ts +0 -86
  359. package/src/view.test.ts +0 -157
  360. package/src/view.ts +0 -364
@@ -0,0 +1,729 @@
1
+ /**
2
+ * The intent reducer in isolation: with the App component's dependencies
3
+ * mocked, each Intent maps to exactly the controller call or state update it
4
+ * should - the navigation clamps, the annotation wrap-around, the message
5
+ * cycle, and the submit-reveal fix, none of which needed a rendered TUI.
6
+ */
7
+
8
+ import { describe, expect, mock, test } from "bun:test";
9
+ import type { SetStateAction } from "react";
10
+ import { SCHEMA_VERSION, type Annotation, type Thread } from "@cueloop/schema";
11
+ import { createIntentDispatch, type IntentDispatchDeps } from "./intent-dispatch";
12
+ import type {
13
+ ControllerSnapshot,
14
+ CurationItem,
15
+ ReviewController,
16
+ } from "../thread/thread-controller";
17
+ import type { DisplayBlock } from "../markdown/view-plan";
18
+ import type { TreeRow } from "../thread/tree-view";
19
+
20
+ function block(text: string): DisplayBlock {
21
+ return { type: "same", kind: "p", work: { kind: "p", text, lineStart: 0, lineEnd: 0 } };
22
+ }
23
+
24
+ function annotation(id: string, overrides: Partial<Annotation> = {}): Annotation {
25
+ return {
26
+ id,
27
+ kind: "comment",
28
+ anchor: { quote: "", prefix: "", suffix: "" },
29
+ body: "",
30
+ createdAt: "2026-01-01T00:00:00.000Z",
31
+ ...overrides,
32
+ };
33
+ }
34
+
35
+ function sessionWith(annotations: Annotation[], overrides: Partial<Thread> = {}): Thread {
36
+ return {
37
+ schemaVersion: SCHEMA_VERSION,
38
+ id: "ses_1",
39
+ workspace: { repoRoot: "/repo", branch: "main" },
40
+ artifact: { type: "plan", content: "# Plan\n", meta: {} },
41
+ revisions: [],
42
+ annotations,
43
+ message: null,
44
+ status: "pending",
45
+ createdAt: "2026-01-01T00:00:00.000Z",
46
+ ...overrides,
47
+ };
48
+ }
49
+
50
+ const EMPTY_SNAPSHOT: ControllerSnapshot = {
51
+ session: null,
52
+ inbox: null,
53
+ status: "",
54
+ toast: null,
55
+ error: null,
56
+ completion: { phase: "idle" },
57
+ editOrphanCount: 0,
58
+ walk: null,
59
+ };
60
+
61
+ /** A controller where every primitive is a mock; annotate returns undefined by default. */
62
+ function baseController(): ReviewController {
63
+ return {
64
+ readOnly: false,
65
+ createEmptyThread: mock(async () => {}),
66
+ subscribe: mock(() => () => {}),
67
+ getSnapshot: mock(() => EMPTY_SNAPSHOT),
68
+ connect: mock(),
69
+ close: mock(),
70
+ applyConfig: mock(),
71
+ setStatus: mock(),
72
+ showToast: mock(),
73
+ dismissToast: mock(),
74
+ display: mock(() => []),
75
+ rows: mock(() => []),
76
+ setFileCollapsed: mock(),
77
+ isFileCollapsed: mock(() => false),
78
+ setFileExpanded: mock(),
79
+ isFileExpanded: mock(() => false),
80
+ canExpandFile: mock(() => false),
81
+ copyFilePath: mock(),
82
+ fileStats: mock(() => new Map()),
83
+ files: mock(() => []),
84
+ working: mock(() => ""),
85
+ projectFiles: mock(() => Promise.resolve<string[]>([])),
86
+ readFile: mock(() => Promise.resolve<string | null>(null)),
87
+ repoFiles: mock(() => Promise.resolve<string[]>([])),
88
+ repoReadFile: mock(() => Promise.resolve<string | null>(null)),
89
+ repoChanges: mock(() => Promise.resolve([])),
90
+ refreshDiff: mock(() => Promise.resolve()),
91
+ open: mock(),
92
+ deleteSession: mock(),
93
+ renameSession: mock(),
94
+ setSelfName: mock(),
95
+ cut: mock(),
96
+ toggleRejectHunk: mock(),
97
+ toggleRejectChange: mock(),
98
+ rejectedRows: mock(() => new Set<number>()),
99
+ curationItems: mock(() => []),
100
+ restoreCuration: mock(),
101
+ edit: mock(),
102
+ saveEditedBody: mock(),
103
+ annotate: mock(() => undefined),
104
+ addComment: mock(() => undefined),
105
+ commentOnWorkbench: mock(() => Promise.resolve()),
106
+ commentOnWorkbenchDiff: mock(() => Promise.resolve()),
107
+ reply: mock(() => undefined),
108
+ annotatePrototype: mock(() => undefined),
109
+ updateAnnotation: mock(),
110
+ removeAnnotation: mock(),
111
+ setWorkingCopy: mock(),
112
+ walkStart: mock(),
113
+ walkForward: mock(),
114
+ walkBack: mock(),
115
+ walkLeave: mock(),
116
+ submit: mock(),
117
+ share: mock(),
118
+ unshare: mock(),
119
+ shareLinks: mock(() => []),
120
+ createShareLink: mock(),
121
+ updateShareLink: mock(),
122
+ deleteShareLink: mock(),
123
+ copyShareLink: mock(),
124
+ treeRows: mock(() => []),
125
+ goToEntry: mock(),
126
+ branch: mock(),
127
+ labelTip: mock(),
128
+ fork: mock(),
129
+ forkAndShare: mock(),
130
+ pullShared: mock(() => Promise.resolve()),
131
+ startShareSync: mock(() => () => {}),
132
+ finishReview: mock(),
133
+ dismissCompletion: mock(),
134
+ optInAutoClose: mock(),
135
+ };
136
+ }
137
+
138
+ /** A deps bag where every effect is a mock and every read has a plain default. */
139
+ function makeDeps(overrides: Partial<IntentDispatchDeps> = {}): IntentDispatchDeps {
140
+ return {
141
+ controller: baseController(),
142
+ onExit: mock(),
143
+ isDiff: false,
144
+ display: [],
145
+ rows: [],
146
+ cursor: 0,
147
+ inbox: null,
148
+ inboxCursor: 0,
149
+ mode: { type: "normal" },
150
+ session: null,
151
+ defaultMessage: "approved",
152
+ focusedAnnotationId: undefined,
153
+ selectedCurationId: undefined,
154
+ railTab: "review",
155
+ selectedEntryId: undefined,
156
+ authorNames: {},
157
+ quickActions: [],
158
+ renameAuthor: mock(),
159
+ renameThread: mock(),
160
+ setLocalIdentityName: mock(),
161
+ liveInput: { current: "" },
162
+ setCursor: mock(),
163
+ setInboxCursor: mock(),
164
+ setMode: mock(),
165
+ setRailTab: mock(),
166
+ setSelectedEntryId: mock(),
167
+ setFocusedAnnotationId: mock(),
168
+ setSelectedCurationId: mock(),
169
+ setPulsedAnnotationId: mock(),
170
+ selectCardFromDocument: mock(),
171
+ openBodyEditor: mock(),
172
+ openCardEdit: mock(),
173
+ toggleDiffView: mock(),
174
+ openShareDialog: mock(),
175
+ ...overrides,
176
+ };
177
+ }
178
+
179
+ function isUpdater(action: SetStateAction<number>): action is (current: number) => number {
180
+ return typeof action === "function";
181
+ }
182
+
183
+ describe("move", () => {
184
+ test("down clamps at the last row and up clamps at the first", () => {
185
+ // Arrange
186
+ const setCursor = mock((_action: SetStateAction<number>) => {});
187
+ const deps = makeDeps({ display: [block("a"), block("b"), block("c")], setCursor });
188
+ const dispatch = createIntentDispatch(deps);
189
+
190
+ // Act
191
+ dispatch({ type: "move", to: "down" });
192
+
193
+ // Assert
194
+ const advance = setCursor.mock.calls[0]![0];
195
+
196
+ if (!isUpdater(advance)) throw new Error("expected setCursor to receive an updater");
197
+
198
+ expect(advance(2)).toBe(2); // already at the end, stays
199
+ expect(advance(0)).toBe(1);
200
+ });
201
+
202
+ test("bottom jumps to the last navigable row", () => {
203
+ // Arrange
204
+ const deps = makeDeps({ display: [block("a"), block("b"), block("c")] });
205
+ const dispatch = createIntentDispatch(deps);
206
+
207
+ // Act
208
+ dispatch({ type: "move", to: "bottom" });
209
+
210
+ // Assert
211
+ expect(deps.setCursor).toHaveBeenCalledWith(2);
212
+ });
213
+ });
214
+
215
+ describe("annotation navigation", () => {
216
+ test("nextAnnotation wraps from the last annotation back to the first", () => {
217
+ // Arrange
218
+ const deps = makeDeps({
219
+ session: sessionWith([annotation("a1"), annotation("a2"), annotation("a3")]),
220
+ focusedAnnotationId: "a3",
221
+ });
222
+ const dispatch = createIntentDispatch(deps);
223
+
224
+ // Act
225
+ dispatch({ type: "nextAnnotation" });
226
+
227
+ // Assert
228
+ expect(deps.selectCardFromDocument).toHaveBeenCalledWith("a1");
229
+ });
230
+
231
+ test("prevAnnotation wraps from the first annotation to the last", () => {
232
+ // Arrange
233
+ const deps = makeDeps({
234
+ session: sessionWith([annotation("a1"), annotation("a2"), annotation("a3")]),
235
+ focusedAnnotationId: "a1",
236
+ });
237
+ const dispatch = createIntentDispatch(deps);
238
+
239
+ // Act
240
+ dispatch({ type: "prevAnnotation" });
241
+
242
+ // Assert
243
+ expect(deps.selectCardFromDocument).toHaveBeenCalledWith("a3");
244
+ });
245
+
246
+ test("cycling skips annotations a revision already addressed", () => {
247
+ // Arrange: a2 is addressed, so next from a1 lands on a3
248
+ const session = sessionWith([
249
+ annotation("a1"),
250
+ annotation("a2", { resolution: { revision: 2, source: "agent" } }),
251
+ annotation("a3"),
252
+ ]);
253
+ const deps = makeDeps({ session, focusedAnnotationId: "a1" });
254
+ const dispatch = createIntentDispatch(deps);
255
+
256
+ // Act
257
+ dispatch({ type: "nextAnnotation" });
258
+
259
+ // Assert
260
+ expect(deps.selectCardFromDocument).toHaveBeenCalledWith("a3");
261
+ });
262
+ });
263
+
264
+ describe("cycleMessage", () => {
265
+ test("advances through the message list in the submit overlay", () => {
266
+ // Arrange
267
+ const deps = makeDeps({ mode: { type: "submit", message: "approved", summary: "" } });
268
+ const dispatch = createIntentDispatch(deps);
269
+
270
+ // Act
271
+ dispatch({ type: "cycleMessage", direction: 1 });
272
+
273
+ // Assert
274
+ expect(deps.setMode).toHaveBeenCalledWith({
275
+ type: "submit",
276
+ message: "changes_requested",
277
+ summary: "",
278
+ });
279
+ });
280
+ });
281
+
282
+ describe("marker-actions popover", () => {
283
+ const span = { displayIndex: 3, wordIndex: 0, wordEnd: 0, start: 2, end: 9 };
284
+
285
+ test("spanCut cuts only the marked character range and closes the popover", () => {
286
+ // Arrange
287
+ const deps = makeDeps({ mode: { type: "span", span } });
288
+ const dispatch = createIntentDispatch(deps);
289
+
290
+ // Act
291
+ dispatch({ type: "spanCut" });
292
+
293
+ // Assert
294
+ expect(deps.controller.cut).toHaveBeenCalledWith(3, 2, 9);
295
+ expect(deps.setMode).toHaveBeenCalledWith({ type: "normal" });
296
+ });
297
+
298
+ test("openSpanActions enters the list at index 0", () => {
299
+ // Arrange
300
+ const deps = makeDeps({ mode: { type: "span", span } });
301
+ const dispatch = createIntentDispatch(deps);
302
+
303
+ // Act
304
+ dispatch({ type: "openSpanActions" });
305
+
306
+ // Assert
307
+ expect(deps.setMode).toHaveBeenCalledWith({ type: "spanActions", span, index: 0 });
308
+ });
309
+
310
+ test("moveSpanAction clamps within the actions list", () => {
311
+ // Arrange
312
+ const deps = makeDeps({
313
+ mode: { type: "spanActions", span, index: 1 },
314
+ quickActions: [{ prompt: "one" }, { prompt: "two" }],
315
+ });
316
+ const dispatch = createIntentDispatch(deps);
317
+
318
+ // Act
319
+ dispatch({ type: "moveSpanAction", direction: 1 });
320
+
321
+ // Assert - already at the last row, so the index holds
322
+ expect(deps.setMode).toHaveBeenCalledWith({ type: "spanActions", span, index: 1 });
323
+ });
324
+
325
+ test("pickSpanAction inserts the preset comment and returns to normal", () => {
326
+ // Arrange
327
+ const annotate = mock(() => "an_new");
328
+ const deps = makeDeps({
329
+ mode: { type: "spanActions", span, index: 1 },
330
+ session: sessionWith([]),
331
+ quickActions: [{ prompt: "Needs a test" }, { prompt: "YAGNI", metadata: "cut scope" }],
332
+ controller: { ...baseController(), annotate },
333
+ });
334
+ const dispatch = createIntentDispatch(deps);
335
+
336
+ // Act
337
+ dispatch({ type: "pickSpanAction" });
338
+
339
+ // Assert - metadata joins the prompt with a blank line
340
+ expect(annotate).toHaveBeenCalledWith("comment", 3, 2, 9, "YAGNI\n\ncut scope");
341
+ expect(deps.setFocusedAnnotationId).toHaveBeenCalledWith("an_new");
342
+ expect(deps.setMode).toHaveBeenCalledWith({ type: "normal" });
343
+ });
344
+
345
+ test("pickSpanAction honors an explicit index (mouse click)", () => {
346
+ // Arrange
347
+ const annotate = mock(() => "an_new");
348
+ const deps = makeDeps({
349
+ mode: { type: "spanActions", span, index: 0 },
350
+ session: sessionWith([]),
351
+ quickActions: [{ prompt: "Needs a test" }, { prompt: "Extract the duplication" }],
352
+ controller: { ...baseController(), annotate },
353
+ });
354
+ const dispatch = createIntentDispatch(deps);
355
+
356
+ // Act
357
+ dispatch({ type: "pickSpanAction", index: 1 });
358
+
359
+ // Assert
360
+ expect(annotate).toHaveBeenCalledWith("comment", 3, 2, 9, "Extract the duplication");
361
+ });
362
+
363
+ test("closeSpanActions returns to the span toolbar", () => {
364
+ // Arrange
365
+ const deps = makeDeps({ mode: { type: "spanActions", span, index: 2 } });
366
+ const dispatch = createIntentDispatch(deps);
367
+
368
+ // Act
369
+ dispatch({ type: "closeSpanActions" });
370
+
371
+ // Assert
372
+ expect(deps.setMode).toHaveBeenCalledWith({ type: "span", span });
373
+ });
374
+ });
375
+
376
+ describe("openSubmit", () => {
377
+ test("opens the submit confirm with the default message", () => {
378
+ // Arrange
379
+ const deps = makeDeps({ session: sessionWith([]) });
380
+ const dispatch = createIntentDispatch(deps);
381
+
382
+ // Act
383
+ dispatch({ type: "openSubmit" });
384
+
385
+ // Assert
386
+ expect(deps.setMode).toHaveBeenCalledWith({ type: "submit", message: "approved", summary: "" });
387
+ });
388
+
389
+ test("does nothing without a session", () => {
390
+ // Arrange
391
+ const deps = makeDeps({ session: null });
392
+ const dispatch = createIntentDispatch(deps);
393
+
394
+ // Act
395
+ dispatch({ type: "openSubmit" });
396
+
397
+ // Assert
398
+ expect(deps.setMode).not.toHaveBeenCalled();
399
+ });
400
+ });
401
+
402
+ describe("share", () => {
403
+ test("opens the share dialog instead of publishing directly", () => {
404
+ // Arrange
405
+ const deps = makeDeps();
406
+ const dispatch = createIntentDispatch(deps);
407
+
408
+ // Act
409
+ dispatch({ type: "share" });
410
+
411
+ // Assert
412
+ expect(deps.openShareDialog).toHaveBeenCalled();
413
+ expect(deps.controller.share).not.toHaveBeenCalled();
414
+ });
415
+ });
416
+
417
+ describe("restoreCuration", () => {
418
+ const items: CurationItem[] = [
419
+ { id: "diff:f#0#hunk", source: "diff", preview: [], revealIndex: 1 },
420
+ { id: "diff:f#1#2", source: "diff", preview: [], revealIndex: 9 },
421
+ ];
422
+
423
+ test("restores the selected item and clears the selection", () => {
424
+ // Arrange
425
+ const controller = { ...baseController(), curationItems: mock(() => items) };
426
+ const deps = makeDeps({ selectedCurationId: "diff:f#1#2", controller });
427
+ const dispatch = createIntentDispatch(deps);
428
+
429
+ // Act
430
+ dispatch({ type: "restoreCuration" });
431
+
432
+ // Assert
433
+ expect(deps.controller.restoreCuration).toHaveBeenCalledWith("diff:f#1#2");
434
+ expect(deps.setSelectedCurationId).toHaveBeenCalledWith(undefined);
435
+ });
436
+
437
+ test("with nothing selected, undoes the last removal", () => {
438
+ // Arrange
439
+ const controller = { ...baseController(), curationItems: mock(() => items) };
440
+ const deps = makeDeps({ selectedCurationId: undefined, controller });
441
+ const dispatch = createIntentDispatch(deps);
442
+
443
+ // Act
444
+ dispatch({ type: "restoreCuration" });
445
+
446
+ // Assert
447
+ expect(deps.controller.restoreCuration).toHaveBeenCalledWith("diff:f#1#2");
448
+ });
449
+
450
+ test("does nothing when there is nothing curated out", () => {
451
+ // Arrange
452
+ const deps = makeDeps();
453
+ const dispatch = createIntentDispatch(deps);
454
+
455
+ // Act
456
+ dispatch({ type: "restoreCuration" });
457
+
458
+ // Assert
459
+ expect(deps.controller.restoreCuration).not.toHaveBeenCalled();
460
+ });
461
+ });
462
+
463
+ describe("toggleDiffView", () => {
464
+ test("delegates to the App-owned toggle", () => {
465
+ // Arrange
466
+ const deps = makeDeps();
467
+ const dispatch = createIntentDispatch(deps);
468
+
469
+ // Act
470
+ dispatch({ type: "toggleDiffView" });
471
+
472
+ // Assert
473
+ expect(deps.toggleDiffView).toHaveBeenCalled();
474
+ });
475
+ });
476
+
477
+ describe("exit", () => {
478
+ test("calls the exit hook with 0", () => {
479
+ // Arrange
480
+ const deps = makeDeps();
481
+ const dispatch = createIntentDispatch(deps);
482
+
483
+ // Act
484
+ dispatch({ type: "exit" });
485
+
486
+ // Assert
487
+ expect(deps.onExit).toHaveBeenCalledWith(0);
488
+ });
489
+ });
490
+
491
+ describe("inbox delete", () => {
492
+ test("requestDeleteSession opens the confirm on the cursor row", () => {
493
+ // Arrange
494
+ const inbox = [
495
+ sessionWith([], { artifact: { type: "plan", content: "", meta: { title: "Plan A" } } }),
496
+ ];
497
+ const deps = makeDeps({ inbox, inboxCursor: 0 });
498
+
499
+ // Act
500
+ createIntentDispatch(deps)({ type: "requestDeleteSession" });
501
+
502
+ // Assert
503
+ expect(deps.setMode).toHaveBeenCalledWith({
504
+ type: "confirmDelete",
505
+ sessionId: "ses_1",
506
+ title: "Plan A",
507
+ });
508
+ });
509
+
510
+ test("confirmDialog deletes the session and closes", () => {
511
+ // Arrange
512
+ const deps = makeDeps({ mode: { type: "confirmDelete", sessionId: "ses_1", title: "Plan A" } });
513
+
514
+ // Act
515
+ createIntentDispatch(deps)({ type: "confirmDialog" });
516
+
517
+ // Assert
518
+ expect(deps.controller.deleteSession).toHaveBeenCalledWith("ses_1");
519
+ expect(deps.setMode).toHaveBeenCalledWith({ type: "normal" });
520
+ });
521
+ });
522
+
523
+ describe("rename author", () => {
524
+ test("openRename seeds the prompt with the author's current local name", () => {
525
+ // Arrange
526
+ const session = sessionWith([annotation("a1", { author: "SHA256:x" })]);
527
+ const deps = makeDeps({
528
+ session,
529
+ focusedAnnotationId: "a1",
530
+ authorNames: { "SHA256:x": "Alex" },
531
+ });
532
+
533
+ // Act
534
+ createIntentDispatch(deps)({ type: "openRename" });
535
+
536
+ // Assert
537
+ expect(deps.setMode).toHaveBeenCalledWith({
538
+ type: "rename",
539
+ authorId: "SHA256:x",
540
+ text: "Alex",
541
+ });
542
+ });
543
+
544
+ test("openRename on your own note does nothing but explain", () => {
545
+ // Arrange
546
+ const session = sessionWith([annotation("a1")]);
547
+ const deps = makeDeps({ session, focusedAnnotationId: "a1" });
548
+
549
+ // Act
550
+ createIntentDispatch(deps)({ type: "openRename" });
551
+
552
+ // Assert
553
+ expect(deps.controller.setStatus).toHaveBeenCalled();
554
+ expect(deps.setMode).not.toHaveBeenCalled();
555
+ });
556
+
557
+ test("confirmDialog in rename mode persists the trimmed name and closes", () => {
558
+ // Arrange
559
+ const deps = makeDeps({ mode: { type: "rename", authorId: "SHA256:x", text: " Alex " } });
560
+
561
+ // Act
562
+ createIntentDispatch(deps)({ type: "confirmDialog" });
563
+
564
+ // Assert
565
+ expect(deps.renameAuthor).toHaveBeenCalledWith("SHA256:x", "Alex");
566
+ expect(deps.setMode).toHaveBeenCalledWith({ type: "normal" });
567
+ });
568
+
569
+ test("confirmDialog in renameSelf mode sets the trimmed local identity name and closes", () => {
570
+ // Arrange
571
+ const deps = makeDeps({ mode: { type: "renameSelf", text: " Robin " } });
572
+
573
+ // Act
574
+ createIntentDispatch(deps)({ type: "confirmDialog" });
575
+
576
+ // Assert
577
+ expect(deps.setLocalIdentityName).toHaveBeenCalledWith("Robin");
578
+ expect(deps.setMode).toHaveBeenCalledWith({ type: "normal" });
579
+ });
580
+ });
581
+
582
+ describe("collaborator self-name", () => {
583
+ test("confirmDialog in nameSelf mode records the trimmed name and closes", () => {
584
+ // Arrange
585
+ const deps = makeDeps({ mode: { type: "nameSelf", text: " Robin " } });
586
+
587
+ // Act
588
+ createIntentDispatch(deps)({ type: "confirmDialog" });
589
+
590
+ // Assert
591
+ expect(deps.controller.setSelfName).toHaveBeenCalledWith("Robin");
592
+ expect(deps.setMode).toHaveBeenCalledWith({ type: "normal" });
593
+ });
594
+
595
+ test("an empty name still closes - the collaborator stays anonymous", () => {
596
+ // Arrange
597
+ const deps = makeDeps({ mode: { type: "nameSelf", text: " " } });
598
+
599
+ // Act
600
+ createIntentDispatch(deps)({ type: "confirmDialog" });
601
+
602
+ // Assert
603
+ expect(deps.controller.setSelfName).toHaveBeenCalledWith("");
604
+ expect(deps.setMode).toHaveBeenCalledWith({ type: "normal" });
605
+ });
606
+ });
607
+
608
+ describe("tree intents", () => {
609
+ const rows: TreeRow[] = [
610
+ {
611
+ entryId: "e1",
612
+ depth: 0,
613
+ glyph: "◉",
614
+ text: "revision 1",
615
+ onPath: true,
616
+ tips: [],
617
+ isCurrentTip: false,
618
+ },
619
+ {
620
+ entryId: "e2",
621
+ depth: 0,
622
+ glyph: "·",
623
+ text: "comment",
624
+ onPath: true,
625
+ tips: ["main"],
626
+ isCurrentTip: false,
627
+ },
628
+ {
629
+ entryId: "e3",
630
+ depth: 1,
631
+ glyph: "·",
632
+ text: "comment",
633
+ onPath: false,
634
+ tips: ["alt"],
635
+ isCurrentTip: true,
636
+ },
637
+ ];
638
+
639
+ function treeDeps(overrides: Partial<IntentDispatchDeps> = {}): IntentDispatchDeps {
640
+ const deps = makeDeps({ railTab: "tree", ...overrides });
641
+
642
+ deps.controller.treeRows = mock(() => rows);
643
+
644
+ return deps;
645
+ }
646
+
647
+ test("toggleTree shows the Tree tab and hides it again", () => {
648
+ // Arrange
649
+ const shown = makeDeps({ railTab: "review" });
650
+ const hidden = makeDeps({ railTab: "tree" });
651
+
652
+ // Act
653
+ createIntentDispatch(shown)({ type: "toggleTree" });
654
+ createIntentDispatch(hidden)({ type: "toggleTree" });
655
+
656
+ // Assert
657
+ expect(shown.setRailTab).toHaveBeenCalledWith("tree");
658
+ expect(hidden.setRailTab).toHaveBeenCalledWith("review");
659
+ });
660
+
661
+ test("treeMove starts from the current tip and stays inside the rows", () => {
662
+ // Arrange
663
+ const fromTip = treeDeps();
664
+ const atTop = treeDeps({ selectedEntryId: "e1" });
665
+
666
+ // Act
667
+ createIntentDispatch(fromTip)({ type: "treeMove", direction: -1 });
668
+ createIntentDispatch(atTop)({ type: "treeMove", direction: -1 });
669
+
670
+ // Assert
671
+ expect(fromTip.setSelectedEntryId).toHaveBeenCalledWith("e2");
672
+ expect(atTop.setSelectedEntryId).toHaveBeenCalledWith("e1");
673
+ });
674
+
675
+ test("treeGo: the tip answers, a tip of another branch switches, an inner entry asks for a summary", () => {
676
+ // Arrange
677
+ const atTip = treeDeps({ selectedEntryId: "e3" });
678
+ const toTip = treeDeps({ selectedEntryId: "e2" });
679
+ const inner = treeDeps({ selectedEntryId: "e1" });
680
+
681
+ // Act
682
+ createIntentDispatch(atTip)({ type: "treeGo" });
683
+ createIntentDispatch(toTip)({ type: "treeGo" });
684
+ createIntentDispatch(inner)({ type: "treeGo" });
685
+
686
+ // Assert
687
+ expect(atTip.controller.setStatus).toHaveBeenCalledWith("already at the tip");
688
+ expect(toTip.controller.goToEntry).toHaveBeenCalledWith("e2");
689
+ expect(inner.setMode).toHaveBeenCalledWith({
690
+ type: "treePrompt",
691
+ ask: "navigate",
692
+ entryId: "e1",
693
+ text: "",
694
+ });
695
+ });
696
+
697
+ test("the prompts confirm into branch, label, and a move back with the typed summary", () => {
698
+ // Arrange
699
+ const branch = treeDeps({ mode: { type: "treePrompt", ask: "branch", text: "alt" } });
700
+ const label = treeDeps({ mode: { type: "treePrompt", ask: "label", text: "start" } });
701
+ const navigate = treeDeps({
702
+ mode: { type: "treePrompt", ask: "navigate", entryId: "e1", text: " too early " },
703
+ });
704
+
705
+ // Act
706
+ createIntentDispatch(branch)({ type: "confirmDialog" });
707
+ createIntentDispatch(label)({ type: "confirmDialog" });
708
+ createIntentDispatch(navigate)({ type: "confirmDialog" });
709
+
710
+ // Assert
711
+ expect(branch.controller.branch).toHaveBeenCalledWith("alt");
712
+ expect(label.controller.labelTip).toHaveBeenCalledWith("start");
713
+ expect(navigate.controller.goToEntry).toHaveBeenCalledWith("e1", "too early");
714
+ expect(navigate.setMode).toHaveBeenCalledWith({ type: "normal" });
715
+ });
716
+
717
+ test("treeFork and treeForkShare reach the controller", () => {
718
+ // Arrange
719
+ const deps = treeDeps();
720
+
721
+ // Act
722
+ createIntentDispatch(deps)({ type: "treeFork" });
723
+ createIntentDispatch(deps)({ type: "treeForkShare" });
724
+
725
+ // Assert
726
+ expect(deps.controller.fork).toHaveBeenCalledTimes(1);
727
+ expect(deps.controller.forkAndShare).toHaveBeenCalledTimes(1);
728
+ });
729
+ });