@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
package/src/keymap.ts DELETED
@@ -1,173 +0,0 @@
1
- /**
2
- * The keyboard grammar as a pure reducer (#70): view state in, intents out.
3
- * App builds a KeyState per key event, calls reduceKey, and dispatches the
4
- * intents to controller verbs and view-state setters - no branch bodies live
5
- * in the key handler. Diff and plan reviews share one path for annotation
6
- * navigation, deletion, and submit; the read-only rule is one gate here.
7
- */
8
-
9
- import { actionFor } from "./config";
10
-
11
- export type Intent =
12
- | { t: "exit" }
13
- | { t: "status"; msg: string }
14
- | { t: "move"; to: "down" | "up" | "top" | "bottom" }
15
- | { t: "inboxMove"; to: "down" | "up" }
16
- | { t: "openSession" }
17
- | { t: "startSpan" }
18
- | { t: "spanKey"; name: string }
19
- | { t: "openCompose"; kind: "comment" | "suggestion"; from: "cursor" | "span" }
20
- | { t: "openSubmit" }
21
- | { t: "cut" }
22
- | { t: "edit" }
23
- | { t: "editCard" }
24
- | { t: "nextAnn" }
25
- | { t: "prevAnn" }
26
- | { t: "removeAnnotation" }
27
- | { t: "deselect" }
28
- | { t: "closeOverlay" }
29
- | { t: "saveCompose" }
30
- | { t: "submitVerdict" }
31
- | { t: "cycleVerdict"; dir: -1 | 1 }
32
- | { t: "finishReview" }
33
- | { t: "optInAutoClose" }
34
- | { t: "dismissCompletion" };
35
-
36
- export interface KeyInput {
37
- name: string;
38
- shift: boolean;
39
- }
40
-
41
- export interface KeyState {
42
- /** Loaded keymap (config.ts): action -> key combos. */
43
- keys: Record<string, string[]>;
44
- readOnly: boolean;
45
- /** Layer that owns keys before the grammar runs. */
46
- overlay: "none" | "compose" | "submit" | "completion-prompt" | "completion-counting";
47
- view: "inbox" | "plan" | "diff";
48
- /** Plan-only span selection sub-mode. */
49
- spanMode: boolean;
50
- resolved: boolean;
51
- hasInboxItems: boolean;
52
- annotationCount: number;
53
- hasFocusedAnnotation: boolean;
54
- /** Cursor sits on annotatable text: a work block (plan) or a code row (diff). */
55
- cursorAnnotatable: boolean;
56
- }
57
-
58
- /** Verbs that write session state; an observer never reaches their handlers. */
59
- const MUTATING_ACTIONS = new Set(["comment", "suggest", "cut", "edit", "delete_annotation", "submit"]);
60
-
61
- const SPAN_KEYS = ["l", "h", "w", "b", "$", "0"];
62
-
63
- function status(msg: string): Intent[] {
64
- return [{ t: "status", msg }];
65
- }
66
-
67
- export function reduceKey(state: KeyState, key: KeyInput): Intent[] {
68
- const name = key.name;
69
- // compose/submit overlays own the keys via focused inputs; only escape,
70
- // return, and verdict arrows route through the grammar
71
- if (state.overlay === "compose" || state.overlay === "submit") {
72
- if (name === "escape") return [{ t: "closeOverlay" }];
73
- if (name === "return" || name === "enter") {
74
- return [state.overlay === "compose" ? { t: "saveCompose" } : { t: "submitVerdict" }];
75
- }
76
- if (state.overlay === "submit" && (name === "left" || name === "right")) {
77
- return [{ t: "cycleVerdict", dir: name === "left" ? -1 : 1 }];
78
- }
79
- return [];
80
- }
81
- if (state.overlay === "completion-prompt" || state.overlay === "completion-counting") {
82
- if (name === "return" || name === "enter" || name === "q") return [{ t: "finishReview" }];
83
- if (name === "a" && state.overlay === "completion-prompt") return [{ t: "optInAutoClose" }];
84
- if (name === "escape") return [{ t: "dismissCompletion" }];
85
- return [];
86
- }
87
- const action = actionFor(state.keys, name, key.shift);
88
- if (action === "quit") return [{ t: "exit" }];
89
- // the ONE read-only rule: any mutating attempt answers instead of acting
90
- // (span-mode c/s are hardwired keys, so they gate by name as well)
91
- const mutating = MUTATING_ACTIONS.has(action ?? "") || (state.spanMode && (name === "c" || name === "s"));
92
- if (state.readOnly && mutating) return status("observer - read-only");
93
-
94
- if (state.view === "inbox") return inboxGrammar(state, name);
95
- if (state.view === "diff") return diffGrammar(state, action);
96
- if (state.spanMode) return spanGrammar(name);
97
- return planGrammar(state, action, name);
98
- }
99
-
100
- function inboxGrammar(state: KeyState, name: string): Intent[] {
101
- if (!state.hasInboxItems) return [];
102
- if (name === "j" || name === "down") return [{ t: "inboxMove", to: "down" }];
103
- if (name === "k" || name === "up") return [{ t: "inboxMove", to: "up" }];
104
- if (name === "return" || name === "enter") return [{ t: "openSession" }];
105
- return [];
106
- }
107
-
108
- function diffGrammar(state: KeyState, action: string | undefined): Intent[] {
109
- const nav = navIntent(action);
110
- if (nav) return nav;
111
- if (action === "comment") {
112
- if (state.resolved) return status("review submitted - read-only");
113
- if (!state.cursorAnnotatable) return status("move to a code line to comment");
114
- return [{ t: "openCompose", kind: "comment", from: "cursor" }];
115
- }
116
- const shared = annotationCluster(state, action);
117
- if (shared) return shared;
118
- if (action === "span" || action === "cut" || action === "edit" || action === "suggest") {
119
- return status("plan-only verb - diff review uses c on a line");
120
- }
121
- return [];
122
- }
123
-
124
- function spanGrammar(name: string): Intent[] {
125
- if (name === "escape") return [{ t: "closeOverlay" }];
126
- if (SPAN_KEYS.includes(name)) return [{ t: "spanKey", name }];
127
- if (name === "c" || name === "s") {
128
- return [{ t: "openCompose", kind: name === "s" ? "suggestion" : "comment", from: "span" }];
129
- }
130
- return [];
131
- }
132
-
133
- function planGrammar(state: KeyState, action: string | undefined, name: string): Intent[] {
134
- const nav = navIntent(action);
135
- if (nav) return nav;
136
- if (name === "escape") return [{ t: "deselect" }];
137
- if (action === "span") return state.cursorAnnotatable ? [{ t: "startSpan" }] : [];
138
- if (action === "comment" || action === "suggest") {
139
- if (state.resolved) return status("review submitted - read-only");
140
- if (!state.cursorAnnotatable) return status("text is cut - restore it first");
141
- return [{ t: "openCompose", kind: action === "suggest" ? "suggestion" : "comment", from: "cursor" }];
142
- }
143
- if (action === "cut" || action === "edit") {
144
- if (state.resolved) return status("review submitted - read-only");
145
- // the document selects, the rail edits: with a card selected, Cut deletes
146
- // the annotation and edit rewrites the card body in place
147
- if (state.hasFocusedAnnotation) return [action === "cut" ? { t: "removeAnnotation" } : { t: "editCard" }];
148
- return [{ t: action }];
149
- }
150
- return annotationCluster(state, action) ?? [];
151
- }
152
-
153
- /** Cursor movement is the same intent everywhere; views clamp their own bounds. */
154
- function navIntent(action: string | undefined): Intent[] | null {
155
- if (action === "down" || action === "up" || action === "top" || action === "bottom") {
156
- return [{ t: "move", to: action }];
157
- }
158
- return null;
159
- }
160
-
161
- /** Annotation navigation, deletion, and submit: one path for plan and diff. */
162
- function annotationCluster(state: KeyState, action: string | undefined): Intent[] | null {
163
- if (action === "next_annotation" || action === "prev_annotation") {
164
- if (!state.annotationCount) return status("no annotations");
165
- return [{ t: action === "next_annotation" ? "nextAnn" : "prevAnn" }];
166
- }
167
- if (action === "delete_annotation") {
168
- if (state.resolved || !state.hasFocusedAnnotation) return [];
169
- return [{ t: "removeAnnotation" }];
170
- }
171
- if (action === "submit") return state.resolved ? [] : [{ t: "openSubmit" }];
172
- return null;
173
- }
package/src/run.ts DELETED
@@ -1,30 +0,0 @@
1
- /** Real-terminal entry: render the App and resolve when the user quits. */
2
-
3
- import React from "react";
4
- import { createCliRenderer } from "@opentui/core";
5
- import { createRoot } from "@opentui/react";
6
- import { App } from "./App";
7
-
8
- export interface RunClientOptions {
9
- sessionId?: string;
10
- home?: string;
11
- }
12
-
13
- export async function runClient(opts: RunClientOptions): Promise<number> {
14
- // mouse movement reporting makes multiplexers forward drags to the app,
15
- // so the renderer's native selection is the drag driver
16
- const renderer = await createCliRenderer({ enableMouseMovement: true });
17
- return new Promise<number>((resolve) => {
18
- createRoot(renderer).render(
19
- React.createElement(App, {
20
- home: opts.home,
21
- sessionId: opts.sessionId,
22
- onExit: (code: number) => {
23
- renderer.destroy();
24
- resolve(code);
25
- process.exit(code);
26
- },
27
- }),
28
- );
29
- });
30
- }
package/src/serve.ts DELETED
@@ -1,75 +0,0 @@
1
- /**
2
- * SSH-served TUI (#22 share path): `cueloop serve` lets teammates join a
3
- * review session over plain ssh. Every SSH connection renders <App> in
4
- * observer mode (readOnly) against the same local daemon; the one writable
5
- * controller stays the local owner's own `cueloop` TUI.
6
- *
7
- * Trust model - tunnel of trust, no passwords, no keys:
8
- * the server authenticates nobody ("open" auth). Access control is the act of
9
- * sharing the address deliberately: it binds to 127.0.0.1 by default, so a
10
- * remote teammate reaches it only through a channel the owner opened on
11
- * purpose (an SSH tunnel, a tailnet address via --host). Anyone who can reach
12
- * the port can watch - and only watch: observers cannot mutate the session.
13
- * The host key is generated once under CUELOOP_HOME/ssh/host_key (0600) so
14
- * fingerprints stay stable across restarts.
15
- */
16
-
17
- import { mkdirSync } from "node:fs";
18
- import { join } from "node:path";
19
- import React from "react";
20
- import { createRoot } from "@opentui/react";
21
- import { createServer } from "@opentui/ssh";
22
- import { cueloopHome } from "@cueloop/daemon";
23
- import { App } from "./App";
24
-
25
- export interface ServeOptions {
26
- /** TCP port for the SSH listener; 0 picks an ephemeral port. Default 2222. */
27
- port?: number;
28
- /** Bind address. Default 127.0.0.1 - widen deliberately (e.g. a tailnet IP). */
29
- host?: string;
30
- /** Open every connection on this session; omit for the inbox. */
31
- sessionId?: string;
32
- /** CUELOOP_HOME override (daemon socket + ssh host key live under it). */
33
- home?: string;
34
- /** Silence the @opentui/ssh startup banner (tests). Default true. */
35
- banner?: boolean;
36
- onError?: (err: unknown) => void;
37
- }
38
-
39
- export interface ServeHandle {
40
- host: string;
41
- port: number;
42
- /** SHA256 host-key fingerprints, for out-of-band verification. */
43
- fingerprints: string[];
44
- close(): Promise<void>;
45
- }
46
-
47
- export async function serveClient(opts: ServeOptions = {}): Promise<ServeHandle> {
48
- const home = opts.home ?? cueloopHome();
49
- const sshDir = join(home, "ssh");
50
- mkdirSync(sshDir, { recursive: true, mode: 0o700 });
51
-
52
- const server = createServer({
53
- // password-less by design; see the trust model in the module comment
54
- auth: "open",
55
- hostKey: { path: join(sshDir, "host_key") },
56
- startupBanner: opts.banner ?? true,
57
- idleTimeout: "2h",
58
- onError: opts.onError,
59
- }).serve((session) => {
60
- const root = createRoot(session.renderer);
61
- root.render(
62
- React.createElement(App, {
63
- home,
64
- sessionId: opts.sessionId,
65
- readOnly: true,
66
- // q disconnects only this observer, never the server
67
- onExit: () => session.end(),
68
- }),
69
- );
70
- session.onClose(() => root.unmount());
71
- });
72
-
73
- const info = await server.listen(opts.port ?? 2222, opts.host ?? "127.0.0.1");
74
- return { ...info, close: () => server.close() };
75
- }
@@ -1,398 +0,0 @@
1
- /**
2
- * The review-session controller (#70): every daemon round-trip and mutation
3
- * verb behind one React-free object. It owns connect/autostart/subscribe,
4
- * the session/inbox/status/error snapshot, optimistic apply, the mutation
5
- * verbs (cut/edit/annotate/submit/...), and the post-submit completion
6
- * lifecycle including the notes-vault export and the herdr return-focus.
7
- * App subscribes to snapshots and keeps only view state.
8
- */
9
-
10
- import { DaemonClient } from "@cueloop/daemon/client";
11
- import {
12
- cutBlock,
13
- makeAnchor,
14
- newAnnotationId,
15
- parseBlocks,
16
- resolveAnchor,
17
- restoreBlock,
18
- restoreLine,
19
- type ReviewSession,
20
- type VerdictKind,
21
- } from "@cueloop/schema";
22
- import { Registry, type Exporter } from "@cueloop/extension-api";
23
- import { createObsidianExtension, shouldExport, type ObsidianConfig } from "@cueloop/integration-obsidian";
24
- import { buildDisplay, nextWorkBlock, type DisplayBlock } from "./view";
25
- import { diffRowAnchor, diffRows, type DiffRow } from "./view-diff";
26
- import { editInEditor } from "./editor";
27
- import { focusHerdrPane, returnPaneFor } from "./herdr";
28
- import { persistAutoClose, type AutoClose, type CueloopConfig } from "./config";
29
-
30
- /**
31
- * Post-submit lifecycle (a review pane should hand you back to the agent,
32
- * not linger): idle → prompt (auto-close off: offer the choice) or
33
- * counting (configured delay) → exit. esc dismisses to the resolved view.
34
- */
35
- export type Completion =
36
- | { phase: "idle" }
37
- | { phase: "prompt" }
38
- | { phase: "counting"; remaining: number }
39
- | { phase: "dismissed" };
40
-
41
- export interface ControllerSnapshot {
42
- session: ReviewSession | null;
43
- inbox: ReviewSession[] | null;
44
- status: string;
45
- error: string | null;
46
- completion: Completion;
47
- /**
48
- * Annotations whose anchor stopped resolving after the last editor
49
- * hand-off - the reconciliation banner count. 0 = no banner.
50
- */
51
- editOrphanCount: number;
52
- }
53
-
54
- export interface ReviewControllerOptions {
55
- home?: string;
56
- sessionId?: string;
57
- /** Observer mode: stored for the key reducer's read-only gate. */
58
- readOnly?: boolean;
59
- onExit?: (code: number) => void;
60
- }
61
-
62
- export interface ReviewController {
63
- readonly readOnly: boolean;
64
- /** Snapshot listeners (stable identity - safe for useSyncExternalStore). */
65
- subscribe(listener: () => void): () => void;
66
- getSnapshot(): ControllerSnapshot;
67
- /** Dial the daemon (autostart), subscribe to events, fetch session or inbox. */
68
- connect(): void;
69
- close(): void;
70
- /** Loaded config parts the controller acts on: auto-close and exporters. */
71
- applyConfig(cfg: CueloopConfig): void;
72
- setStatus(msg: string): void;
73
- /** Derived projections, cached per session identity. */
74
- display(): DisplayBlock[];
75
- rows(): DiffRow[];
76
- working(): string;
77
- /** Open a session from the inbox. */
78
- open(id: string): void;
79
- /** Cut the block under the cursor, or restore a cut one. */
80
- cut(dispIdx: number): void;
81
- /** The $EDITOR hand-off on the working copy. */
82
- edit(): void;
83
- /**
84
- * Anchor and store an annotation; both plan and diff anchor constructions.
85
- * Returns the minted annotation id so the view can select the new card.
86
- */
87
- annotate(kind: "comment" | "suggestion", dispIdx: number, start: number, end: number, body: string): string | undefined;
88
- /** Rewrite a stored annotation's body in place (the rail-card edit). */
89
- updateAnnotation(id: string, body: string): void;
90
- removeAnnotation(id: string): void;
91
- setWorkingCopy(content: string | undefined): void;
92
- /** Resolve the review, run the export, start the completion hand-back. */
93
- submit(verdict: VerdictKind, summary: string): void;
94
- /** Close the review and, inside herdr, bounce focus back to the agent. */
95
- finishReview(): void;
96
- dismissCompletion(): void;
97
- /** From the completion prompt: persist auto-close and start the countdown. */
98
- optInAutoClose(): void;
99
- }
100
-
101
- export function createReviewController(opts: ReviewControllerOptions): ReviewController {
102
- return new Controller(opts);
103
- }
104
-
105
- class Controller implements ReviewController {
106
- readonly readOnly: boolean;
107
- private client: DaemonClient | null = null;
108
- private closed = false;
109
- private snap: ControllerSnapshot = {
110
- session: null,
111
- inbox: null,
112
- status: "",
113
- error: null,
114
- completion: { phase: "idle" },
115
- editOrphanCount: 0,
116
- };
117
- private listeners = new Set<() => void>();
118
- private autoClose: AutoClose = "off";
119
- private obsidian: ObsidianConfig | null = null;
120
- private exporters = new Map<string, Exporter>();
121
- private countdown: ReturnType<typeof setTimeout> | undefined;
122
- /** Projections keyed by session identity so renders reuse one computation. */
123
- private derivedFor: ReviewSession | null = null;
124
- private derived: { display: DisplayBlock[]; rows: DiffRow[] } = { display: [], rows: [] };
125
-
126
- constructor(private readonly opts: ReviewControllerOptions) {
127
- this.readOnly = opts.readOnly ?? false;
128
- }
129
-
130
- subscribe = (listener: () => void): (() => void) => {
131
- this.listeners.add(listener);
132
- return () => this.listeners.delete(listener);
133
- };
134
-
135
- getSnapshot = (): ControllerSnapshot => this.snap;
136
-
137
- private update(patch: Partial<ControllerSnapshot>): void {
138
- this.snap = { ...this.snap, ...patch };
139
- for (const l of this.listeners) l();
140
- }
141
-
142
- connect(): void {
143
- void (async () => {
144
- try {
145
- const client = await DaemonClient.connect({ home: this.opts.home, autostart: true });
146
- if (this.closed) return void client.close();
147
- this.client = client;
148
- client.onEvent((e) => {
149
- // another controller/observer changed state: re-fetch
150
- const s = this.snap.session;
151
- if (s && e.sessionId === s.id) void this.refreshSession(s.id);
152
- if (!this.opts.sessionId) void this.refreshInbox();
153
- });
154
- await client.subscribe();
155
- if (this.opts.sessionId) {
156
- this.update({ session: await client.sessionGet(this.opts.sessionId) });
157
- } else {
158
- this.update({ inbox: await client.sessionList({ status: "pending" }) });
159
- }
160
- } catch (err) {
161
- this.update({ error: err instanceof Error ? err.message : String(err) });
162
- }
163
- })();
164
- }
165
-
166
- close(): void {
167
- this.closed = true;
168
- clearTimeout(this.countdown);
169
- this.client?.close();
170
- }
171
-
172
- applyConfig(cfg: CueloopConfig): void {
173
- this.autoClose = cfg.ui.autoClose;
174
- this.obsidian = cfg.integrations.obsidian;
175
- // bundled integrations register through the public extension API
176
- const registry = new Registry();
177
- void registry.load("obsidian", createObsidianExtension(cfg.integrations.obsidian)).then((rec) => {
178
- this.exporters = rec.exporters;
179
- });
180
- }
181
-
182
- setStatus(msg: string): void {
183
- this.update({ status: msg });
184
- }
185
-
186
- // ── derived projections ─────────────────────
187
- private ensureDerived(): void {
188
- const s = this.snap.session;
189
- if (this.derivedFor === s) return;
190
- this.derivedFor = s;
191
- this.derived = {
192
- display: s ? buildDisplay(s.artifact.content, s.workingCopy) : [],
193
- rows: s && s.artifact.type === "diff" ? diffRows(s.artifact.content) : [],
194
- };
195
- }
196
-
197
- display(): DisplayBlock[] {
198
- this.ensureDerived();
199
- return this.derived.display;
200
- }
201
-
202
- rows(): DiffRow[] {
203
- this.ensureDerived();
204
- return this.derived.rows;
205
- }
206
-
207
- working(): string {
208
- const s = this.snap.session;
209
- return s ? (s.workingCopy ?? s.artifact.content) : "";
210
- }
211
-
212
- private async refreshSession(id: string): Promise<void> {
213
- if (this.client) this.update({ session: await this.client.sessionGet(id) });
214
- }
215
-
216
- private async refreshInbox(): Promise<void> {
217
- if (this.client) this.update({ inbox: await this.client.sessionList({ status: "pending" }) });
218
- }
219
-
220
- /** Optimistic apply: the daemon response is the next session snapshot. */
221
- private apply(p: Promise<ReviewSession>): void {
222
- p.then((session) => this.update({ session })).catch((e: unknown) =>
223
- this.setStatus(String(e instanceof Error ? e.message : e)),
224
- );
225
- }
226
-
227
- // ── verbs ───────────────────────────────────
228
- open(id: string): void {
229
- const cached = this.snap.inbox?.find((s) => s.id === id);
230
- if (cached) this.update({ session: cached });
231
- else void this.refreshSession(id);
232
- }
233
-
234
- cut(dispIdx: number): void {
235
- const session = this.snap.session;
236
- if (!session || session.status === "resolved") return;
237
- const d = this.display()[dispIdx];
238
- if (!d) return;
239
- const working = this.working();
240
- if (d.type === "del") {
241
- const line = restoreLine(nextWorkBlock(this.display(), dispIdx), working.split("\n").length);
242
- // restoreBlock returns undefined when the block structure round-trips
243
- // to the submitted revision - the working copy is gone
244
- const restored = restoreBlock(session.artifact.content, working, d.base!, line);
245
- this.setWorkingCopy(restored);
246
- this.setStatus("cut restored");
247
- } else if (d.work) {
248
- this.setWorkingCopy(cutBlock(working, d.work));
249
- this.setStatus("block cut - it serializes into the diff");
250
- }
251
- }
252
-
253
- edit(): void {
254
- const session = this.snap.session;
255
- if (!session || session.status === "resolved") return;
256
- try {
257
- const result = editInEditor(this.working(), "plan.md");
258
- if (result.changed) {
259
- this.setWorkingCopy(result.content);
260
- this.reconcileAnnotations(session, result.content);
261
- this.setStatus("edits tracked - one diff");
262
- } else this.setStatus("no changes");
263
- } catch (err) {
264
- this.setStatus(err instanceof Error ? err.message : String(err));
265
- }
266
- }
267
-
268
- /**
269
- * Edit-exit reconciliation: re-resolve every annotation against the edited
270
- * working copy with the quote-primary cascade. Annotations that stop
271
- * resolving stay stored (the feedback serializer flags orphaned anchors);
272
- * the count feeds the one-line banner above the sheet.
273
- */
274
- private reconcileAnnotations(session: ReviewSession, editedContent: string): void {
275
- const editedBlocks = parseBlocks(editedContent);
276
- const orphanCount = session.annotations.filter(
277
- (annotation) => resolveAnchor(annotation.anchor, editedBlocks) === null,
278
- ).length;
279
- this.update({ editOrphanCount: orphanCount });
280
- }
281
-
282
- annotate(
283
- kind: "comment" | "suggestion",
284
- dispIdx: number,
285
- start: number,
286
- end: number,
287
- body: string,
288
- ): string | undefined {
289
- const session = this.snap.session;
290
- if (!session) return undefined;
291
- let anchor;
292
- if (session.artifact.type === "diff") {
293
- anchor = { ...diffRowAnchor(this.rows(), dispIdx), blockIndex: dispIdx };
294
- } else {
295
- const display = this.display();
296
- const workBlocks = display.filter((d) => d.work).map((d) => d.work!);
297
- const workIdx = display.slice(0, dispIdx + 1).filter((d) => d.work).length - 1;
298
- anchor = makeAnchor(workBlocks, workIdx, start, end);
299
- }
300
- const annotationId = newAnnotationId();
301
- this.apply(
302
- this.client!.sessionAnnotate(session.id, {
303
- id: annotationId,
304
- kind,
305
- anchor,
306
- body,
307
- }),
308
- );
309
- this.setStatus(kind === "suggestion" ? "suggestion added - the agent applies it" : "comment added");
310
- return annotationId;
311
- }
312
-
313
- updateAnnotation(id: string, body: string): void {
314
- const session = this.snap.session;
315
- if (!session) return;
316
- const existing = session.annotations.find((annotation) => annotation.id === id);
317
- if (!existing) return;
318
- // the daemon's annotate verb upserts by id: same id + anchor, new body
319
- this.apply(
320
- this.client!.sessionAnnotate(session.id, {
321
- id: existing.id,
322
- kind: existing.kind,
323
- anchor: existing.anchor,
324
- body,
325
- }),
326
- );
327
- this.setStatus("annotation updated");
328
- }
329
-
330
- removeAnnotation(id: string): void {
331
- const session = this.snap.session;
332
- if (!session) return;
333
- this.apply(this.client!.sessionRemoveAnnotation(session.id, id));
334
- this.setStatus("annotation deleted");
335
- }
336
-
337
- setWorkingCopy(content: string | undefined): void {
338
- const session = this.snap.session;
339
- if (!session) return;
340
- this.apply(this.client!.sessionSetWorkingCopy(session.id, content));
341
- }
342
-
343
- submit(verdict: VerdictKind, summary: string): void {
344
- const session = this.snap.session;
345
- if (!session) return;
346
- this.client!.sessionResolve(session.id, verdict, summary)
347
- .then((resolved) => {
348
- this.update({ session: resolved, status: `review submitted - ${verdict.replace("_", " ")}` });
349
- // notes-vault export: guarded by config, default is manual (no-op)
350
- const obsidian = this.obsidian;
351
- const exporter = this.exporters.get("obsidian");
352
- if (obsidian && exporter && shouldExport(obsidian.exportOn, verdict)) {
353
- void exporter(resolved).then((r) => {
354
- this.setStatus(r.success && r.path ? `exported to ${r.path}` : `export failed: ${r.error ?? "unknown"}`);
355
- });
356
- }
357
- // hand the reviewer back to the agent: prompt or count down per config;
358
- // inside herdr with a known return pane, closing is the default
359
- const delay = this.autoClose;
360
- const returns = returnPaneFor(resolved.artifact.meta.herdrPane) !== undefined;
361
- if (delay === 0) this.finishReview();
362
- else if (typeof delay === "number") this.startCounting(delay);
363
- else if (returns) this.startCounting(3);
364
- else this.update({ completion: { phase: "prompt" } });
365
- })
366
- .catch((e: unknown) => this.setStatus(String(e instanceof Error ? e.message : e)));
367
- }
368
-
369
- // ── completion hand-back ────────────────────
370
- private startCounting(remaining: number): void {
371
- if (remaining <= 0) return this.finishReview();
372
- this.update({ completion: { phase: "counting", remaining } });
373
- this.countdown = setTimeout(() => this.startCounting(remaining - 1), 1000);
374
- }
375
-
376
- finishReview(): void {
377
- clearTimeout(this.countdown);
378
- const pane = returnPaneFor(this.snap.session?.artifact.meta.herdrPane);
379
- if (pane) focusHerdrPane(pane);
380
- this.opts.onExit?.(0);
381
- }
382
-
383
- dismissCompletion(): void {
384
- clearTimeout(this.countdown);
385
- this.update({ completion: { phase: "dismissed" } });
386
- }
387
-
388
- optInAutoClose(): void {
389
- // opt in: close in 3s from now on, persisted to the user config
390
- try {
391
- persistAutoClose(3);
392
- } catch {
393
- // a read-only config dir must not block closing the review
394
- }
395
- this.autoClose = 3;
396
- this.startCounting(3);
397
- }
398
- }