@cueloop/client 0.1.0-alpha.10 → 0.1.0-alpha.101

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (356) hide show
  1. package/native/build-pty.sh +51 -0
  2. package/native/build.sh +82 -0
  3. package/native/darwin-arm64/libcuelooppty.dylib +0 -0
  4. package/native/darwin-arm64/libcueloopvt.dylib +0 -0
  5. package/native/src/pty.zig +193 -0
  6. package/native/src/shim.zig +114 -0
  7. package/package.json +42 -34
  8. package/src/annotations/annotation-palette.ts +62 -0
  9. package/src/annotations/components/AnnotationCards.stories.tsx +86 -0
  10. package/src/annotations/components/AnnotationCards.test.tsx +42 -0
  11. package/src/annotations/components/AnnotationCards.tsx +369 -0
  12. package/src/annotations/components/DiscussionMarkerRail.stories.tsx +54 -0
  13. package/src/annotations/components/DiscussionMarkerRail.tsx +186 -0
  14. package/src/annotations/discussions.ts +77 -0
  15. package/src/annotations/thread-selection.test.ts +210 -0
  16. package/src/annotations/thread-selection.ts +242 -0
  17. package/src/annotations/use-annotation-surface.tsx +1243 -0
  18. package/src/app/App.appearance.test.tsx +53 -0
  19. package/src/{App.autoclose.test.tsx → app/App.autoclose.test.tsx} +77 -51
  20. package/src/app/App.deleted-scroll.test.tsx +187 -0
  21. package/src/app/App.diff.test.tsx +313 -0
  22. package/src/app/App.export.test.tsx +105 -0
  23. package/src/app/App.focus.test.ts +89 -0
  24. package/src/app/App.identity.test.tsx +84 -0
  25. package/src/app/App.plan-render.test.tsx +209 -0
  26. package/src/app/App.plan-review.test.tsx +428 -0
  27. package/src/app/App.prototype-markdown.test.tsx +128 -0
  28. package/src/app/App.prototype.test.tsx +176 -0
  29. package/src/app/App.readonly.test.tsx +210 -0
  30. package/src/app/App.regression.test.tsx +99 -0
  31. package/src/app/App.rendering.test.tsx +134 -0
  32. package/src/app/App.share-dialog.test.tsx +177 -0
  33. package/src/app/App.submit-confirm.test.tsx +267 -0
  34. package/src/app/App.test.tsx +496 -0
  35. package/src/app/App.thread-agent.test.ts +20 -0
  36. package/src/app/App.toast.test.tsx +124 -0
  37. package/src/app/App.tsx +1667 -0
  38. package/src/app/App.visual.test.tsx +59 -0
  39. package/src/app/App.walk.test.tsx +299 -0
  40. package/src/app/App.workbench.test.tsx +1149 -0
  41. package/src/app/app-key-state.ts +61 -0
  42. package/src/app/app-screens.tsx +606 -0
  43. package/src/app/app-view-model.ts +168 -0
  44. package/src/app/components/AppHeader.stories.tsx +50 -0
  45. package/src/app/components/AppHeader.tsx +81 -0
  46. package/src/app/components/AppShell.brand.test.tsx +58 -0
  47. package/src/app/components/AppShell.extensions.test.tsx +191 -0
  48. package/src/app/components/AppShell.stories.tsx +89 -0
  49. package/src/app/components/AppShell.tsx +358 -0
  50. package/src/app/components/AppShell.zoom.test.tsx +149 -0
  51. package/src/app/components/Breadcrumb.stories.tsx +19 -0
  52. package/src/app/components/Breadcrumb.tsx +45 -0
  53. package/src/app/components/MenuBar.stories.tsx +28 -0
  54. package/src/app/components/MenuBar.tsx +44 -0
  55. package/src/app/components/PaneGrid.stories.tsx +84 -0
  56. package/src/app/components/PaneGrid.tsx +128 -0
  57. package/src/app/components/PanelColumn.stories.tsx +25 -0
  58. package/src/app/components/PanelColumn.tsx +93 -0
  59. package/src/app/components/ShellHeader.stories.tsx +50 -0
  60. package/src/app/components/ShellHeader.tsx +137 -0
  61. package/src/app/components/WorkspacePanels.stories.tsx +36 -0
  62. package/src/app/components/WorkspacePanels.tsx +120 -0
  63. package/src/app/components/pages/Workspace.stories.tsx +181 -0
  64. package/src/app/intent-dispatch.test.ts +728 -0
  65. package/src/app/intent-dispatch.ts +687 -0
  66. package/src/app/launch-layout.test.ts +78 -0
  67. package/src/app/launch-layout.ts +57 -0
  68. package/src/app/ready-signal.test.ts +85 -0
  69. package/src/app/ready-signal.ts +52 -0
  70. package/src/app/run.ts +109 -0
  71. package/src/{serve.test.ts → app/serve.test.ts} +24 -12
  72. package/src/app/serve.ts +111 -0
  73. package/src/app/version.ts +5 -0
  74. package/src/appearance/components/theme-context.stories.tsx +46 -0
  75. package/src/appearance/components/theme-context.tsx +37 -0
  76. package/src/appearance/theme-presets.test.ts +51 -0
  77. package/src/appearance/theme-presets.ts +219 -0
  78. package/src/appearance/theme.ts +112 -0
  79. package/src/appearance/visual/contrast.test.ts +35 -0
  80. package/src/appearance/visual/contrast.ts +33 -0
  81. package/src/diff/components/AnnotatableFileView.stories.tsx +42 -0
  82. package/src/diff/components/AnnotatableFileView.test.tsx +157 -0
  83. package/src/diff/components/AnnotatableFileView.tsx +141 -0
  84. package/src/diff/components/BareWorkbenchFileView.stories.tsx +27 -0
  85. package/src/diff/components/BareWorkbenchFileView.tsx +63 -0
  86. package/src/diff/components/DiffContentView.scroll.test.tsx +434 -0
  87. package/src/diff/components/DiffContentView.split.test.tsx +140 -0
  88. package/src/diff/components/DiffContentView.stories.tsx +80 -0
  89. package/src/diff/components/DiffContentView.syntax.test.tsx +82 -0
  90. package/src/diff/components/DiffContentView.tsx +1055 -0
  91. package/src/diff/components/WalkWizard.stories.tsx +100 -0
  92. package/src/diff/components/WalkWizard.tsx +146 -0
  93. package/src/diff/components/diff-content-view-layout.test.ts +198 -0
  94. package/src/diff/components/diff-content-view-layout.ts +84 -0
  95. package/src/diff/diff-fold.test.ts +78 -0
  96. package/src/diff/diff-fold.ts +126 -0
  97. package/src/diff/diff-hunk-curate.test.ts +315 -0
  98. package/src/diff/diff-hunk-curate.ts +152 -0
  99. package/src/diff/diff-intraline.test.ts +239 -0
  100. package/src/diff/diff-intraline.ts +238 -0
  101. package/src/diff/diff-syntax.test.ts +125 -0
  102. package/src/diff/diff-syntax.ts +186 -0
  103. package/src/diff/split-diff.test.ts +110 -0
  104. package/src/diff/split-diff.ts +144 -0
  105. package/src/diff/view-diff.test.ts +325 -0
  106. package/src/diff/view-diff.ts +522 -0
  107. package/src/diff/walk.test.ts +121 -0
  108. package/src/diff/walk.ts +73 -0
  109. package/src/index.ts +31 -2
  110. package/src/integrations/attribution.test.ts +31 -0
  111. package/src/integrations/attribution.ts +42 -0
  112. package/src/integrations/client-extension-registry.test.tsx +61 -0
  113. package/src/integrations/client-extension-registry.ts +159 -0
  114. package/src/integrations/clipboard.test.ts +23 -0
  115. package/src/integrations/clipboard.ts +33 -0
  116. package/src/integrations/components/ExtensionZone.stories.tsx +28 -0
  117. package/src/integrations/components/ExtensionZone.tsx +145 -0
  118. package/src/integrations/components/ShareDialog.agent.test.tsx +50 -0
  119. package/src/integrations/components/ShareDialog.stories.tsx +121 -0
  120. package/src/integrations/components/ShareDialog.tsx +574 -0
  121. package/src/integrations/components/share-dialog-store.ts +59 -0
  122. package/src/integrations/editor.test.ts +149 -0
  123. package/src/integrations/editor.ts +160 -0
  124. package/src/integrations/github-identity.test.ts +58 -0
  125. package/src/integrations/github-identity.ts +41 -0
  126. package/src/integrations/herdr.test.ts +43 -0
  127. package/src/integrations/herdr.ts +38 -0
  128. package/src/integrations/integrations.test.ts +41 -0
  129. package/src/integrations/integrations.ts +32 -0
  130. package/src/integrations/pasted-image.test.ts +67 -0
  131. package/src/integrations/pasted-image.ts +60 -0
  132. package/src/integrations/share.ts +238 -0
  133. package/src/keyboard/components/CompletionOverlay.stories.tsx +37 -0
  134. package/src/keyboard/components/CompletionOverlay.tsx +75 -0
  135. package/src/keyboard/components/HandleChipsInput.stories.tsx +24 -0
  136. package/src/keyboard/components/HandleChipsInput.test.tsx +35 -0
  137. package/src/keyboard/components/HandleChipsInput.tsx +143 -0
  138. package/src/keyboard/components/KeybindsDialog.stories.tsx +31 -0
  139. package/src/keyboard/components/KeybindsDialog.tsx +103 -0
  140. package/src/keyboard/components/NavModeHint.stories.tsx +40 -0
  141. package/src/keyboard/components/NavModeHint.tsx +31 -0
  142. package/src/keyboard/components/SlashComposer.stories.tsx +37 -0
  143. package/src/keyboard/components/SlashComposer.tsx +87 -0
  144. package/src/keyboard/key-bindings.test.ts +156 -0
  145. package/src/keyboard/key-bindings.ts +459 -0
  146. package/src/keyboard/keymap.test.ts +611 -0
  147. package/src/keyboard/keymap.ts +395 -0
  148. package/src/keyboard/skills.test.ts +74 -0
  149. package/src/keyboard/skills.ts +75 -0
  150. package/src/keyboard/slash-palette.test.ts +103 -0
  151. package/src/keyboard/slash-palette.ts +129 -0
  152. package/src/keyboard/thread-chords.test.ts +79 -0
  153. package/src/keyboard/thread-chords.ts +163 -0
  154. package/src/keyboard/use-shared-keyboard.ts +55 -0
  155. package/src/markdown/__snapshots__/view-plan.regression.test.ts.snap +100 -0
  156. package/src/markdown/components/MarkdownGridBlock.stories.tsx +33 -0
  157. package/src/markdown/components/MarkdownGridBlock.tsx +72 -0
  158. package/src/markdown/components/MarkdownThreadEditor.stories.tsx +15 -0
  159. package/src/markdown/components/MarkdownThreadEditor.test.tsx +98 -0
  160. package/src/markdown/components/MarkdownThreadEditor.tsx +160 -0
  161. package/src/markdown/components/ThreadView.selection.test.tsx +951 -0
  162. package/src/markdown/components/ThreadView.slash.test.tsx +146 -0
  163. package/src/markdown/components/ThreadView.stories.tsx +26 -0
  164. package/src/markdown/components/ThreadView.tsx +560 -0
  165. package/src/markdown/mark-runs.ts +121 -0
  166. package/src/markdown/markdown-editor-syntax-style.ts +50 -0
  167. package/src/markdown/markdown-grid.test.ts +101 -0
  168. package/src/markdown/markdown-grid.ts +203 -0
  169. package/src/markdown/markdown-highlight.test.ts +61 -0
  170. package/src/markdown/markdown-highlight.ts +124 -0
  171. package/src/markdown/thread-code-syntax.ts +45 -0
  172. package/src/markdown/use-thread-body-editing.tsx +54 -0
  173. package/src/markdown/view-plan.regression.test.ts +60 -0
  174. package/src/markdown/view-plan.test.ts +545 -0
  175. package/src/markdown/view-plan.ts +1009 -0
  176. package/src/performance/otlp-trace.test.ts +119 -0
  177. package/src/performance/otlp-trace.ts +134 -0
  178. package/src/performance/perf-report.ts +18 -0
  179. package/src/performance/perf-timings.test.ts +18 -0
  180. package/src/performance/perf-timings.ts +39 -0
  181. package/src/prototype/components/PrototypeContentView.stories.tsx +19 -0
  182. package/src/prototype/components/PrototypeContentView.tsx +442 -0
  183. package/src/prototype/prototype-browser.test.ts +81 -0
  184. package/src/prototype/prototype-browser.ts +321 -0
  185. package/src/prototype/prototype-pixels.tsx +35 -0
  186. package/src/settings/components/SettingsDialog.stories.tsx +117 -0
  187. package/src/settings/components/SettingsDialog.tsx +183 -0
  188. package/src/settings/components/SettingsRows.stories.tsx +18 -0
  189. package/src/settings/components/SettingsRows.tsx +112 -0
  190. package/src/settings/components/quick-actions-editor.stories.tsx +58 -0
  191. package/src/settings/components/quick-actions-editor.test.tsx +120 -0
  192. package/src/settings/components/quick-actions-editor.tsx +222 -0
  193. package/src/settings/config-identity.test.ts +78 -0
  194. package/src/settings/config.test.ts +676 -0
  195. package/src/settings/config.ts +628 -0
  196. package/src/settings/use-settings-dialog.tsx +351 -0
  197. package/src/stories/__snapshots__/stories.test.tsx.snap +2358 -0
  198. package/src/stories/stories-app.tsx +196 -0
  199. package/src/stories/stories.test.tsx +74 -0
  200. package/src/stories/story-fixtures.ts +102 -0
  201. package/src/stories/story-tree.test.ts +108 -0
  202. package/src/stories/story.ts +154 -0
  203. package/src/terminal/ansi-screen.test.ts +28 -0
  204. package/src/terminal/ansi-screen.ts +72 -0
  205. package/src/terminal/components/AnsiScreen.stories.tsx +12 -0
  206. package/src/terminal/components/AnsiScreen.tsx +28 -0
  207. package/src/terminal/components/terminal-pane.test.tsx +40 -0
  208. package/src/terminal/components/terminal-pane.ts +196 -0
  209. package/src/terminal/ghostty-terminal.test.ts +85 -0
  210. package/src/terminal/ghostty-terminal.ts +197 -0
  211. package/src/terminal/kitty-image.test.ts +66 -0
  212. package/src/terminal/kitty-image.ts +137 -0
  213. package/src/terminal/pty.test.ts +81 -0
  214. package/src/terminal/pty.ts +246 -0
  215. package/src/testing/test-support.test.ts +264 -0
  216. package/src/testing/test-support.ts +367 -0
  217. package/src/thread/agent-continuation.test.ts +32 -0
  218. package/src/thread/agent-continuation.ts +13 -0
  219. package/src/thread/agent-transcript.test.ts +161 -0
  220. package/src/thread/agent-transcript.ts +336 -0
  221. package/src/thread/components/AgentConfigControls.stories.tsx +39 -0
  222. package/src/thread/components/AgentConfigControls.test.tsx +112 -0
  223. package/src/thread/components/AgentConfigControls.tsx +114 -0
  224. package/src/thread/components/AgentThreadPane.stories.tsx +105 -0
  225. package/src/thread/components/AgentThreadPane.test.tsx +935 -0
  226. package/src/thread/components/AgentThreadPane.tsx +377 -0
  227. package/src/thread/components/ThreadFooter.stories.tsx +30 -0
  228. package/src/thread/components/ThreadFooter.test.tsx +49 -0
  229. package/src/thread/components/ThreadFooter.tsx +77 -0
  230. package/src/thread/components/ThreadTree.stories.tsx +46 -0
  231. package/src/thread/components/ThreadTree.test.tsx +41 -0
  232. package/src/thread/components/ThreadTree.tsx +267 -0
  233. package/src/thread/components/thread-tree.test.ts +137 -0
  234. package/src/thread/components/thread-tree.ts +112 -0
  235. package/src/thread/thread-controller.curate.test.ts +502 -0
  236. package/src/thread/thread-controller.derivation.test.ts +62 -0
  237. package/src/thread/thread-controller.live-diff.test.ts +357 -0
  238. package/src/thread/thread-controller.optimistic.test.ts +69 -0
  239. package/src/thread/thread-controller.share.test.ts +573 -0
  240. package/src/thread/thread-controller.switch-cache.test.ts +174 -0
  241. package/src/thread/thread-controller.tree.test.ts +263 -0
  242. package/src/thread/thread-controller.ts +2026 -0
  243. package/src/thread/tree-view.test.ts +128 -0
  244. package/src/thread/tree-view.ts +146 -0
  245. package/src/thread/use-thread-agent.test.tsx +208 -0
  246. package/src/thread/use-thread-agent.ts +117 -0
  247. package/src/ui/components/CodeBlock.stories.tsx +42 -0
  248. package/src/ui/components/CodeBlock.tsx +80 -0
  249. package/src/ui/components/ConfirmCard.stories.tsx +37 -0
  250. package/src/ui/components/ConfirmCard.tsx +151 -0
  251. package/src/ui/components/ConfirmDialog.stories.tsx +21 -0
  252. package/src/ui/components/ConfirmDialog.tsx +72 -0
  253. package/src/ui/components/OverlayScrollbar.stories.tsx +33 -0
  254. package/src/ui/components/OverlayScrollbar.test.tsx +139 -0
  255. package/src/ui/components/OverlayScrollbar.tsx +129 -0
  256. package/src/ui/components/PromptDialog.stories.tsx +36 -0
  257. package/src/ui/components/PromptDialog.test.tsx +65 -0
  258. package/src/ui/components/PromptDialog.tsx +99 -0
  259. package/src/ui/components/RootOverlay.stories.tsx +45 -0
  260. package/src/ui/components/RootOverlay.tsx +49 -0
  261. package/src/ui/components/ScrollArea.stories.tsx +20 -0
  262. package/src/ui/components/ScrollArea.tsx +61 -0
  263. package/src/ui/components/Toast.stories.tsx +19 -0
  264. package/src/ui/components/Toast.tsx +54 -0
  265. package/src/ui/components/Tooltip.stories.tsx +26 -0
  266. package/src/ui/components/Tooltip.tsx +72 -0
  267. package/src/ui/components/menu-control.stories.tsx +35 -0
  268. package/src/ui/components/menu-control.tsx +43 -0
  269. package/src/ui/components/primitives/Button.stories.tsx +32 -0
  270. package/src/ui/components/primitives/Button.tsx +57 -0
  271. package/src/ui/components/primitives/Card.stories.tsx +25 -0
  272. package/src/ui/components/primitives/Card.tsx +62 -0
  273. package/src/ui/components/primitives/Dialog.stories.tsx +24 -0
  274. package/src/ui/components/primitives/Dialog.test.tsx +38 -0
  275. package/src/ui/components/primitives/Dialog.tsx +69 -0
  276. package/src/ui/components/primitives/DialogActions.stories.tsx +20 -0
  277. package/src/ui/components/primitives/DialogActions.tsx +39 -0
  278. package/src/ui/components/primitives/IconButton.stories.tsx +36 -0
  279. package/src/ui/components/primitives/IconButton.tsx +58 -0
  280. package/src/ui/components/primitives/StatusBar.stories.tsx +11 -0
  281. package/src/ui/components/primitives/StatusBar.tsx +23 -0
  282. package/src/ui/components/primitives/Switch.stories.tsx +17 -0
  283. package/src/ui/components/primitives/Switch.tsx +32 -0
  284. package/src/ui/components/primitives/Tabs.stories.tsx +29 -0
  285. package/src/ui/components/primitives/Tabs.tsx +101 -0
  286. package/src/ui/components/primitives/Toolbar.stories.tsx +17 -0
  287. package/src/ui/components/primitives/Toolbar.tsx +14 -0
  288. package/src/ui/components/primitives/Tree.stories.tsx +124 -0
  289. package/src/ui/components/primitives/Tree.tsx +133 -0
  290. package/src/ui/components/primitives/frame.test.ts +12 -0
  291. package/src/ui/components/primitives/frame.ts +17 -0
  292. package/src/ui/components/primitives/icons.ts +61 -0
  293. package/src/ui/components/primitives/tree-model.test.ts +99 -0
  294. package/src/ui/components/primitives/tree-model.ts +137 -0
  295. package/src/ui/components/syntax-highlight.ts +133 -0
  296. package/src/ui/components/truncate-text.ts +7 -0
  297. package/src/ui/components/truncate-title.test.ts +31 -0
  298. package/src/ui/components/truncate-title.ts +15 -0
  299. package/src/ui/use-frame-measure.ts +74 -0
  300. package/src/ui/use-terminal-virtualizer.ts +259 -0
  301. package/src/workbench/components/ChangesColumn.stories.tsx +44 -0
  302. package/src/workbench/components/ChangesColumn.tsx +203 -0
  303. package/src/workbench/components/ChangesFileTree.test.tsx +181 -0
  304. package/src/workbench/components/EditorGrid.split.test.tsx +118 -0
  305. package/src/workbench/components/EditorGrid.stories.tsx +86 -0
  306. package/src/workbench/components/EditorGrid.tabs.test.tsx +118 -0
  307. package/src/workbench/components/EditorGrid.tsx +544 -0
  308. package/src/workbench/components/EditorGrid.zoom.test.tsx +132 -0
  309. package/src/workbench/components/FileContentsView.stories.tsx +16 -0
  310. package/src/workbench/components/FileContentsView.test.tsx +29 -0
  311. package/src/workbench/components/FileContentsView.tsx +100 -0
  312. package/src/workbench/components/GridTabContent.actions.test.tsx +151 -0
  313. package/src/workbench/components/GridTabContent.stories.tsx +59 -0
  314. package/src/workbench/components/GridTabContent.tsx +193 -0
  315. package/src/workbench/components/ProjectTreeView.stories.tsx +25 -0
  316. package/src/workbench/components/ProjectTreeView.test.tsx +236 -0
  317. package/src/workbench/components/ProjectTreeView.tsx +151 -0
  318. package/src/workbench/components/TreePane.stories.tsx +106 -0
  319. package/src/workbench/components/TreePane.tsx +155 -0
  320. package/src/workbench/components/WelcomePlayground.stories.tsx +12 -0
  321. package/src/workbench/components/WelcomePlayground.test.tsx +83 -0
  322. package/src/workbench/components/WelcomePlayground.tsx +138 -0
  323. package/src/workbench/components/editor-grid.test.ts +184 -0
  324. package/src/workbench/components/editor-grid.ts +281 -0
  325. package/src/workbench/components/file-tree.test.ts +60 -0
  326. package/src/workbench/components/file-tree.ts +112 -0
  327. package/src/workbench/use-changes-workbench.ts +205 -0
  328. package/src/workbench/use-remember-layout.ts +31 -0
  329. package/src/workbench/use-repo-changes.ts +91 -0
  330. package/src/workbench/workbench-snapshot.test.ts +99 -0
  331. package/src/workbench/workbench-snapshot.ts +59 -0
  332. package/src/App.diff.test.tsx +0 -99
  333. package/src/App.export.test.tsx +0 -88
  334. package/src/App.plan-review.test.tsx +0 -259
  335. package/src/App.readonly.test.tsx +0 -148
  336. package/src/App.rendering.test.tsx +0 -108
  337. package/src/App.submit-confirm.test.tsx +0 -183
  338. package/src/App.test.tsx +0 -218
  339. package/src/App.tsx +0 -1249
  340. package/src/config.test.ts +0 -116
  341. package/src/config.ts +0 -150
  342. package/src/editor.ts +0 -34
  343. package/src/herdr.test.ts +0 -68
  344. package/src/herdr.ts +0 -34
  345. package/src/keymap.test.ts +0 -270
  346. package/src/keymap.ts +0 -173
  347. package/src/run.ts +0 -30
  348. package/src/serve.ts +0 -75
  349. package/src/session-controller.ts +0 -398
  350. package/src/syntax.test.ts +0 -41
  351. package/src/syntax.ts +0 -88
  352. package/src/theme.ts +0 -46
  353. package/src/view-diff.test.ts +0 -55
  354. package/src/view-diff.ts +0 -86
  355. package/src/view.test.ts +0 -157
  356. package/src/view.ts +0 -364
@@ -1,116 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
5
- import { DEFAULT_KEYS, actionFor, loadConfig } from "./config";
6
- import { DARK } from "./theme";
7
-
8
- describe("loadConfig", () => {
9
- test("defaults when no file exists", () => {
10
- const c = loadConfig({ userConfigPath: "/nonexistent/config.toml" });
11
- expect(c.keys["comment"]).toEqual(["c"]);
12
- expect(c.theme.accent).toBe(DARK.accent);
13
- });
14
-
15
- test("user config rebinds actions and overrides theme tokens", () => {
16
- const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg-"));
17
- const path = join(dir, "config.toml");
18
- writeFileSync(
19
- path,
20
- `[keys]\ncomment = "a"\nsubmit = ["return", "S"]\n\n[theme]\naccent = "#ff0000"\n`,
21
- );
22
- try {
23
- const c = loadConfig({ userConfigPath: path });
24
- expect(c.keys["comment"]).toEqual(["a"]);
25
- expect(c.keys["submit"]).toEqual(["return", "S"]);
26
- expect(c.keys["cut"]).toEqual(["x"]); // untouched defaults survive
27
- expect(c.theme.accent).toBe("#ff0000");
28
- expect(c.theme.green).toBe(DARK.green);
29
- } finally {
30
- rmSync(dir, { recursive: true, force: true });
31
- }
32
- });
33
-
34
- test("repo config layers over user config", () => {
35
- const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg2-"));
36
- const user = join(dir, "user.toml");
37
- const repoRoot = join(dir, "repo");
38
- writeFileSync(user, `[keys]\ncomment = "a"\n`);
39
- Bun.spawnSync(["mkdir", "-p", join(repoRoot, ".cueloop")]);
40
- writeFileSync(join(repoRoot, ".cueloop", "config.toml"), `[keys]\ncomment = "z"\n`);
41
- try {
42
- const c = loadConfig({ userConfigPath: user, repoRoot });
43
- expect(c.keys["comment"]).toEqual(["z"]);
44
- } finally {
45
- rmSync(dir, { recursive: true, force: true });
46
- }
47
- });
48
-
49
- test("broken config never blocks a review", () => {
50
- const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg3-"));
51
- const path = join(dir, "config.toml");
52
- writeFileSync(path, "not [valid toml");
53
- try {
54
- const c = loadConfig({ userConfigPath: path });
55
- expect(c.keys["comment"]).toEqual(["c"]);
56
- } finally {
57
- rmSync(dir, { recursive: true, force: true });
58
- }
59
- });
60
- });
61
-
62
- describe("integrations.obsidian config", () => {
63
- test("defaults when the section is absent", () => {
64
- const c = loadConfig({ userConfigPath: "/nonexistent/config.toml" });
65
- expect(c.integrations.obsidian.vault).toBeUndefined();
66
- expect(c.integrations.obsidian.folder).toBe("cueloop");
67
- expect(c.integrations.obsidian.filenameFormat).toBe("{YYYY}-{MM}-{DD} - {title}");
68
- expect(c.integrations.obsidian.separator).toBe("space");
69
- expect(c.integrations.obsidian.exportOn).toBe("manual");
70
- });
71
-
72
- test("user config sets the section; invalid enum values are ignored", () => {
73
- const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg-obs-"));
74
- const path = join(dir, "config.toml");
75
- writeFileSync(
76
- path,
77
- `[integrations.obsidian]\nvault = "/notes/vault"\nfolder = "plans"\nexportOn = "approve"\nseparator = "comma"\n`,
78
- );
79
- try {
80
- const c = loadConfig({ userConfigPath: path });
81
- expect(c.integrations.obsidian.vault).toBe("/notes/vault");
82
- expect(c.integrations.obsidian.folder).toBe("plans");
83
- expect(c.integrations.obsidian.exportOn).toBe("approve");
84
- expect(c.integrations.obsidian.separator).toBe("space"); // invalid value falls back
85
- expect(c.keys["comment"]).toEqual(["c"]); // other sections untouched
86
- } finally {
87
- rmSync(dir, { recursive: true, force: true });
88
- }
89
- });
90
-
91
- test("repo config layers over user config for the section", () => {
92
- const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg-obs2-"));
93
- const user = join(dir, "user.toml");
94
- const repoRoot = join(dir, "repo");
95
- writeFileSync(user, `[integrations.obsidian]\nvault = "/user/vault"\nexportOn = "resolve"\n`);
96
- Bun.spawnSync(["mkdir", "-p", join(repoRoot, ".cueloop")]);
97
- writeFileSync(join(repoRoot, ".cueloop", "config.toml"), `[integrations.obsidian]\nfolder = "repo-plans"\n`);
98
- try {
99
- const c = loadConfig({ userConfigPath: user, repoRoot });
100
- expect(c.integrations.obsidian.vault).toBe("/user/vault"); // user layer survives
101
- expect(c.integrations.obsidian.exportOn).toBe("resolve");
102
- expect(c.integrations.obsidian.folder).toBe("repo-plans"); // repo layer wins
103
- } finally {
104
- rmSync(dir, { recursive: true, force: true });
105
- }
106
- });
107
- });
108
-
109
- describe("actionFor", () => {
110
- test("resolves keys and shifted keys to actions", () => {
111
- expect(actionFor(DEFAULT_KEYS, "j", false)).toBe("down");
112
- expect(actionFor(DEFAULT_KEYS, "g", true)).toBe("bottom"); // G
113
- expect(actionFor(DEFAULT_KEYS, "return", false)).toBe("submit");
114
- expect(actionFor(DEFAULT_KEYS, "zz", false)).toBeUndefined();
115
- });
116
- });
package/src/config.ts DELETED
@@ -1,150 +0,0 @@
1
- /**
2
- * Layered TOML config (#20): built-in defaults → user config → trusted repo
3
- * config → env. Sections: [keys] action = "combo" (every action rebindable),
4
- * [theme] per-token overrides, [integrations.obsidian] notes-vault export.
5
- */
6
-
7
- import { existsSync, readFileSync } from "node:fs";
8
- import { homedir } from "node:os";
9
- import { join } from "node:path";
10
- import { OBSIDIAN_DEFAULTS, type ObsidianConfig } from "@cueloop/integration-obsidian";
11
- import { writeFileSync, mkdirSync } from "node:fs";
12
- import { dirname } from "node:path";
13
- import { DARK, type Theme } from "./theme";
14
-
15
- export interface KeymapConfig {
16
- [action: string]: string | string[];
17
- }
18
-
19
- export interface IntegrationsConfig {
20
- obsidian: ObsidianConfig;
21
- }
22
-
23
- /** Post-submit behavior: "off" prompts, 0 closes instantly, N counts down. */
24
- export type AutoClose = "off" | number;
25
-
26
- export interface CueloopConfig {
27
- keys: Record<string, string[]>;
28
- theme: Theme;
29
- ui: { autoClose: AutoClose };
30
- integrations: IntegrationsConfig;
31
- }
32
-
33
- /** Every action in the grammar, with its default binding(s). */
34
- export const DEFAULT_KEYS: Record<string, string[]> = {
35
- down: ["j", "down"],
36
- up: ["k", "up"],
37
- top: ["g"],
38
- bottom: ["G"],
39
- span: ["v"],
40
- comment: ["c"],
41
- suggest: ["s"],
42
- cut: ["x"],
43
- edit: ["e"],
44
- next_annotation: ["n"],
45
- prev_annotation: ["p"],
46
- delete_annotation: ["backspace"],
47
- submit: ["return", "enter"],
48
- quit: ["q"],
49
- };
50
-
51
- function parseToml(text: string): Record<string, unknown> {
52
- // Bun ships a native TOML parser
53
- return Bun.TOML.parse(text) as Record<string, unknown>;
54
- }
55
-
56
- function layer(base: CueloopConfig, raw: Record<string, unknown>): CueloopConfig {
57
- const out: CueloopConfig = {
58
- keys: { ...base.keys },
59
- theme: { ...base.theme },
60
- ui: { ...base.ui },
61
- integrations: { obsidian: { ...base.integrations.obsidian } },
62
- };
63
- const keys = raw["keys"] as KeymapConfig | undefined;
64
- if (keys) {
65
- for (const [action, combo] of Object.entries(keys)) {
66
- out.keys[action] = Array.isArray(combo) ? combo : [combo];
67
- }
68
- }
69
- const ui = raw["ui"] as { auto_close?: unknown } | undefined;
70
- if (ui && ui.auto_close !== undefined) {
71
- if (ui.auto_close === "off") out.ui.autoClose = "off";
72
- else if (typeof ui.auto_close === "number" && ui.auto_close >= 0) out.ui.autoClose = ui.auto_close;
73
- }
74
- const theme = raw["theme"] as Partial<Record<keyof Theme, string>> | undefined;
75
- if (theme) {
76
- for (const [token, value] of Object.entries(theme)) {
77
- if (token in out.theme && typeof value === "string") {
78
- (out.theme as unknown as Record<string, string>)[token] = value;
79
- }
80
- }
81
- }
82
- const integrations = raw["integrations"] as Record<string, unknown> | undefined;
83
- const obsidian = integrations?.["obsidian"] as Record<string, unknown> | undefined;
84
- if (obsidian) {
85
- const o = out.integrations.obsidian;
86
- if (typeof obsidian["vault"] === "string") o.vault = obsidian["vault"];
87
- if (typeof obsidian["folder"] === "string") o.folder = obsidian["folder"];
88
- if (typeof obsidian["filenameFormat"] === "string") o.filenameFormat = obsidian["filenameFormat"];
89
- const sep = obsidian["separator"];
90
- if (sep === "space" || sep === "dash" || sep === "underscore") o.separator = sep;
91
- const on = obsidian["exportOn"];
92
- if (on === "approve" || on === "resolve" || on === "manual") o.exportOn = on;
93
- }
94
- return out;
95
- }
96
-
97
- export function loadConfig(opts: { repoRoot?: string; userConfigPath?: string } = {}): CueloopConfig {
98
- let config: CueloopConfig = {
99
- keys: { ...DEFAULT_KEYS },
100
- theme: { ...DARK },
101
- ui: { autoClose: "off" },
102
- integrations: { obsidian: { ...OBSIDIAN_DEFAULTS } },
103
- };
104
- const userPath =
105
- opts.userConfigPath ??
106
- process.env.CUELOOP_CONFIG ??
107
- join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "cueloop", "config.toml");
108
- for (const path of [userPath, opts.repoRoot ? join(opts.repoRoot, ".cueloop", "config.toml") : undefined]) {
109
- if (!path || !existsSync(path)) continue;
110
- try {
111
- config = layer(config, parseToml(readFileSync(path, "utf8")));
112
- } catch {
113
- // a broken config never blocks a review; defaults win
114
- }
115
- }
116
- return config;
117
- }
118
-
119
- /** Reverse lookup: key name (+shift) → action, per the loaded keymap. */
120
- export function actionFor(keys: Record<string, string[]>, name: string, shift: boolean): string | undefined {
121
- const wanted = shift && name.length === 1 ? name.toUpperCase() : name;
122
- for (const [action, combos] of Object.entries(keys)) {
123
- if (combos.includes(wanted)) return action;
124
- }
125
- return undefined;
126
- }
127
-
128
- /**
129
- * Persist the auto-close choice into the user config file. Line-level TOML
130
- * surgery on the one key we own: replace an existing `auto_close`, or append
131
- * an `[ui]` section - a full TOML writer is not worth its weight here.
132
- */
133
- export function persistAutoClose(value: AutoClose, userConfigPath?: string): void {
134
- const path =
135
- userConfigPath ??
136
- process.env.CUELOOP_CONFIG ??
137
- join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "cueloop", "config.toml");
138
- const rendered = value === "off" ? '"off"' : String(value);
139
- let text = "";
140
- if (existsSync(path)) text = readFileSync(path, "utf8");
141
- if (/^\s*auto_close\s*=/m.test(text)) {
142
- text = text.replace(/^(\s*)auto_close\s*=.*$/m, `$1auto_close = ${rendered}`);
143
- } else if (/^\[ui\]/m.test(text)) {
144
- text = text.replace(/^\[ui\]\s*$/m, `[ui]\nauto_close = ${rendered}`);
145
- } else {
146
- text = text.trimEnd() + (text.trim() ? "\n\n" : "") + `[ui]\nauto_close = ${rendered}\n`;
147
- }
148
- mkdirSync(dirname(path), { recursive: true });
149
- writeFileSync(path, text);
150
- }
package/src/editor.ts DELETED
@@ -1,34 +0,0 @@
1
- /**
2
- * File-level edit mode = the $EDITOR hand-off (#22, #11): write the working
3
- * copy to a temp file, run the user's editor on it, read the result back.
4
- * Tests point CUELOOP_EDITOR at a non-interactive command.
5
- */
6
-
7
- import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
8
- import { tmpdir } from "node:os";
9
- import { join } from "node:path";
10
-
11
- export interface EditResult {
12
- content: string;
13
- changed: boolean;
14
- }
15
-
16
- export function editInEditor(content: string, filename = "plan.md"): EditResult {
17
- const editor = process.env.CUELOOP_EDITOR ?? process.env.VISUAL ?? process.env.EDITOR;
18
- if (!editor) throw new Error("no editor: set $EDITOR (or $CUELOOP_EDITOR)");
19
- const dir = mkdtempSync(join(tmpdir(), "cueloop-edit-"));
20
- const path = join(dir, filename);
21
- writeFileSync(path, content);
22
- try {
23
- const proc = Bun.spawnSync([...editor.split(" "), path], {
24
- stdin: "inherit",
25
- stdout: "inherit",
26
- stderr: "inherit",
27
- });
28
- if (proc.exitCode !== 0) throw new Error(`editor exited ${proc.exitCode}`);
29
- const next = readFileSync(path, "utf8");
30
- return { content: next, changed: next !== content };
31
- } finally {
32
- rmSync(dir, { recursive: true, force: true });
33
- }
34
- }
package/src/herdr.test.ts DELETED
@@ -1,68 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
- import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
5
- import { focusHerdrPane, returnPaneFor } from "./herdr";
6
-
7
- let dir: string;
8
- const savedEnv: Record<string, string | undefined> = {};
9
-
10
- beforeEach(() => {
11
- dir = mkdtempSync(join(tmpdir(), "cueloop-herdr-"));
12
- for (const k of ["HERDR_ENV", "HERDR_PANE_ID", "HERDR_BIN_PATH", "CUELOOP_RETURN_PANE"]) {
13
- savedEnv[k] = process.env[k];
14
- delete process.env[k];
15
- }
16
- });
17
- afterEach(() => {
18
- for (const [k, v] of Object.entries(savedEnv)) {
19
- if (v === undefined) delete process.env[k];
20
- else process.env[k] = v;
21
- }
22
- rmSync(dir, { recursive: true, force: true });
23
- });
24
-
25
- describe("returnPaneFor", () => {
26
- test("outside herdr there is never a return target", () => {
27
- expect(returnPaneFor("w1:p1")).toBeUndefined();
28
- });
29
-
30
- test("env override wins, session meta is the fallback", () => {
31
- process.env.HERDR_ENV = "1";
32
- process.env.HERDR_PANE_ID = "w1:p9";
33
- expect(returnPaneFor("w1:p1")).toBe("w1:p1");
34
- process.env.CUELOOP_RETURN_PANE = "w1:p2";
35
- expect(returnPaneFor("w1:p1")).toBe("w1:p2");
36
- });
37
-
38
- test("returning to our own pane is a no-op", () => {
39
- process.env.HERDR_ENV = "1";
40
- process.env.HERDR_PANE_ID = "w1:p1";
41
- expect(returnPaneFor("w1:p1")).toBeUndefined();
42
- });
43
- });
44
-
45
- describe("focusHerdrPane", () => {
46
- test("resolves the pane's tab and focuses it", () => {
47
- const log = join(dir, "calls.log");
48
- const stub = join(dir, "herdr");
49
- writeFileSync(
50
- stub,
51
- `#!/bin/sh
52
- echo "$@" >> "${log}"
53
- if [ "$1" = "pane" ]; then echo '{"result":{"pane":{"tab_id":"w1:t3"}}}'; fi
54
- `,
55
- );
56
- Bun.spawnSync(["chmod", "+x", stub]);
57
- process.env.HERDR_BIN_PATH = stub;
58
- expect(focusHerdrPane("w1:p1")).toBe(true);
59
- const calls = readFileSync(log, "utf8").trim().split("\n");
60
- expect(calls[0]).toBe("pane get w1:p1");
61
- expect(calls[1]).toBe("tab focus w1:t3");
62
- });
63
-
64
- test("a missing binary never throws", () => {
65
- process.env.HERDR_BIN_PATH = join(dir, "nope");
66
- expect(focusHerdrPane("w1:p1")).toBe(false);
67
- });
68
- });
package/src/herdr.ts DELETED
@@ -1,34 +0,0 @@
1
- /**
2
- * First-class herdr return (#23): a review opened beside an agent should hand
3
- * focus back to the agent's pane when it closes, not leave the reviewer on a
4
- * dead tab. Detection is the ambient env contract; the return target comes
5
- * from CUELOOP_RETURN_PANE (set by whoever opened this pane) or the session's
6
- * recorded herdrPane (set by the adapter hook). Everything here is
7
- * best-effort: focus failures never block closing the review.
8
- */
9
-
10
- export function insideHerdr(): boolean {
11
- return process.env.HERDR_ENV === "1";
12
- }
13
-
14
- export function returnPaneFor(sessionHerdrPane?: string): string | undefined {
15
- if (!insideHerdr()) return undefined;
16
- const pane = process.env.CUELOOP_RETURN_PANE ?? sessionHerdrPane;
17
- // returning to our own pane would be a no-op at best
18
- return pane && pane !== process.env.HERDR_PANE_ID ? pane : undefined;
19
- }
20
-
21
- /** Focus the tab holding the target pane. Synchronous: runs just before exit. */
22
- export function focusHerdrPane(paneId: string): boolean {
23
- const bin = process.env.HERDR_BIN_PATH ?? "herdr";
24
- try {
25
- const got = Bun.spawnSync([bin, "pane", "get", paneId], { stdout: "pipe", stderr: "ignore" });
26
- if (got.exitCode !== 0) return false;
27
- const parsed = JSON.parse(got.stdout.toString()) as { result?: { pane?: { tab_id?: string } } };
28
- const tab = parsed.result?.pane?.tab_id;
29
- if (!tab) return false;
30
- return Bun.spawnSync([bin, "tab", "focus", tab], { stdout: "ignore", stderr: "ignore" }).exitCode === 0;
31
- } catch {
32
- return false;
33
- }
34
- }
@@ -1,270 +0,0 @@
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
- */
6
-
7
- import { describe, expect, test } from "bun:test";
8
- import { DEFAULT_KEYS } from "./config";
9
- import { reduceKey, type Intent, type KeyState } from "./keymap";
10
-
11
- function state(patch: Partial<KeyState> = {}): KeyState {
12
- return {
13
- keys: DEFAULT_KEYS,
14
- readOnly: false,
15
- overlay: "none",
16
- view: "plan",
17
- spanMode: false,
18
- resolved: false,
19
- hasInboxItems: true,
20
- annotationCount: 1,
21
- hasFocusedAnnotation: true,
22
- cursorAnnotatable: true,
23
- ...patch,
24
- };
25
- }
26
-
27
- function key(name: string, shift = false): { name: string; shift: boolean } {
28
- return { name, shift };
29
- }
30
-
31
- describe("compose overlay", () => {
32
- const s = state({ overlay: "compose" });
33
- const table: [string, Intent[]][] = [
34
- ["escape", [{ t: "closeOverlay" }]],
35
- ["return", [{ t: "saveCompose" }]],
36
- ["enter", [{ t: "saveCompose" }]],
37
- ["left", []],
38
- ["right", []],
39
- ["j", []],
40
- ["q", []],
41
- ];
42
- for (const [name, expected] of table) {
43
- test(`${name} -> ${JSON.stringify(expected)}`, () => {
44
- expect(reduceKey(s, key(name))).toEqual(expected);
45
- });
46
- }
47
- });
48
-
49
- describe("submit overlay", () => {
50
- const s = state({ overlay: "submit" });
51
- 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 }]],
57
- ["j", []],
58
- ["q", []],
59
- ];
60
- for (const [name, expected] of table) {
61
- test(`${name} -> ${JSON.stringify(expected)}`, () => {
62
- expect(reduceKey(s, key(name))).toEqual(expected);
63
- });
64
- }
65
- });
66
-
67
- describe("completion overlay", () => {
68
- const prompt = state({ overlay: "completion-prompt" });
69
- const counting = state({ overlay: "completion-counting" });
70
- 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" }]],
76
- [prompt, "j", []],
77
- [counting, "return", [{ t: "finishReview" }]],
78
- [counting, "q", [{ t: "finishReview" }]],
79
- [counting, "a", []],
80
- [counting, "escape", [{ t: "dismissCompletion" }]],
81
- ];
82
- for (const [s, name, expected] of table) {
83
- test(`${s.overlay} ${name} -> ${JSON.stringify(expected)}`, () => {
84
- expect(reduceKey(s, key(name))).toEqual(expected);
85
- });
86
- }
87
- });
88
-
89
- describe("inbox mode", () => {
90
- const s = state({ view: "inbox" });
91
- 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" }]],
99
- ["c", []],
100
- ["escape", []],
101
- ];
102
- for (const [name, expected] of table) {
103
- test(`${name} -> ${JSON.stringify(expected)}`, () => {
104
- expect(reduceKey(s, key(name))).toEqual(expected);
105
- });
106
- }
107
-
108
- test("an empty inbox swallows every non-quit key", () => {
109
- const empty = state({ view: "inbox", hasInboxItems: false });
110
- for (const name of ["j", "k", "down", "up", "return", "enter"]) {
111
- expect(reduceKey(empty, key(name))).toEqual([]);
112
- }
113
- expect(reduceKey(empty, key("q"))).toEqual([{ t: "exit" }]);
114
- });
115
- });
116
-
117
- describe("plan normal mode", () => {
118
- const s = state();
119
- 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
- // the default state has a selected card, so Cut and edit act on the card
130
- ["x", false, [{ t: "removeAnnotation" }]],
131
- ["e", false, [{ t: "editCard" }]],
132
- ["n", false, [{ t: "nextAnn" }]],
133
- ["p", false, [{ t: "prevAnn" }]],
134
- ["backspace", false, [{ t: "removeAnnotation" }]],
135
- ["return", false, [{ t: "openSubmit" }]],
136
- ["q", false, [{ t: "exit" }]],
137
- ["escape", false, [{ t: "deselect" }]],
138
- ["z", false, []],
139
- ];
140
- for (const [name, shift, expected] of table) {
141
- test(`${shift ? "shift+" : ""}${name} -> ${JSON.stringify(expected)}`, () => {
142
- expect(reduceKey(s, key(name, shift))).toEqual(expected);
143
- });
144
- }
145
-
146
- test("resolved sessions guard the mutating verbs", () => {
147
- const r = state({ resolved: true });
148
- for (const name of ["c", "s", "x", "e"]) {
149
- expect(reduceKey(r, key(name))).toEqual([{ t: "status", msg: "review submitted - read-only" }]);
150
- }
151
- expect(reduceKey(r, key("return"))).toEqual([]);
152
- expect(reduceKey(r, key("backspace"))).toEqual([]);
153
- });
154
-
155
- test("cut text cannot host a comment or a span", () => {
156
- const cut = state({ cursorAnnotatable: false });
157
- expect(reduceKey(cut, key("c"))).toEqual([{ t: "status", msg: "text is cut - restore it first" }]);
158
- expect(reduceKey(cut, key("s"))).toEqual([{ t: "status", msg: "text is cut - restore it first" }]);
159
- expect(reduceKey(cut, key("v"))).toEqual([]);
160
- });
161
-
162
- test("annotation navigation without annotations reports, delete without focus is silent", () => {
163
- const none = state({ annotationCount: 0, hasFocusedAnnotation: false });
164
- expect(reduceKey(none, key("n"))).toEqual([{ t: "status", msg: "no annotations" }]);
165
- expect(reduceKey(none, key("p"))).toEqual([{ t: "status", msg: "no annotations" }]);
166
- expect(reduceKey(none, key("backspace"))).toEqual([]);
167
- });
168
-
169
- test("without a selected card x cuts the block and e opens the editor", () => {
170
- const unfocused = state({ hasFocusedAnnotation: false });
171
- expect(reduceKey(unfocused, key("x"))).toEqual([{ t: "cut" }]);
172
- expect(reduceKey(unfocused, key("e"))).toEqual([{ t: "edit" }]);
173
- });
174
- });
175
-
176
- describe("span mode", () => {
177
- const s = state({ spanMode: true });
178
- const table: [string, Intent[]][] = [
179
- ["l", [{ t: "spanKey", name: "l" }]],
180
- ["h", [{ t: "spanKey", name: "h" }]],
181
- ["w", [{ t: "spanKey", name: "w" }]],
182
- ["b", [{ t: "spanKey", name: "b" }]],
183
- ["$", [{ t: "spanKey", name: "$" }]],
184
- ["0", [{ t: "spanKey", name: "0" }]],
185
- ["c", [{ t: "openCompose", kind: "comment", from: "span" }]],
186
- ["s", [{ t: "openCompose", kind: "suggestion", from: "span" }]],
187
- ["escape", [{ t: "closeOverlay" }]],
188
- ["q", [{ t: "exit" }]],
189
- ["j", []],
190
- ];
191
- for (const [name, expected] of table) {
192
- test(`${name} -> ${JSON.stringify(expected)}`, () => {
193
- expect(reduceKey(s, key(name))).toEqual(expected);
194
- });
195
- }
196
- });
197
-
198
- describe("diff mode", () => {
199
- const s = state({ view: "diff" });
200
- const table: [string, Intent[]][] = [
201
- ["j", [{ t: "move", to: "down" }]],
202
- ["k", [{ t: "move", to: "up" }]],
203
- ["g", [{ t: "move", to: "top" }]],
204
- ["c", [{ t: "openCompose", kind: "comment", from: "cursor" }]],
205
- ["n", [{ t: "nextAnn" }]],
206
- ["p", [{ t: "prevAnn" }]],
207
- ["backspace", [{ t: "removeAnnotation" }]],
208
- ["return", [{ t: "openSubmit" }]],
209
- ["v", [{ t: "status", msg: "plan-only verb - diff review uses c on a line" }]],
210
- ["x", [{ t: "status", msg: "plan-only verb - diff review uses c on a line" }]],
211
- ["e", [{ t: "status", msg: "plan-only verb - diff review uses c on a line" }]],
212
- ["s", [{ t: "status", msg: "plan-only verb - diff review uses c on a line" }]],
213
- ["q", [{ t: "exit" }]],
214
- ];
215
- for (const [name, expected] of table) {
216
- test(`${name} -> ${JSON.stringify(expected)}`, () => {
217
- expect(reduceKey(s, key(name))).toEqual(expected);
218
- });
219
- }
220
-
221
- test("comment guards: resolved first, then non-code rows", () => {
222
- expect(reduceKey(state({ view: "diff", resolved: true }), key("c"))).toEqual([
223
- { t: "status", msg: "review submitted - read-only" },
224
- ]);
225
- expect(reduceKey(state({ view: "diff", cursorAnnotatable: false }), key("c"))).toEqual([
226
- { t: "status", msg: "move to a code line to comment" },
227
- ]);
228
- });
229
- });
230
-
231
- describe("duplicated-branch collapse", () => {
232
- test("plan and diff produce the same intents for annotation nav, delete, and submit", () => {
233
- for (const patch of [
234
- {},
235
- { annotationCount: 0, hasFocusedAnnotation: false },
236
- { resolved: true },
237
- ] as Partial<KeyState>[]) {
238
- for (const name of ["n", "p", "backspace", "return"]) {
239
- const plan = reduceKey(state(patch), key(name));
240
- const diff = reduceKey(state({ ...patch, view: "diff" }), key(name));
241
- expect(diff).toEqual(plan);
242
- }
243
- }
244
- });
245
- });
246
-
247
- describe("read-only filter", () => {
248
- test("every mutating key answers observer - read-only, in plan and diff", () => {
249
- for (const view of ["plan", "diff"] as const) {
250
- const s = state({ view, readOnly: true });
251
- for (const name of ["c", "s", "x", "e", "backspace", "return", "enter"]) {
252
- expect(reduceKey(s, key(name))).toEqual([{ t: "status", msg: "observer - read-only" }]);
253
- }
254
- }
255
- });
256
-
257
- test("navigation and annotation focus still work for observers", () => {
258
- const s = state({ readOnly: true });
259
- expect(reduceKey(s, key("j"))).toEqual([{ t: "move", to: "down" }]);
260
- expect(reduceKey(s, key("n"))).toEqual([{ t: "nextAnn" }]);
261
- expect(reduceKey(s, key("q"))).toEqual([{ t: "exit" }]);
262
- });
263
-
264
- test("span-mode c/s are gated by key name even under a rebound keymap", () => {
265
- const rebound = { ...DEFAULT_KEYS, comment: ["m"], suggest: ["t"] };
266
- const s = state({ spanMode: true, readOnly: true, keys: rebound });
267
- expect(reduceKey(s, key("c"))).toEqual([{ t: "status", msg: "observer - read-only" }]);
268
- expect(reduceKey(s, key("s"))).toEqual([{ t: "status", msg: "observer - read-only" }]);
269
- });
270
- });