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

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