@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
@@ -1,8 +1,4 @@
1
- /**
2
- * The keyboard grammar as data: every mode x key produces a known intent
3
- * list, the read-only rule is one gate, and plan and diff reviews share one
4
- * path for annotation navigation, deletion, and submit.
5
- */
1
+ /** The keyboard grammar as data: every mode x key maps to a known intent list, gated once by the read-only rule. */
6
2
 
7
3
  import { describe, expect, test } from "bun:test";
8
4
  import { DEFAULT_KEYS } from "./config";
@@ -20,217 +16,512 @@ function state(patch: Partial<KeyState> = {}): KeyState {
20
16
  annotationCount: 1,
21
17
  hasFocusedAnnotation: true,
22
18
  cursorAnnotatable: true,
19
+ walkAtEnd: false,
23
20
  ...patch,
24
21
  };
25
22
  }
26
23
 
27
- function key(name: string, shift = false): { name: string; shift: boolean } {
28
- return { name, shift };
24
+ function key(name: string, shift = false, meta = false) {
25
+ return { name, shift, meta };
29
26
  }
30
27
 
31
28
  describe("compose overlay", () => {
32
- const s = state({ overlay: "compose" });
29
+ // Arrange
30
+ const keyState = state({ overlay: "compose" });
33
31
  const table: [string, Intent[]][] = [
34
- ["escape", [{ t: "closeOverlay" }]],
35
- ["return", [{ t: "saveCompose" }]],
36
- ["enter", [{ t: "saveCompose" }]],
32
+ ["escape", [{ type: "closeOverlay" }]],
33
+ ["return", [{ type: "saveCompose" }]],
34
+ ["enter", [{ type: "saveCompose" }]],
37
35
  ["left", []],
38
36
  ["right", []],
39
37
  ["j", []],
40
38
  ["q", []],
41
39
  ];
40
+
42
41
  for (const [name, expected] of table) {
43
42
  test(`${name} -> ${JSON.stringify(expected)}`, () => {
44
- expect(reduceKey(s, key(name))).toEqual(expected);
43
+ expect(reduceKey(keyState, key(name))).toEqual(expected);
45
44
  });
46
45
  }
46
+
47
+ // ⌥/Alt+⏎ (meta) and shift+⏎ insert a newline in the composer, so the
48
+ // grammar must not save on them; only a bare ⏎ saves.
49
+ test("option/alt+return (meta) does not save - the textarea inserts a newline", () => {
50
+ expect(reduceKey(keyState, key("return", false, true))).toEqual([]);
51
+ });
52
+ test("shift+return does not save - the textarea inserts a newline", () => {
53
+ expect(reduceKey(keyState, key("return", true, false))).toEqual([]);
54
+ });
47
55
  });
48
56
 
49
57
  describe("submit overlay", () => {
50
- const s = state({ overlay: "submit" });
58
+ // Arrange
59
+ const keyState = state({ overlay: "submit" });
51
60
  const table: [string, Intent[]][] = [
52
- ["escape", [{ t: "closeOverlay" }]],
53
- ["return", [{ t: "submitVerdict" }]],
54
- ["enter", [{ t: "submitVerdict" }]],
55
- ["left", [{ t: "cycleVerdict", dir: -1 }]],
56
- ["right", [{ t: "cycleVerdict", dir: 1 }]],
61
+ ["escape", [{ type: "closeOverlay" }]],
62
+ ["return", []],
63
+ ["enter", []],
64
+ ["left", [{ type: "cycleMessage", direction: -1 }]],
65
+ ["right", [{ type: "cycleMessage", direction: 1 }]],
57
66
  ["j", []],
58
67
  ["q", []],
59
68
  ];
69
+
60
70
  for (const [name, expected] of table) {
61
71
  test(`${name} -> ${JSON.stringify(expected)}`, () => {
62
- expect(reduceKey(s, key(name))).toEqual(expected);
72
+ expect(reduceKey(keyState, key(name))).toEqual(expected);
63
73
  });
64
74
  }
65
75
  });
66
76
 
67
77
  describe("completion overlay", () => {
78
+ // Arrange
68
79
  const prompt = state({ overlay: "completion-prompt" });
69
80
  const counting = state({ overlay: "completion-counting" });
70
81
  const table: [KeyState, string, Intent[]][] = [
71
- [prompt, "return", [{ t: "finishReview" }]],
72
- [prompt, "enter", [{ t: "finishReview" }]],
73
- [prompt, "q", [{ t: "finishReview" }]],
74
- [prompt, "a", [{ t: "optInAutoClose" }]],
75
- [prompt, "escape", [{ t: "dismissCompletion" }]],
82
+ [prompt, "return", [{ type: "finishReview" }]],
83
+ [prompt, "enter", [{ type: "finishReview" }]],
84
+ [prompt, "q", [{ type: "finishReview" }]],
85
+ [prompt, "a", [{ type: "optInAutoClose" }]],
86
+ [prompt, "escape", [{ type: "dismissCompletion" }]],
76
87
  [prompt, "j", []],
77
- [counting, "return", [{ t: "finishReview" }]],
78
- [counting, "q", [{ t: "finishReview" }]],
79
- [counting, "a", []],
80
- [counting, "escape", [{ t: "dismissCompletion" }]],
88
+ [counting, "return", [{ type: "finishReview" }]],
89
+ [counting, "q", [{ type: "finishReview" }]],
90
+ [counting, "a", [{ type: "optInAutoClose" }]],
91
+ [counting, "escape", [{ type: "dismissCompletion" }]],
81
92
  ];
82
- for (const [s, name, expected] of table) {
83
- test(`${s.overlay} ${name} -> ${JSON.stringify(expected)}`, () => {
84
- expect(reduceKey(s, key(name))).toEqual(expected);
93
+
94
+ for (const [overlayState, name, expected] of table) {
95
+ test(`${overlayState.overlay} ${name} -> ${JSON.stringify(expected)}`, () => {
96
+ expect(reduceKey(overlayState, key(name))).toEqual(expected);
85
97
  });
86
98
  }
87
99
  });
88
100
 
89
101
  describe("inbox mode", () => {
90
- const s = state({ view: "inbox" });
102
+ // Arrange
103
+ const keyState = state({ view: "inbox" });
91
104
  const table: [string, Intent[]][] = [
92
- ["j", [{ t: "inboxMove", to: "down" }]],
93
- ["down", [{ t: "inboxMove", to: "down" }]],
94
- ["k", [{ t: "inboxMove", to: "up" }]],
95
- ["up", [{ t: "inboxMove", to: "up" }]],
96
- ["return", [{ t: "openSession" }]],
97
- ["enter", [{ t: "openSession" }]],
98
- ["q", [{ t: "exit" }]],
105
+ ["j", [{ type: "inboxMove", to: "down" }]],
106
+ ["down", [{ type: "inboxMove", to: "down" }]],
107
+ ["k", [{ type: "inboxMove", to: "up" }]],
108
+ ["up", [{ type: "inboxMove", to: "up" }]],
109
+ ["return", [{ type: "openSession" }]],
110
+ ["enter", [{ type: "openSession" }]],
111
+ ["q", [{ type: "exit" }]],
99
112
  ["c", []],
100
113
  ["escape", []],
101
114
  ];
115
+
102
116
  for (const [name, expected] of table) {
103
117
  test(`${name} -> ${JSON.stringify(expected)}`, () => {
104
- expect(reduceKey(s, key(name))).toEqual(expected);
118
+ expect(reduceKey(keyState, key(name))).toEqual(expected);
105
119
  });
106
120
  }
107
121
 
108
122
  test("an empty inbox swallows every non-quit key", () => {
123
+ // Arrange
109
124
  const empty = state({ view: "inbox", hasInboxItems: false });
125
+
126
+ // Assert
110
127
  for (const name of ["j", "k", "down", "up", "return", "enter"]) {
111
128
  expect(reduceKey(empty, key(name))).toEqual([]);
112
129
  }
113
- expect(reduceKey(empty, key("q"))).toEqual([{ t: "exit" }]);
130
+ expect(reduceKey(empty, key("q"))).toEqual([{ type: "exit" }]);
131
+ });
132
+ });
133
+
134
+ describe("collaborator (share) capabilities", () => {
135
+ test("can annotate: comment opens the composer", () => {
136
+ // Arrange
137
+ const collab = state({ canEditPlan: false, canSubmitMessage: false });
138
+
139
+ // Act / Assert
140
+ expect(reduceKey(collab, key("c"), "comment")).toEqual([
141
+ { type: "openCompose", kind: "comment", from: "cursor" },
142
+ ]);
143
+ });
144
+
145
+ test("can edit their own card: edit with a focused annotation rewrites it", () => {
146
+ // Arrange
147
+ const collab = state({ canEditPlan: false, hasFocusedAnnotation: true });
148
+
149
+ // Act / Assert
150
+ expect(reduceKey(collab, key("e"), "edit")).toEqual([{ type: "editCard" }]);
151
+ });
152
+
153
+ test("cannot edit the plan: cut/edit with no card is silent (no nag, no button)", () => {
154
+ // Arrange
155
+ const collab = state({ canEditPlan: false, hasFocusedAnnotation: false });
156
+
157
+ // Act / Assert
158
+ expect(reduceKey(collab, key("e"), "edit")).toEqual([]);
159
+ expect(reduceKey(collab, key("c"), "cut")).toEqual([]);
160
+ });
161
+
162
+ test("cannot submit a message: there is no agent on a share", () => {
163
+ // Arrange
164
+ const collab = state({ canSubmitMessage: false });
165
+
166
+ // Act / Assert
167
+ expect(reduceKey(collab, key("x"), "submit")).toEqual([
168
+ { type: "status", message: "shared view - your notes save as you go; q to leave" },
169
+ ]);
170
+ });
171
+
172
+ test("cannot re-share: only the owner shares", () => {
173
+ // Arrange
174
+ const collab = state({ canShare: false });
175
+
176
+ // Act / Assert
177
+ expect(reduceKey(collab, key("S", true), "share")).toEqual([
178
+ { type: "status", message: "only the plan owner can share" },
179
+ ]);
180
+ });
181
+ });
182
+
183
+ describe("share", () => {
184
+ test("the owner's share key publishes", () => {
185
+ // Act / Assert
186
+ expect(reduceKey(state(), key("S", true), "share")).toEqual([{ type: "share" }]);
187
+ });
188
+
189
+ test("an observer cannot share", () => {
190
+ // Act / Assert
191
+ expect(reduceKey(state({ readOnly: true }), key("S", true), "share")).toEqual([
192
+ { type: "status", message: "observer - read-only" },
193
+ ]);
114
194
  });
115
195
  });
116
196
 
117
197
  describe("plan normal mode", () => {
118
- const s = state();
198
+ // Arrange
199
+ const keyState = state();
119
200
  const table: [string, boolean, Intent[]][] = [
120
- ["j", false, [{ t: "move", to: "down" }]],
121
- ["down", false, [{ t: "move", to: "down" }]],
122
- ["k", false, [{ t: "move", to: "up" }]],
123
- ["up", false, [{ t: "move", to: "up" }]],
124
- ["g", false, [{ t: "move", to: "top" }]],
125
- ["g", true, [{ t: "move", to: "bottom" }]],
126
- ["v", false, [{ t: "startSpan" }]],
127
- ["c", false, [{ t: "openCompose", kind: "comment", from: "cursor" }]],
128
- ["s", false, [{ t: "openCompose", kind: "suggestion", from: "cursor" }]],
129
- ["x", false, [{ t: "cut" }]],
130
- ["e", false, [{ t: "edit" }]],
131
- ["n", false, [{ t: "nextAnn" }]],
132
- ["p", false, [{ t: "prevAnn" }]],
133
- ["backspace", false, [{ t: "removeAnnotation" }]],
134
- ["return", false, [{ t: "openSubmit" }]],
135
- ["q", false, [{ t: "exit" }]],
136
- ["escape", false, []],
201
+ ["j", false, [{ type: "move", to: "down" }]],
202
+ ["down", false, [{ type: "move", to: "down" }]],
203
+ ["k", false, [{ type: "move", to: "up" }]],
204
+ ["up", false, [{ type: "move", to: "up" }]],
205
+ ["g", false, [{ type: "move", to: "top" }]],
206
+ ["g", true, [{ type: "move", to: "bottom" }]],
207
+ ["v", false, [{ type: "startSpan" }]],
208
+ ["c", false, [{ type: "openCompose", kind: "comment", from: "cursor" }]],
209
+ // the default state has a selected card, so Cut and edit act on the card
210
+ ["x", false, [{ type: "removeAnnotation" }]],
211
+ ["e", false, [{ type: "editCard" }]],
212
+ ["n", false, [{ type: "nextAnnotation" }]],
213
+ ["p", false, [{ type: "prevAnnotation" }]],
214
+ ["backspace", false, [{ type: "removeAnnotation" }]],
215
+ ["return", false, [{ type: "openSubmit" }]],
216
+ ["q", false, [{ type: "exit" }]],
217
+ ["escape", false, [{ type: "deselect" }]],
137
218
  ["z", false, []],
138
219
  ];
220
+
139
221
  for (const [name, shift, expected] of table) {
140
222
  test(`${shift ? "shift+" : ""}${name} -> ${JSON.stringify(expected)}`, () => {
141
- expect(reduceKey(s, key(name, shift))).toEqual(expected);
223
+ expect(reduceKey(keyState, key(name, shift))).toEqual(expected);
142
224
  });
143
225
  }
144
226
 
145
- test("resolved sessions guard the mutating verbs", () => {
146
- const r = state({ resolved: true });
147
- for (const name of ["c", "s", "x", "e"]) {
148
- expect(reduceKey(r, key(name))).toEqual([{ t: "status", msg: "review submitted - read-only" }]);
227
+ test("resolved sessions guard the mutating primitives", () => {
228
+ // Arrange
229
+ const resolvedState = state({ resolved: true });
230
+
231
+ // Assert
232
+ for (const name of ["c", "x", "e"]) {
233
+ expect(reduceKey(resolvedState, key(name))).toEqual([
234
+ { type: "status", message: "review submitted - read-only" },
235
+ ]);
149
236
  }
150
- expect(reduceKey(r, key("return"))).toEqual([]);
151
- expect(reduceKey(r, key("backspace"))).toEqual([]);
237
+ expect(reduceKey(resolvedState, key("return"))).toEqual([]);
238
+ expect(reduceKey(resolvedState, key("backspace"))).toEqual([]);
239
+ });
240
+
241
+ test("u restores a rail removal (a cut block); a share viewer's is silent", () => {
242
+ // Assert - the undo intent leaves the target to the dispatcher
243
+ expect(reduceKey(state(), key("u"))).toEqual([{ type: "restoreCuration" }]);
244
+ expect(reduceKey(state({ resolved: true }), key("u"))).toEqual([
245
+ { type: "status", message: "review submitted - read-only" },
246
+ ]);
247
+ expect(reduceKey(state({ canEditPlan: false }), key("u"))).toEqual([]);
248
+ expect(reduceKey(state({ readOnly: true }), key("u"))).toEqual([
249
+ { type: "status", message: "observer - read-only" },
250
+ ]);
152
251
  });
153
252
 
154
253
  test("cut text cannot host a comment or a span", () => {
254
+ // Arrange
155
255
  const cut = state({ cursorAnnotatable: false });
156
- expect(reduceKey(cut, key("c"))).toEqual([{ t: "status", msg: "text is cut - restore it first" }]);
157
- expect(reduceKey(cut, key("s"))).toEqual([{ t: "status", msg: "text is cut - restore it first" }]);
256
+
257
+ // Assert
258
+ expect(reduceKey(cut, key("c"))).toEqual([
259
+ { type: "status", message: "text is cut - restore it first" },
260
+ ]);
158
261
  expect(reduceKey(cut, key("v"))).toEqual([]);
159
262
  });
160
263
 
161
264
  test("annotation navigation without annotations reports, delete without focus is silent", () => {
265
+ // Arrange
162
266
  const none = state({ annotationCount: 0, hasFocusedAnnotation: false });
163
- expect(reduceKey(none, key("n"))).toEqual([{ t: "status", msg: "no annotations" }]);
164
- expect(reduceKey(none, key("p"))).toEqual([{ t: "status", msg: "no annotations" }]);
267
+
268
+ // Assert
269
+ expect(reduceKey(none, key("n"))).toEqual([{ type: "status", message: "no annotations" }]);
270
+ expect(reduceKey(none, key("p"))).toEqual([{ type: "status", message: "no annotations" }]);
165
271
  expect(reduceKey(none, key("backspace"))).toEqual([]);
166
272
  });
273
+
274
+ test("without a selected card x cuts the block and e opens the editor", () => {
275
+ // Arrange
276
+ const unfocused = state({ hasFocusedAnnotation: false });
277
+
278
+ // Assert
279
+ expect(reduceKey(unfocused, key("x"))).toEqual([{ type: "cut" }]);
280
+ expect(reduceKey(unfocused, key("e"))).toEqual([{ type: "edit" }]);
281
+ });
167
282
  });
168
283
 
169
284
  describe("span mode", () => {
170
- const s = state({ spanMode: true });
285
+ // Arrange
286
+ const keyState = state({ spanMode: true });
171
287
  const table: [string, Intent[]][] = [
172
- ["l", [{ t: "spanKey", name: "l" }]],
173
- ["h", [{ t: "spanKey", name: "h" }]],
174
- ["w", [{ t: "spanKey", name: "w" }]],
175
- ["b", [{ t: "spanKey", name: "b" }]],
176
- ["$", [{ t: "spanKey", name: "$" }]],
177
- ["0", [{ t: "spanKey", name: "0" }]],
178
- ["c", [{ t: "openCompose", kind: "comment", from: "span" }]],
179
- ["s", [{ t: "openCompose", kind: "suggestion", from: "span" }]],
180
- ["escape", [{ t: "closeOverlay" }]],
181
- ["q", [{ t: "exit" }]],
288
+ ["l", [{ type: "spanKey", name: "l" }]],
289
+ ["h", [{ type: "spanKey", name: "h" }]],
290
+ ["w", [{ type: "spanKey", name: "w" }]],
291
+ ["b", [{ type: "spanKey", name: "b" }]],
292
+ ["$", [{ type: "spanKey", name: "$" }]],
293
+ ["0", [{ type: "spanKey", name: "0" }]],
294
+ ["c", [{ type: "openCompose", kind: "comment", from: "span" }]],
295
+ ["x", [{ type: "spanCut" }]],
296
+ ["a", [{ type: "openSpanActions" }]],
297
+ ["escape", [{ type: "closeOverlay" }]],
298
+ ["q", [{ type: "exit" }]],
182
299
  ["j", []],
183
300
  ];
301
+
302
+ for (const [name, expected] of table) {
303
+ test(`${name} -> ${JSON.stringify(expected)}`, () => {
304
+ expect(reduceKey(keyState, key(name))).toEqual(expected);
305
+ });
306
+ }
307
+
308
+ test("an observer's span-mode mutations answer read-only", () => {
309
+ // Arrange
310
+ const observer = state({ spanMode: true, readOnly: true });
311
+
312
+ // Assert
313
+ expect(reduceKey(observer, key("x"))).toEqual([
314
+ { type: "status", message: "observer - read-only" },
315
+ ]);
316
+ expect(reduceKey(observer, key("a"))).toEqual([
317
+ { type: "status", message: "observer - read-only" },
318
+ ]);
319
+ expect(reduceKey(observer, key("c"))).toEqual([
320
+ { type: "status", message: "observer - read-only" },
321
+ ]);
322
+ });
323
+
324
+ test("span-mode cut is owner-only, like the normal plan cut", () => {
325
+ // Arrange
326
+ const collaborator = state({ spanMode: true, canEditPlan: false });
327
+
328
+ // Assert
329
+ expect(reduceKey(collaborator, key("x"))).toEqual([]);
330
+ });
331
+
332
+ test("all span-mode mutations are blocked once the review is resolved", () => {
333
+ // Arrange
334
+ const resolved = state({ spanMode: true, resolved: true });
335
+ const readOnlyStatus: Intent[] = [{ type: "status", message: "review submitted - read-only" }];
336
+
337
+ // Assert - comment, cut, and quick-actions all answer read-only
338
+ expect(reduceKey(resolved, key("x"))).toEqual(readOnlyStatus);
339
+ expect(reduceKey(resolved, key("c"))).toEqual(readOnlyStatus);
340
+ expect(reduceKey(resolved, key("a"))).toEqual(readOnlyStatus);
341
+ });
342
+
343
+ test("span-mode cut gates for an observer even when cut is rebound off x", () => {
344
+ // Arrange - the span sub-grammar hardwires x, so the read-only gate must catch it by name
345
+ const rebound = { ...DEFAULT_KEYS, cut: ["d"] };
346
+ const observer = state({ spanMode: true, readOnly: true, keys: rebound });
347
+
348
+ // Assert
349
+ expect(reduceKey(observer, key("x"))).toEqual([
350
+ { type: "status", message: "observer - read-only" },
351
+ ]);
352
+ });
353
+ });
354
+
355
+ describe("span actions overlay", () => {
356
+ // Arrange
357
+ const keyState = state({ overlay: "spanActions" });
358
+ const table: [string, Intent[]][] = [
359
+ ["j", [{ type: "moveSpanAction", direction: 1 }]],
360
+ ["down", [{ type: "moveSpanAction", direction: 1 }]],
361
+ ["k", [{ type: "moveSpanAction", direction: -1 }]],
362
+ ["up", [{ type: "moveSpanAction", direction: -1 }]],
363
+ ["return", [{ type: "pickSpanAction" }]],
364
+ ["enter", [{ type: "pickSpanAction" }]],
365
+ ["escape", [{ type: "closeSpanActions" }]],
366
+ ["c", []],
367
+ ["q", []],
368
+ ];
369
+
184
370
  for (const [name, expected] of table) {
185
371
  test(`${name} -> ${JSON.stringify(expected)}`, () => {
186
- expect(reduceKey(s, key(name))).toEqual(expected);
372
+ expect(reduceKey(keyState, key(name))).toEqual(expected);
187
373
  });
188
374
  }
189
375
  });
190
376
 
191
377
  describe("diff mode", () => {
192
- const s = state({ view: "diff" });
378
+ // Arrange
379
+ const keyState = state({ view: "diff" });
193
380
  const table: [string, Intent[]][] = [
194
- ["j", [{ t: "move", to: "down" }]],
195
- ["k", [{ t: "move", to: "up" }]],
196
- ["g", [{ t: "move", to: "top" }]],
197
- ["c", [{ t: "openCompose", kind: "comment", from: "cursor" }]],
198
- ["n", [{ t: "nextAnn" }]],
199
- ["p", [{ t: "prevAnn" }]],
200
- ["backspace", [{ t: "removeAnnotation" }]],
201
- ["return", [{ t: "openSubmit" }]],
202
- ["v", [{ t: "status", msg: "plan-only verb - diff review uses c on a line" }]],
203
- ["x", [{ t: "status", msg: "plan-only verb - diff review uses c on a line" }]],
204
- ["e", [{ t: "status", msg: "plan-only verb - diff review uses c on a line" }]],
205
- ["s", [{ t: "status", msg: "plan-only verb - diff review uses c on a line" }]],
206
- ["q", [{ t: "exit" }]],
381
+ ["j", [{ type: "move", to: "down" }]],
382
+ ["k", [{ type: "move", to: "up" }]],
383
+ ["g", [{ type: "move", to: "top" }]],
384
+ ["c", [{ type: "openCompose", kind: "comment", from: "cursor" }]],
385
+ ["n", [{ type: "nextAnnotation" }]],
386
+ ["p", [{ type: "prevAnnotation" }]],
387
+ ["backspace", [{ type: "removeAnnotation" }]],
388
+ ["return", [{ type: "openSubmit" }]],
389
+ ["v", [{ type: "status", message: "plan-only primitive - diff review uses c on a line" }]],
390
+ ["x", [{ type: "rejectChange" }]],
391
+ ["e", [{ type: "status", message: "plan-only primitive - diff review uses c on a line" }]],
392
+ ["s", [{ type: "toggleDiffView" }]],
393
+ ["q", [{ type: "exit" }]],
207
394
  ];
395
+
208
396
  for (const [name, expected] of table) {
209
397
  test(`${name} -> ${JSON.stringify(expected)}`, () => {
210
- expect(reduceKey(s, key(name))).toEqual(expected);
398
+ expect(reduceKey(keyState, key(name))).toEqual(expected);
211
399
  });
212
400
  }
213
401
 
402
+ test("shift+x rejects the whole hunk under the cursor", () => {
403
+ // Assert
404
+ expect(reduceKey(keyState, key("x", true))).toEqual([{ type: "rejectHunk" }]);
405
+ });
406
+
407
+ test("curation is owner-only and locked once resolved", () => {
408
+ // Assert - a resolved diff answers before curating
409
+ expect(reduceKey(state({ view: "diff", resolved: true }), key("x"))).toEqual([
410
+ { type: "status", message: "review submitted - read-only" },
411
+ ]);
412
+ // a collaborator (canEditPlan false) cannot curate hunks
413
+ expect(reduceKey(state({ view: "diff", canEditPlan: false }), key("x"))).toEqual([
414
+ { type: "status", message: "only the diff owner can curate hunks" },
415
+ ]);
416
+ // an observer's mutating attempt is stopped by the read-only gate
417
+ expect(reduceKey(state({ view: "diff", readOnly: true }), key("x"))).toEqual([
418
+ { type: "status", message: "observer - read-only" },
419
+ ]);
420
+ });
421
+
422
+ test("u restores a curated-out item, owner-only and locked once resolved", () => {
423
+ // Assert - the undo intent leaves the target to the dispatcher
424
+ expect(reduceKey(keyState, key("u"))).toEqual([{ type: "restoreCuration" }]);
425
+ expect(reduceKey(state({ view: "diff", resolved: true }), key("u"))).toEqual([
426
+ { type: "status", message: "review submitted - read-only" },
427
+ ]);
428
+ expect(reduceKey(state({ view: "diff", canEditPlan: false }), key("u"))).toEqual([
429
+ { type: "status", message: "only the diff owner can curate hunks" },
430
+ ]);
431
+ expect(reduceKey(state({ view: "diff", readOnly: true }), key("u"))).toEqual([
432
+ { type: "status", message: "observer - read-only" },
433
+ ]);
434
+ });
435
+
214
436
  test("comment guards: resolved first, then non-code rows", () => {
437
+ // Assert
215
438
  expect(reduceKey(state({ view: "diff", resolved: true }), key("c"))).toEqual([
216
- { t: "status", msg: "review submitted - read-only" },
439
+ { type: "status", message: "review submitted - read-only" },
217
440
  ]);
218
441
  expect(reduceKey(state({ view: "diff", cursorAnnotatable: false }), key("c"))).toEqual([
219
- { t: "status", msg: "move to a code line to comment" },
442
+ { type: "status", message: "move to a code line to comment" },
443
+ ]);
444
+ });
445
+ });
446
+
447
+ describe("guided walk", () => {
448
+ test("w enters the walk in diff view; resolved and read-only sessions answer", () => {
449
+ // Assert
450
+ expect(reduceKey(state({ view: "diff" }), key("w"))).toEqual([{ type: "walkStart" }]);
451
+ expect(reduceKey(state({ view: "diff", resolved: true }), key("w"))).toEqual([
452
+ { type: "status", message: "review submitted - read-only" },
453
+ ]);
454
+ expect(reduceKey(state({ view: "diff", readOnly: true }), key("w"))).toEqual([
455
+ { type: "status", message: "observer - read-only" },
456
+ ]);
457
+ expect(reduceKey(state(), key("w"))).toEqual([
458
+ { type: "status", message: "the guided walk is a diff-review mode" },
459
+ ]);
460
+ });
461
+
462
+ // Arrange
463
+ const walking = state({ view: "diff", overlay: "walk" });
464
+ const table: [string, Intent[]][] = [
465
+ ["]", [{ type: "walkForward" }]],
466
+ ["[", [{ type: "walkBack" }]],
467
+ ["escape", [{ type: "walkLeave" }]],
468
+ ["q", [{ type: "exit" }]],
469
+ // mid-walk return does nothing: submit lives on the end card only
470
+ ["return", []],
471
+ ["j", []],
472
+ ["c", []],
473
+ ];
474
+
475
+ for (const [name, expected] of table) {
476
+ test(`walking ${name} -> ${JSON.stringify(expected)}`, () => {
477
+ expect(reduceKey(walking, key(name))).toEqual(expected);
478
+ });
479
+ }
480
+
481
+ test("the end card's return leaves the walk and opens the submit confirm", () => {
482
+ // Arrange
483
+ const atEnd = state({ view: "diff", overlay: "walk", walkAtEnd: true });
484
+
485
+ // Assert
486
+ expect(reduceKey(atEnd, key("return"))).toEqual([
487
+ { type: "walkLeave" },
488
+ { type: "openSubmit" },
220
489
  ]);
490
+ expect(reduceKey(atEnd, key("enter"))).toEqual([{ type: "walkLeave" }, { type: "openSubmit" }]);
491
+ });
492
+ });
493
+
494
+ describe("span and walk own their keys", () => {
495
+ test("span mode owns b as a span primitive", () => {
496
+ // Assert
497
+ expect(reduceKey(state({ spanMode: true }), key("b"))).toEqual([
498
+ { type: "spanKey", name: "b" },
499
+ ]);
500
+ });
501
+
502
+ test("the walk overlay keeps the brackets for stepping", () => {
503
+ // Arrange
504
+ const walking = state({ view: "diff", overlay: "walk" });
505
+
506
+ // Assert
507
+ expect(reduceKey(walking, key("]"))).toEqual([{ type: "walkForward" }]);
508
+ expect(reduceKey(walking, key("["))).toEqual([{ type: "walkBack" }]);
221
509
  });
222
510
  });
223
511
 
224
512
  describe("duplicated-branch collapse", () => {
225
513
  test("plan and diff produce the same intents for annotation nav, delete, and submit", () => {
226
- for (const patch of [
514
+ const patches: Partial<KeyState>[] = [
227
515
  {},
228
516
  { annotationCount: 0, hasFocusedAnnotation: false },
229
517
  { resolved: true },
230
- ] as Partial<KeyState>[]) {
518
+ ];
519
+
520
+ for (const patch of patches) {
231
521
  for (const name of ["n", "p", "backspace", "return"]) {
232
522
  const plan = reduceKey(state(patch), key(name));
233
523
  const diff = reduceKey(state({ ...patch, view: "diff" }), key(name));
524
+
234
525
  expect(diff).toEqual(plan);
235
526
  }
236
527
  }
@@ -240,24 +531,81 @@ describe("duplicated-branch collapse", () => {
240
531
  describe("read-only filter", () => {
241
532
  test("every mutating key answers observer - read-only, in plan and diff", () => {
242
533
  for (const view of ["plan", "diff"] as const) {
243
- const s = state({ view, readOnly: true });
244
- for (const name of ["c", "s", "x", "e", "backspace", "return", "enter"]) {
245
- expect(reduceKey(s, key(name))).toEqual([{ t: "status", msg: "observer - read-only" }]);
534
+ const keyState = state({ view, readOnly: true });
535
+
536
+ for (const name of ["c", "x", "e", "backspace", "return", "enter"]) {
537
+ expect(reduceKey(keyState, key(name))).toEqual([
538
+ { type: "status", message: "observer - read-only" },
539
+ ]);
246
540
  }
247
541
  }
248
542
  });
249
543
 
250
544
  test("navigation and annotation focus still work for observers", () => {
251
- const s = state({ readOnly: true });
252
- expect(reduceKey(s, key("j"))).toEqual([{ t: "move", to: "down" }]);
253
- expect(reduceKey(s, key("n"))).toEqual([{ t: "nextAnn" }]);
254
- expect(reduceKey(s, key("q"))).toEqual([{ t: "exit" }]);
545
+ // Arrange
546
+ const keyState = state({ readOnly: true });
547
+
548
+ // Assert
549
+ expect(reduceKey(keyState, key("j"))).toEqual([{ type: "move", to: "down" }]);
550
+ expect(reduceKey(keyState, key("n"))).toEqual([{ type: "nextAnnotation" }]);
551
+ expect(reduceKey(keyState, key("q"))).toEqual([{ type: "exit" }]);
255
552
  });
256
553
 
257
- test("span-mode c/s are gated by key name even under a rebound keymap", () => {
258
- const rebound = { ...DEFAULT_KEYS, comment: ["m"], suggest: ["t"] };
259
- const s = state({ spanMode: true, readOnly: true, keys: rebound });
260
- expect(reduceKey(s, key("c"))).toEqual([{ t: "status", msg: "observer - read-only" }]);
261
- expect(reduceKey(s, key("s"))).toEqual([{ t: "status", msg: "observer - read-only" }]);
554
+ test("span-mode c is gated by key name even under a rebound keymap", () => {
555
+ // Arrange
556
+ const rebound = { ...DEFAULT_KEYS, comment: ["m"] };
557
+ const keyState = state({ spanMode: true, readOnly: true, keys: rebound });
558
+
559
+ // Assert
560
+ expect(reduceKey(keyState, key("c"))).toEqual([
561
+ { type: "status", message: "observer - read-only" },
562
+ ]);
563
+ });
564
+ });
565
+
566
+ describe("inbox delete", () => {
567
+ test("d on a session requests delete", () => {
568
+ // Arrange / Act / Assert
569
+ expect(reduceKey(state({ view: "inbox" }), key("d"))).toEqual([
570
+ { type: "requestDeleteSession" },
571
+ ]);
572
+ });
573
+ });
574
+
575
+ describe("confirm overlay", () => {
576
+ // Arrange
577
+ const keyState = state({ overlay: "confirm" });
578
+ const table: [string, Intent[]][] = [
579
+ ["return", [{ type: "confirmDialog" }]],
580
+ ["enter", [{ type: "confirmDialog" }]],
581
+ ["escape", [{ type: "closeOverlay" }]],
582
+ ["j", []],
583
+ ["d", []],
584
+ ];
585
+
586
+ for (const [name, expected] of table) {
587
+ test(`${name} -> ${JSON.stringify(expected)}`, () => {
588
+ expect(reduceKey(keyState, key(name))).toEqual(expected);
589
+ });
590
+ }
591
+ });
592
+
593
+ describe("rename grammar", () => {
594
+ test("r on a focused note opens rename", () => {
595
+ // Arrange / Act / Assert
596
+ expect(reduceKey(state({ hasFocusedAnnotation: true }), key("r"))).toEqual([
597
+ { type: "openRename" },
598
+ ]);
599
+ });
600
+
601
+ test("prompt overlay routes ⏎ to confirm and esc to cancel", () => {
602
+ // Arrange / Act / Assert
603
+ expect(reduceKey(state({ overlay: "prompt" }), key("return"))).toEqual([
604
+ { type: "confirmDialog" },
605
+ ]);
606
+ expect(reduceKey(state({ overlay: "prompt" }), key("escape"))).toEqual([
607
+ { type: "closeOverlay" },
608
+ ]);
609
+ expect(reduceKey(state({ overlay: "prompt" }), key("a"))).toEqual([]);
262
610
  });
263
611
  });