@cueloop/client 0.1.0-alpha.8 → 0.1.0-alpha.81

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