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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (345) hide show
  1. package/native/build-pty.sh +51 -0
  2. package/native/build.sh +82 -0
  3. package/native/darwin-arm64/libcuelooppty.dylib +0 -0
  4. package/native/darwin-arm64/libcueloopvt.dylib +0 -0
  5. package/native/src/pty.zig +193 -0
  6. package/native/src/shim.zig +114 -0
  7. package/package.json +41 -27
  8. package/src/annotations/annotation-palette.ts +62 -0
  9. package/src/annotations/components/AnnotationCards.stories.tsx +86 -0
  10. package/src/annotations/components/AnnotationCards.test.tsx +42 -0
  11. package/src/annotations/components/AnnotationCards.tsx +364 -0
  12. package/src/annotations/components/DiscussionMarkerRail.stories.tsx +54 -0
  13. package/src/annotations/components/DiscussionMarkerRail.tsx +186 -0
  14. package/src/annotations/discussions.ts +77 -0
  15. package/src/annotations/thread-selection.test.ts +210 -0
  16. package/src/annotations/thread-selection.ts +242 -0
  17. package/src/annotations/use-annotation-surface.tsx +1238 -0
  18. package/src/app/App.appearance.test.tsx +53 -0
  19. package/src/app/App.autoclose.test.tsx +144 -0
  20. package/src/app/App.deleted-scroll.test.tsx +187 -0
  21. package/src/app/App.diff.test.tsx +313 -0
  22. package/src/app/App.export.test.tsx +105 -0
  23. package/src/app/App.focus.test.ts +89 -0
  24. package/src/app/App.identity.test.tsx +84 -0
  25. package/src/app/App.plan-render.test.tsx +209 -0
  26. package/src/app/App.plan-review.test.tsx +428 -0
  27. package/src/app/App.prototype-markdown.test.tsx +128 -0
  28. package/src/app/App.prototype.test.tsx +176 -0
  29. package/src/app/App.readonly.test.tsx +210 -0
  30. package/src/app/App.regression.test.tsx +99 -0
  31. package/src/app/App.rendering.test.tsx +134 -0
  32. package/src/app/App.share-dialog.test.tsx +177 -0
  33. package/src/app/App.submit-confirm.test.tsx +267 -0
  34. package/src/app/App.test.tsx +496 -0
  35. package/src/app/App.thread-agent.test.ts +12 -0
  36. package/src/app/App.toast.test.tsx +124 -0
  37. package/src/app/App.tsx +1650 -0
  38. package/src/app/App.visual.test.tsx +59 -0
  39. package/src/app/App.walk.test.tsx +299 -0
  40. package/src/app/App.workbench.test.tsx +1149 -0
  41. package/src/app/app-key-state.ts +61 -0
  42. package/src/app/app-screens.tsx +606 -0
  43. package/src/app/app-view-model.ts +168 -0
  44. package/src/app/components/AppHeader.stories.tsx +50 -0
  45. package/src/app/components/AppHeader.tsx +81 -0
  46. package/src/app/components/AppShell.brand.test.tsx +58 -0
  47. package/src/app/components/AppShell.extensions.test.tsx +191 -0
  48. package/src/app/components/AppShell.stories.tsx +89 -0
  49. package/src/app/components/AppShell.tsx +358 -0
  50. package/src/app/components/AppShell.zoom.test.tsx +149 -0
  51. package/src/app/components/Breadcrumb.stories.tsx +19 -0
  52. package/src/app/components/Breadcrumb.tsx +45 -0
  53. package/src/app/components/MenuBar.stories.tsx +28 -0
  54. package/src/app/components/MenuBar.tsx +44 -0
  55. package/src/app/components/PaneGrid.stories.tsx +84 -0
  56. package/src/app/components/PaneGrid.tsx +128 -0
  57. package/src/app/components/PanelColumn.stories.tsx +25 -0
  58. package/src/app/components/PanelColumn.tsx +93 -0
  59. package/src/app/components/ShellHeader.stories.tsx +50 -0
  60. package/src/app/components/ShellHeader.tsx +137 -0
  61. package/src/app/components/WorkspacePanels.stories.tsx +36 -0
  62. package/src/app/components/WorkspacePanels.tsx +120 -0
  63. package/src/app/components/pages/Workspace.stories.tsx +181 -0
  64. package/src/app/intent-dispatch.test.ts +728 -0
  65. package/src/app/intent-dispatch.ts +687 -0
  66. package/src/app/launch-layout.test.ts +78 -0
  67. package/src/app/launch-layout.ts +57 -0
  68. package/src/app/ready-signal.test.ts +85 -0
  69. package/src/app/ready-signal.ts +52 -0
  70. package/src/app/run.ts +109 -0
  71. package/src/{serve.test.ts → app/serve.test.ts} +24 -12
  72. package/src/app/serve.ts +111 -0
  73. package/src/app/version.ts +5 -0
  74. package/src/appearance/components/theme-context.stories.tsx +46 -0
  75. package/src/appearance/components/theme-context.tsx +37 -0
  76. package/src/appearance/theme-presets.test.ts +51 -0
  77. package/src/appearance/theme-presets.ts +219 -0
  78. package/src/appearance/theme.ts +112 -0
  79. package/src/appearance/visual/contrast.test.ts +35 -0
  80. package/src/appearance/visual/contrast.ts +33 -0
  81. package/src/diff/components/AnnotatableFileView.stories.tsx +42 -0
  82. package/src/diff/components/AnnotatableFileView.test.tsx +157 -0
  83. package/src/diff/components/AnnotatableFileView.tsx +141 -0
  84. package/src/diff/components/BareWorkbenchFileView.stories.tsx +27 -0
  85. package/src/diff/components/BareWorkbenchFileView.tsx +63 -0
  86. package/src/diff/components/DiffContentView.scroll.test.tsx +434 -0
  87. package/src/diff/components/DiffContentView.split.test.tsx +140 -0
  88. package/src/diff/components/DiffContentView.stories.tsx +80 -0
  89. package/src/diff/components/DiffContentView.syntax.test.tsx +82 -0
  90. package/src/diff/components/DiffContentView.tsx +1055 -0
  91. package/src/diff/components/WalkWizard.stories.tsx +100 -0
  92. package/src/diff/components/WalkWizard.tsx +146 -0
  93. package/src/diff/components/diff-content-view-layout.test.ts +198 -0
  94. package/src/diff/components/diff-content-view-layout.ts +84 -0
  95. package/src/diff/diff-fold.test.ts +78 -0
  96. package/src/diff/diff-fold.ts +126 -0
  97. package/src/diff/diff-hunk-curate.test.ts +315 -0
  98. package/src/diff/diff-hunk-curate.ts +152 -0
  99. package/src/diff/diff-intraline.test.ts +239 -0
  100. package/src/diff/diff-intraline.ts +238 -0
  101. package/src/diff/diff-syntax.test.ts +125 -0
  102. package/src/diff/diff-syntax.ts +186 -0
  103. package/src/diff/split-diff.test.ts +110 -0
  104. package/src/diff/split-diff.ts +144 -0
  105. package/src/diff/view-diff.test.ts +325 -0
  106. package/src/diff/view-diff.ts +522 -0
  107. package/src/diff/walk.test.ts +121 -0
  108. package/src/diff/walk.ts +73 -0
  109. package/src/index.ts +31 -2
  110. package/src/integrations/attribution.test.ts +31 -0
  111. package/src/integrations/attribution.ts +42 -0
  112. package/src/integrations/client-extension-registry.test.tsx +61 -0
  113. package/src/integrations/client-extension-registry.ts +159 -0
  114. package/src/integrations/clipboard.test.ts +23 -0
  115. package/src/integrations/clipboard.ts +33 -0
  116. package/src/integrations/components/ExtensionZone.stories.tsx +28 -0
  117. package/src/integrations/components/ExtensionZone.tsx +145 -0
  118. package/src/integrations/components/ShareDialog.stories.tsx +121 -0
  119. package/src/integrations/components/ShareDialog.tsx +550 -0
  120. package/src/integrations/components/share-dialog-store.ts +55 -0
  121. package/src/integrations/editor.test.ts +149 -0
  122. package/src/integrations/editor.ts +160 -0
  123. package/src/integrations/github-identity.test.ts +58 -0
  124. package/src/integrations/github-identity.ts +41 -0
  125. package/src/integrations/herdr.test.ts +43 -0
  126. package/src/integrations/herdr.ts +38 -0
  127. package/src/integrations/integrations.test.ts +41 -0
  128. package/src/integrations/integrations.ts +32 -0
  129. package/src/integrations/pasted-image.test.ts +67 -0
  130. package/src/integrations/pasted-image.ts +60 -0
  131. package/src/integrations/share.ts +238 -0
  132. package/src/keyboard/components/CompletionOverlay.stories.tsx +37 -0
  133. package/src/keyboard/components/CompletionOverlay.tsx +75 -0
  134. package/src/keyboard/components/HandleChipsInput.stories.tsx +24 -0
  135. package/src/keyboard/components/HandleChipsInput.test.tsx +35 -0
  136. package/src/keyboard/components/HandleChipsInput.tsx +143 -0
  137. package/src/keyboard/components/KeybindsDialog.stories.tsx +31 -0
  138. package/src/keyboard/components/KeybindsDialog.tsx +103 -0
  139. package/src/keyboard/components/NavModeHint.stories.tsx +40 -0
  140. package/src/keyboard/components/NavModeHint.tsx +31 -0
  141. package/src/keyboard/components/SlashComposer.stories.tsx +37 -0
  142. package/src/keyboard/components/SlashComposer.tsx +87 -0
  143. package/src/keyboard/key-bindings.test.ts +156 -0
  144. package/src/keyboard/key-bindings.ts +459 -0
  145. package/src/keyboard/keymap.test.ts +611 -0
  146. package/src/keyboard/keymap.ts +395 -0
  147. package/src/keyboard/skills.test.ts +74 -0
  148. package/src/keyboard/skills.ts +75 -0
  149. package/src/keyboard/slash-palette.test.ts +103 -0
  150. package/src/keyboard/slash-palette.ts +129 -0
  151. package/src/keyboard/thread-chords.test.ts +79 -0
  152. package/src/keyboard/thread-chords.ts +163 -0
  153. package/src/keyboard/use-shared-keyboard.ts +55 -0
  154. package/src/markdown/__snapshots__/view-plan.regression.test.ts.snap +100 -0
  155. package/src/markdown/components/MarkdownGridBlock.stories.tsx +33 -0
  156. package/src/markdown/components/MarkdownGridBlock.tsx +72 -0
  157. package/src/markdown/components/MarkdownThreadEditor.stories.tsx +15 -0
  158. package/src/markdown/components/MarkdownThreadEditor.test.tsx +98 -0
  159. package/src/markdown/components/MarkdownThreadEditor.tsx +160 -0
  160. package/src/markdown/components/ThreadView.selection.test.tsx +951 -0
  161. package/src/markdown/components/ThreadView.slash.test.tsx +146 -0
  162. package/src/markdown/components/ThreadView.stories.tsx +26 -0
  163. package/src/markdown/components/ThreadView.tsx +560 -0
  164. package/src/markdown/mark-runs.ts +121 -0
  165. package/src/markdown/markdown-editor-syntax-style.ts +50 -0
  166. package/src/markdown/markdown-grid.test.ts +101 -0
  167. package/src/markdown/markdown-grid.ts +203 -0
  168. package/src/markdown/markdown-highlight.test.ts +61 -0
  169. package/src/markdown/markdown-highlight.ts +124 -0
  170. package/src/markdown/thread-code-syntax.ts +45 -0
  171. package/src/markdown/use-thread-body-editing.tsx +54 -0
  172. package/src/markdown/view-plan.regression.test.ts +60 -0
  173. package/src/markdown/view-plan.test.ts +545 -0
  174. package/src/markdown/view-plan.ts +1009 -0
  175. package/src/performance/otlp-trace.test.ts +119 -0
  176. package/src/performance/otlp-trace.ts +134 -0
  177. package/src/performance/perf-report.ts +18 -0
  178. package/src/performance/perf-timings.test.ts +18 -0
  179. package/src/performance/perf-timings.ts +39 -0
  180. package/src/prototype/components/PrototypeContentView.stories.tsx +19 -0
  181. package/src/prototype/components/PrototypeContentView.tsx +442 -0
  182. package/src/prototype/prototype-browser.test.ts +81 -0
  183. package/src/prototype/prototype-browser.ts +321 -0
  184. package/src/prototype/prototype-pixels.tsx +35 -0
  185. package/src/settings/components/SettingsDialog.stories.tsx +117 -0
  186. package/src/settings/components/SettingsDialog.tsx +183 -0
  187. package/src/settings/components/SettingsRows.stories.tsx +18 -0
  188. package/src/settings/components/SettingsRows.tsx +112 -0
  189. package/src/settings/components/quick-actions-editor.stories.tsx +58 -0
  190. package/src/settings/components/quick-actions-editor.test.tsx +120 -0
  191. package/src/settings/components/quick-actions-editor.tsx +222 -0
  192. package/src/settings/config-identity.test.ts +78 -0
  193. package/src/settings/config.test.ts +676 -0
  194. package/src/settings/config.ts +628 -0
  195. package/src/settings/use-settings-dialog.tsx +351 -0
  196. package/src/stories/__snapshots__/stories.test.tsx.snap +2358 -0
  197. package/src/stories/stories-app.tsx +196 -0
  198. package/src/stories/stories.test.tsx +74 -0
  199. package/src/stories/story-fixtures.ts +102 -0
  200. package/src/stories/story-tree.test.ts +108 -0
  201. package/src/stories/story.ts +154 -0
  202. package/src/terminal/ansi-screen.test.ts +28 -0
  203. package/src/terminal/ansi-screen.ts +72 -0
  204. package/src/terminal/components/AnsiScreen.stories.tsx +12 -0
  205. package/src/terminal/components/AnsiScreen.tsx +28 -0
  206. package/src/terminal/components/terminal-pane.test.tsx +40 -0
  207. package/src/terminal/components/terminal-pane.ts +196 -0
  208. package/src/terminal/ghostty-terminal.test.ts +85 -0
  209. package/src/terminal/ghostty-terminal.ts +197 -0
  210. package/src/terminal/kitty-image.test.ts +66 -0
  211. package/src/terminal/kitty-image.ts +137 -0
  212. package/src/terminal/pty.test.ts +81 -0
  213. package/src/terminal/pty.ts +246 -0
  214. package/src/testing/test-support.test.ts +264 -0
  215. package/src/testing/test-support.ts +367 -0
  216. package/src/thread/agent-continuation.test.ts +32 -0
  217. package/src/thread/agent-continuation.ts +13 -0
  218. package/src/thread/agent-transcript.test.ts +161 -0
  219. package/src/thread/agent-transcript.ts +336 -0
  220. package/src/thread/components/AgentConfigControls.stories.tsx +39 -0
  221. package/src/thread/components/AgentConfigControls.test.tsx +112 -0
  222. package/src/thread/components/AgentConfigControls.tsx +114 -0
  223. package/src/thread/components/AgentThreadPane.stories.tsx +105 -0
  224. package/src/thread/components/AgentThreadPane.test.tsx +829 -0
  225. package/src/thread/components/AgentThreadPane.tsx +367 -0
  226. package/src/thread/components/ThreadFooter.stories.tsx +30 -0
  227. package/src/thread/components/ThreadFooter.test.tsx +49 -0
  228. package/src/thread/components/ThreadFooter.tsx +77 -0
  229. package/src/thread/components/ThreadTree.stories.tsx +46 -0
  230. package/src/thread/components/ThreadTree.test.tsx +41 -0
  231. package/src/thread/components/ThreadTree.tsx +267 -0
  232. package/src/thread/components/thread-tree.test.ts +137 -0
  233. package/src/thread/components/thread-tree.ts +112 -0
  234. package/src/thread/thread-controller.curate.test.ts +502 -0
  235. package/src/thread/thread-controller.derivation.test.ts +62 -0
  236. package/src/thread/thread-controller.live-diff.test.ts +357 -0
  237. package/src/thread/thread-controller.optimistic.test.ts +69 -0
  238. package/src/thread/thread-controller.share.test.ts +573 -0
  239. package/src/thread/thread-controller.switch-cache.test.ts +174 -0
  240. package/src/thread/thread-controller.tree.test.ts +263 -0
  241. package/src/thread/thread-controller.ts +2018 -0
  242. package/src/thread/tree-view.test.ts +128 -0
  243. package/src/thread/tree-view.ts +146 -0
  244. package/src/thread/use-thread-agent.test.tsx +208 -0
  245. package/src/thread/use-thread-agent.ts +111 -0
  246. package/src/ui/components/CodeBlock.stories.tsx +42 -0
  247. package/src/ui/components/CodeBlock.tsx +80 -0
  248. package/src/ui/components/ConfirmCard.stories.tsx +37 -0
  249. package/src/ui/components/ConfirmCard.tsx +151 -0
  250. package/src/ui/components/ConfirmDialog.stories.tsx +21 -0
  251. package/src/ui/components/ConfirmDialog.tsx +72 -0
  252. package/src/ui/components/OverlayScrollbar.stories.tsx +33 -0
  253. package/src/ui/components/OverlayScrollbar.test.tsx +139 -0
  254. package/src/ui/components/OverlayScrollbar.tsx +129 -0
  255. package/src/ui/components/PromptDialog.stories.tsx +36 -0
  256. package/src/ui/components/PromptDialog.test.tsx +65 -0
  257. package/src/ui/components/PromptDialog.tsx +99 -0
  258. package/src/ui/components/RootOverlay.stories.tsx +45 -0
  259. package/src/ui/components/RootOverlay.tsx +49 -0
  260. package/src/ui/components/ScrollArea.stories.tsx +20 -0
  261. package/src/ui/components/ScrollArea.tsx +61 -0
  262. package/src/ui/components/Toast.stories.tsx +19 -0
  263. package/src/ui/components/Toast.tsx +54 -0
  264. package/src/ui/components/Tooltip.stories.tsx +26 -0
  265. package/src/ui/components/Tooltip.tsx +72 -0
  266. package/src/ui/components/menu-control.stories.tsx +35 -0
  267. package/src/ui/components/menu-control.tsx +43 -0
  268. package/src/ui/components/primitives/Button.stories.tsx +32 -0
  269. package/src/ui/components/primitives/Button.tsx +57 -0
  270. package/src/ui/components/primitives/Card.stories.tsx +25 -0
  271. package/src/ui/components/primitives/Card.tsx +62 -0
  272. package/src/ui/components/primitives/Dialog.stories.tsx +24 -0
  273. package/src/ui/components/primitives/Dialog.test.tsx +38 -0
  274. package/src/ui/components/primitives/Dialog.tsx +69 -0
  275. package/src/ui/components/primitives/DialogActions.stories.tsx +20 -0
  276. package/src/ui/components/primitives/DialogActions.tsx +39 -0
  277. package/src/ui/components/primitives/IconButton.stories.tsx +36 -0
  278. package/src/ui/components/primitives/IconButton.tsx +58 -0
  279. package/src/ui/components/primitives/StatusBar.stories.tsx +11 -0
  280. package/src/ui/components/primitives/StatusBar.tsx +23 -0
  281. package/src/ui/components/primitives/Switch.stories.tsx +17 -0
  282. package/src/ui/components/primitives/Switch.tsx +32 -0
  283. package/src/ui/components/primitives/Tabs.stories.tsx +29 -0
  284. package/src/ui/components/primitives/Tabs.tsx +101 -0
  285. package/src/ui/components/primitives/Toolbar.stories.tsx +17 -0
  286. package/src/ui/components/primitives/Toolbar.tsx +14 -0
  287. package/src/ui/components/primitives/Tree.stories.tsx +124 -0
  288. package/src/ui/components/primitives/Tree.tsx +133 -0
  289. package/src/ui/components/primitives/frame.test.ts +12 -0
  290. package/src/ui/components/primitives/frame.ts +17 -0
  291. package/src/ui/components/primitives/icons.ts +61 -0
  292. package/src/ui/components/primitives/tree-model.test.ts +99 -0
  293. package/src/ui/components/primitives/tree-model.ts +137 -0
  294. package/src/ui/components/syntax-highlight.ts +133 -0
  295. package/src/ui/components/truncate-text.ts +7 -0
  296. package/src/ui/components/truncate-title.test.ts +31 -0
  297. package/src/ui/components/truncate-title.ts +15 -0
  298. package/src/ui/use-frame-measure.ts +74 -0
  299. package/src/ui/use-terminal-virtualizer.ts +259 -0
  300. package/src/workbench/components/ChangesColumn.stories.tsx +44 -0
  301. package/src/workbench/components/ChangesColumn.tsx +203 -0
  302. package/src/workbench/components/ChangesFileTree.test.tsx +181 -0
  303. package/src/workbench/components/EditorGrid.split.test.tsx +118 -0
  304. package/src/workbench/components/EditorGrid.stories.tsx +86 -0
  305. package/src/workbench/components/EditorGrid.tabs.test.tsx +118 -0
  306. package/src/workbench/components/EditorGrid.tsx +544 -0
  307. package/src/workbench/components/EditorGrid.zoom.test.tsx +132 -0
  308. package/src/workbench/components/FileContentsView.stories.tsx +16 -0
  309. package/src/workbench/components/FileContentsView.test.tsx +29 -0
  310. package/src/workbench/components/FileContentsView.tsx +100 -0
  311. package/src/workbench/components/GridTabContent.actions.test.tsx +151 -0
  312. package/src/workbench/components/GridTabContent.stories.tsx +59 -0
  313. package/src/workbench/components/GridTabContent.tsx +193 -0
  314. package/src/workbench/components/ProjectTreeView.stories.tsx +25 -0
  315. package/src/workbench/components/ProjectTreeView.test.tsx +236 -0
  316. package/src/workbench/components/ProjectTreeView.tsx +151 -0
  317. package/src/workbench/components/TreePane.stories.tsx +106 -0
  318. package/src/workbench/components/TreePane.tsx +155 -0
  319. package/src/workbench/components/WelcomePlayground.stories.tsx +12 -0
  320. package/src/workbench/components/WelcomePlayground.test.tsx +83 -0
  321. package/src/workbench/components/WelcomePlayground.tsx +138 -0
  322. package/src/workbench/components/editor-grid.test.ts +184 -0
  323. package/src/workbench/components/editor-grid.ts +281 -0
  324. package/src/workbench/components/file-tree.test.ts +60 -0
  325. package/src/workbench/components/file-tree.ts +112 -0
  326. package/src/workbench/use-changes-workbench.ts +205 -0
  327. package/src/workbench/use-remember-layout.ts +31 -0
  328. package/src/workbench/use-repo-changes.ts +91 -0
  329. package/src/workbench/workbench-snapshot.test.ts +99 -0
  330. package/src/workbench/workbench-snapshot.ts +59 -0
  331. package/src/App.diff.test.tsx +0 -99
  332. package/src/App.export.test.tsx +0 -80
  333. package/src/App.readonly.test.tsx +0 -148
  334. package/src/App.test.tsx +0 -217
  335. package/src/App.tsx +0 -0
  336. package/src/config.test.ts +0 -116
  337. package/src/config.ts +0 -113
  338. package/src/editor.ts +0 -34
  339. package/src/run.ts +0 -28
  340. package/src/serve.ts +0 -75
  341. package/src/theme.ts +0 -46
  342. package/src/view-diff.test.ts +0 -55
  343. package/src/view-diff.ts +0 -86
  344. package/src/view.test.ts +0 -157
  345. package/src/view.ts +0 -0
@@ -0,0 +1,676 @@
1
+ import { 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 {
6
+ DEFAULT_KEYS,
7
+ DEFAULT_QUICK_ACTIONS,
8
+ actionFor,
9
+ loadConfig,
10
+ persistAuthorName,
11
+ persistActions,
12
+ persistLayout,
13
+ persistPins,
14
+ persistReviewSkill,
15
+ persistReviewWorkspace,
16
+ persistTheme,
17
+ quickActionBody,
18
+ resolveQuickAction,
19
+ } from "./config";
20
+ import { DARK } from "../appearance/theme";
21
+ import { themeForName } from "../appearance/theme-presets";
22
+
23
+ describe("loadConfig", () => {
24
+ test("review defaults use the bundled skill in an isolated worktree", () => {
25
+ const config = loadConfig({ userConfigPath: "/nonexistent/config.toml" });
26
+
27
+ expect(config.review).toEqual({ skill: "code-review", workspace: "worktree" });
28
+ });
29
+
30
+ test("review settings load and persist without changing other tables", () => {
31
+ const dir = mkdtempSync(join(tmpdir(), "cueloop-review-config-"));
32
+ const path = join(dir, "config.toml");
33
+
34
+ writeFileSync(path, '[ui]\ntheme = "cueloop"\n\n[review]\nskill = "my-review"\n');
35
+ try {
36
+ persistReviewSkill("code-review", path);
37
+ persistReviewWorkspace("current", path);
38
+
39
+ expect(loadConfig({ userConfigPath: path }).review).toEqual({
40
+ skill: "code-review",
41
+ workspace: "current",
42
+ });
43
+ expect(readFileSync(path, "utf8")).toContain('[ui]\ntheme = "cueloop"');
44
+ } finally {
45
+ rmSync(dir, { recursive: true, force: true });
46
+ }
47
+ });
48
+
49
+ test("review settings update a table whose header has a comment", () => {
50
+ const dir = mkdtempSync(join(tmpdir(), "cueloop-review-commented-config-"));
51
+ const path = join(dir, "config.toml");
52
+
53
+ writeFileSync(path, '[review] # preferred settings\nskill = "my-review"\n');
54
+ try {
55
+ persistReviewSkill("code-review", path);
56
+
57
+ const text = readFileSync(path, "utf8");
58
+
59
+ expect(loadConfig({ userConfigPath: path }).review.skill).toBe("code-review");
60
+ expect(text.match(/^\[review\]/gm)).toHaveLength(1);
61
+ expect(text).toContain("[review] # preferred settings");
62
+ } finally {
63
+ rmSync(dir, { recursive: true, force: true });
64
+ }
65
+ });
66
+
67
+ test("review settings update a CRLF table without duplicating it", () => {
68
+ const dir = mkdtempSync(join(tmpdir(), "cueloop-review-crlf-config-"));
69
+ const path = join(dir, "config.toml");
70
+
71
+ writeFileSync(path, '[review]\r\nskill = "code-review"\r\nworkspace = "worktree"\r\n');
72
+ try {
73
+ persistReviewWorkspace("current", path);
74
+
75
+ const text = readFileSync(path, "utf8");
76
+
77
+ expect(loadConfig({ userConfigPath: path }).review.workspace).toBe("current");
78
+ expect(text.match(/^\[review\]/gm)).toHaveLength(1);
79
+ } finally {
80
+ rmSync(dir, { recursive: true, force: true });
81
+ }
82
+ });
83
+ test("defaults when no file exists", () => {
84
+ // Act
85
+ const config = loadConfig({ userConfigPath: "/nonexistent/config.toml" });
86
+
87
+ // Assert
88
+ expect(config.keys["comment"]).toEqual(["c"]);
89
+ expect(config.theme.accent).toBe(DARK.accent);
90
+ });
91
+
92
+ test("user config rebinds actions and overrides theme tokens", () => {
93
+ // Arrange
94
+ const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg-"));
95
+ const path = join(dir, "config.toml");
96
+
97
+ writeFileSync(
98
+ path,
99
+ `[keys]\ncomment = "a"\nsubmit = ["return", "S"]\n\n[theme]\naccent = "#ff0000"\n`,
100
+ );
101
+
102
+ try {
103
+ // Act
104
+ const config = loadConfig({ userConfigPath: path });
105
+
106
+ // Assert
107
+ expect(config.keys["comment"]).toEqual(["a"]);
108
+ expect(config.keys["submit"]).toEqual(["return", "S"]);
109
+ expect(config.keys["cut"]).toEqual(["x"]); // untouched defaults survive
110
+ expect(config.theme.accent).toBe("#ff0000");
111
+ expect(config.theme.green).toBe(DARK.green);
112
+ } finally {
113
+ rmSync(dir, { recursive: true, force: true });
114
+ }
115
+ });
116
+
117
+ test("repo config layers over user config", () => {
118
+ // Arrange
119
+ const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg2-"));
120
+ const user = join(dir, "user.toml");
121
+ const repoRoot = join(dir, "repo");
122
+
123
+ writeFileSync(user, `[keys]\ncomment = "a"\n`);
124
+ Bun.spawnSync(["mkdir", "-p", join(repoRoot, ".cueloop")]);
125
+ writeFileSync(join(repoRoot, ".cueloop", "config.toml"), `[keys]\ncomment = "z"\n`);
126
+
127
+ try {
128
+ // Act
129
+ const config = loadConfig({ userConfigPath: user, repoRoot });
130
+
131
+ // Assert
132
+ expect(config.keys["comment"]).toEqual(["z"]);
133
+ } finally {
134
+ rmSync(dir, { recursive: true, force: true });
135
+ }
136
+ });
137
+
138
+ test("broken config never blocks a review", () => {
139
+ // Arrange
140
+ const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg3-"));
141
+ const path = join(dir, "config.toml");
142
+
143
+ writeFileSync(path, "not [valid toml");
144
+
145
+ try {
146
+ // Act
147
+ const config = loadConfig({ userConfigPath: path });
148
+
149
+ // Assert
150
+ expect(config.keys["comment"]).toEqual(["c"]);
151
+ } finally {
152
+ rmSync(dir, { recursive: true, force: true });
153
+ }
154
+ });
155
+
156
+ test("[ui] parses auto_close and the editor override", () => {
157
+ // Arrange
158
+ const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg4-"));
159
+ const path = join(dir, "config.toml");
160
+
161
+ writeFileSync(path, `[ui]\nauto_close = 3\neditor = "code --wait"\n`);
162
+
163
+ try {
164
+ // Act
165
+ const config = loadConfig({ userConfigPath: path });
166
+
167
+ // Assert
168
+ expect(config.ui.autoClose).toBe(3);
169
+ expect(config.ui.editor).toBe("code --wait");
170
+ } finally {
171
+ rmSync(dir, { recursive: true, force: true });
172
+ }
173
+ });
174
+
175
+ test("[ui] editor defaults to undefined and ignores a blank value", () => {
176
+ // Arrange
177
+ const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg5-"));
178
+ const path = join(dir, "config.toml");
179
+
180
+ writeFileSync(path, `[ui]\neditor = " "\n`);
181
+
182
+ try {
183
+ // Assert
184
+ expect(loadConfig({ userConfigPath: "/nonexistent/config.toml" }).ui.editor).toBeUndefined();
185
+ expect(loadConfig({ userConfigPath: path }).ui.editor).toBeUndefined();
186
+ } finally {
187
+ rmSync(dir, { recursive: true, force: true });
188
+ }
189
+ });
190
+
191
+ test("[ui] diff_view defaults to split and parses stacked", () => {
192
+ // Arrange
193
+ const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg-diff-"));
194
+ const path = join(dir, "config.toml");
195
+
196
+ writeFileSync(path, `[ui]\ndiff_view = "stacked"\n`);
197
+
198
+ try {
199
+ // Assert
200
+ expect(loadConfig({ userConfigPath: "/nonexistent/config.toml" }).ui.diffView).toBe("split");
201
+ expect(loadConfig({ userConfigPath: path }).ui.diffView).toBe("stacked");
202
+ } finally {
203
+ rmSync(dir, { recursive: true, force: true });
204
+ }
205
+ });
206
+
207
+ test("[ui] default_message defaults to approve and parses changes_requested", () => {
208
+ // Arrange
209
+ const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg-message-"));
210
+ const path = join(dir, "config.toml");
211
+
212
+ writeFileSync(path, `[ui]\ndefault_message = "changes_requested"\n`);
213
+
214
+ try {
215
+ // Assert
216
+ expect(loadConfig({ userConfigPath: "/nonexistent/config.toml" }).ui.defaultMessage).toBe(
217
+ "approved",
218
+ );
219
+ expect(loadConfig({ userConfigPath: path }).ui.defaultMessage).toBe("changes_requested");
220
+ } finally {
221
+ rmSync(dir, { recursive: true, force: true });
222
+ }
223
+ });
224
+
225
+ test("[skills] path overrides the default and a malformed value never discards the config", () => {
226
+ const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg-skills-"));
227
+ const path = join(dir, "config.toml");
228
+
229
+ try {
230
+ writeFileSync(path, `[skills]\npath = "/custom/skills"\n`);
231
+ expect(loadConfig({ userConfigPath: path }).skillsPath).toBe("/custom/skills");
232
+
233
+ // a wrong-typed path falls back to the default without dropping the sibling [keys] setting
234
+ writeFileSync(path, `[skills]\npath = 123\n\n[keys]\ncomment = "z"\n`);
235
+ const config = loadConfig({ userConfigPath: path });
236
+
237
+ expect(config.keys["comment"]).toEqual(["z"]);
238
+ expect(config.skillsPath).toContain(".agents");
239
+ } finally {
240
+ rmSync(dir, { recursive: true, force: true });
241
+ }
242
+ });
243
+
244
+ test('[ui] diff_view loads the pre-rename "unified" value as "stacked"', () => {
245
+ // Arrange - an upgrade must not flip a user who had persisted the old spelling
246
+ const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg-diff-"));
247
+ const path = join(dir, "config.toml");
248
+
249
+ writeFileSync(path, `[ui]\ndiff_view = "unified"\n`);
250
+
251
+ try {
252
+ // Assert
253
+ expect(loadConfig({ userConfigPath: path }).ui.diffView).toBe("stacked");
254
+ } finally {
255
+ rmSync(dir, { recursive: true, force: true });
256
+ }
257
+ });
258
+
259
+ test("[ui] theme defaults to the branded cueloop preset", () => {
260
+ // Act
261
+ const config = loadConfig({ userConfigPath: "/nonexistent/config.toml" });
262
+
263
+ // Assert
264
+ expect(config.ui.theme).toBe("cueloop");
265
+ expect(config.theme.accent).toBe(DARK.accent);
266
+ });
267
+
268
+ test("[ui] theme selects a named preset as the token base", () => {
269
+ // Arrange
270
+ const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg-theme-"));
271
+ const path = join(dir, "config.toml");
272
+
273
+ writeFileSync(path, `[ui]\ntheme = "nord"\n`);
274
+
275
+ try {
276
+ // Act
277
+ const config = loadConfig({ userConfigPath: path });
278
+
279
+ // Assert
280
+ expect(config.ui.theme).toBe("nord");
281
+ expect(config.theme).toEqual(themeForName("nord"));
282
+ } finally {
283
+ rmSync(dir, { recursive: true, force: true });
284
+ }
285
+ });
286
+
287
+ test("[theme] token overrides layer on top of the selected preset", () => {
288
+ // Arrange
289
+ const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg-theme2-"));
290
+ const path = join(dir, "config.toml");
291
+
292
+ writeFileSync(path, `[ui]\ntheme = "nord"\n\n[theme]\naccent = "#ff0000"\n`);
293
+
294
+ try {
295
+ // Act
296
+ const config = loadConfig({ userConfigPath: path });
297
+
298
+ // Assert
299
+ expect(config.theme.accent).toBe("#ff0000"); // override wins
300
+ expect(config.theme.background).toBe(themeForName("nord").background); // preset base survives
301
+ expect(config.themeOverrides).toEqual({ accent: "#ff0000" }); // deltas exposed for live re-compose
302
+ } finally {
303
+ rmSync(dir, { recursive: true, force: true });
304
+ }
305
+ });
306
+
307
+ test("a later file's preset keeps an earlier file's [theme] override", () => {
308
+ // Arrange - user overrides accent; repo picks a preset in a separate file
309
+ const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg-theme-x-"));
310
+ const user = join(dir, "user.toml");
311
+ const repoRoot = join(dir, "repo");
312
+
313
+ writeFileSync(user, `[theme]\naccent = "#ff0000"\n`);
314
+ Bun.spawnSync(["mkdir", "-p", join(repoRoot, ".cueloop")]);
315
+ writeFileSync(join(repoRoot, ".cueloop", "config.toml"), `[ui]\ntheme = "nord"\n`);
316
+
317
+ try {
318
+ // Act
319
+ const config = loadConfig({ userConfigPath: user, repoRoot });
320
+
321
+ // Assert - the preset is the base, but the earlier override still wins its token
322
+ expect(config.ui.theme).toBe("nord");
323
+ expect(config.theme.background).toBe(themeForName("nord").background);
324
+ expect(config.theme.accent).toBe("#ff0000");
325
+ } finally {
326
+ rmSync(dir, { recursive: true, force: true });
327
+ }
328
+ });
329
+
330
+ test("[ui] ignores an unknown theme name and keeps the default", () => {
331
+ // Arrange
332
+ const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg-theme3-"));
333
+ const path = join(dir, "config.toml");
334
+
335
+ writeFileSync(path, `[ui]\ntheme = "solarized-galaxy"\n`);
336
+
337
+ try {
338
+ // Act
339
+ const config = loadConfig({ userConfigPath: path });
340
+
341
+ // Assert
342
+ expect(config.ui.theme).toBe("cueloop");
343
+ expect(config.theme.accent).toBe(DARK.accent);
344
+ } finally {
345
+ rmSync(dir, { recursive: true, force: true });
346
+ }
347
+ });
348
+
349
+ test("persistTheme round-trips through the config file", () => {
350
+ // Arrange
351
+ const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg-theme4-"));
352
+ const path = join(dir, "config.toml");
353
+
354
+ try {
355
+ // Act
356
+ persistTheme("tokyo-night", path);
357
+
358
+ // Assert
359
+ expect(loadConfig({ userConfigPath: path }).ui.theme).toBe("tokyo-night");
360
+
361
+ // a second write replaces the key in place
362
+ // Act
363
+ persistTheme("gruvbox-dark", path);
364
+
365
+ // Assert
366
+ expect(loadConfig({ userConfigPath: path }).ui.theme).toBe("gruvbox-dark");
367
+ } finally {
368
+ rmSync(dir, { recursive: true, force: true });
369
+ }
370
+ });
371
+
372
+ test("persistPins round-trips the pinned-thread ids through the config file", () => {
373
+ // Arrange
374
+ const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg-pins-"));
375
+ const path = join(dir, "config.toml");
376
+
377
+ try {
378
+ // Act
379
+ persistPins(["s_one", "s_two"], path);
380
+
381
+ // Assert
382
+ expect(loadConfig({ userConfigPath: path }).ui.pins).toEqual(["s_one", "s_two"]);
383
+
384
+ // unpinning rewrites the list in place, including down to empty
385
+ // Act
386
+ persistPins(["s_two"], path);
387
+
388
+ // Assert
389
+ expect(loadConfig({ userConfigPath: path }).ui.pins).toEqual(["s_two"]);
390
+
391
+ // Act
392
+ persistPins([], path);
393
+
394
+ // Assert
395
+ expect(loadConfig({ userConfigPath: path }).ui.pins).toEqual([]);
396
+ } finally {
397
+ rmSync(dir, { recursive: true, force: true });
398
+ }
399
+ });
400
+
401
+ test("persistLayout round-trips the remembered pane composition through the config file", () => {
402
+ // Arrange
403
+ const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg-layout-"));
404
+ const path = join(dir, "config.toml");
405
+
406
+ try {
407
+ // Act
408
+ persistLayout({ threads: false, rightSidebar: "project", zoomChanges: false }, path);
409
+
410
+ // Assert
411
+ expect(loadConfig({ userConfigPath: path }).ui.layout).toEqual({
412
+ threads: false,
413
+ rightSidebar: "project",
414
+ zoomChanges: false,
415
+ });
416
+
417
+ // Act
418
+ persistLayout({ threads: true, rightSidebar: "off", zoomChanges: true }, path);
419
+
420
+ // Assert
421
+ expect(loadConfig({ userConfigPath: path }).ui.layout).toEqual({
422
+ threads: true,
423
+ rightSidebar: "off",
424
+ zoomChanges: true,
425
+ });
426
+ } finally {
427
+ rmSync(dir, { recursive: true, force: true });
428
+ }
429
+ });
430
+
431
+ test("layout is unset until the user changes one", () => {
432
+ // Assert
433
+ expect(loadConfig({ userConfigPath: "/nonexistent/config.toml" }).ui.layout).toBeUndefined();
434
+ });
435
+ });
436
+
437
+ describe("integrations.obsidian config", () => {
438
+ test("defaults when the section is absent", () => {
439
+ // Act
440
+ const config = loadConfig({ userConfigPath: "/nonexistent/config.toml" });
441
+
442
+ // Assert
443
+ expect(config.integrations.obsidian.vault).toBeUndefined();
444
+ expect(config.integrations.obsidian.folder).toBe("cueloop");
445
+ expect(config.integrations.obsidian.filenameFormat).toBe("{YYYY}-{MM}-{DD} - {title}");
446
+ expect(config.integrations.obsidian.separator).toBe("space");
447
+ expect(config.integrations.obsidian.exportOn).toBe("manual");
448
+ });
449
+
450
+ test("user config sets the section; invalid enum values are ignored", () => {
451
+ // Arrange
452
+ const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg-obs-"));
453
+ const path = join(dir, "config.toml");
454
+
455
+ writeFileSync(
456
+ path,
457
+ `[integrations.obsidian]\nvault = "/notes/vault"\nfolder = "plans"\nexportOn = "approved"\nseparator = "comma"\n`,
458
+ );
459
+
460
+ try {
461
+ // Act
462
+ const config = loadConfig({ userConfigPath: path });
463
+
464
+ // Assert
465
+ expect(config.integrations.obsidian.vault).toBe("/notes/vault");
466
+ expect(config.integrations.obsidian.folder).toBe("plans");
467
+ expect(config.integrations.obsidian.exportOn).toBe("approved");
468
+ expect(config.integrations.obsidian.separator).toBe("space"); // invalid value falls back
469
+ expect(config.keys["comment"]).toEqual(["c"]); // other sections untouched
470
+ } finally {
471
+ rmSync(dir, { recursive: true, force: true });
472
+ }
473
+ });
474
+
475
+ test("repo config layers over user config for the section", () => {
476
+ // Arrange
477
+ const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg-obs2-"));
478
+ const user = join(dir, "user.toml");
479
+ const repoRoot = join(dir, "repo");
480
+
481
+ writeFileSync(user, `[integrations.obsidian]\nvault = "/user/vault"\nexportOn = "message"\n`);
482
+ Bun.spawnSync(["mkdir", "-p", join(repoRoot, ".cueloop")]);
483
+ writeFileSync(
484
+ join(repoRoot, ".cueloop", "config.toml"),
485
+ `[integrations.obsidian]\nfolder = "repo-plans"\n`,
486
+ );
487
+
488
+ try {
489
+ // Act
490
+ const config = loadConfig({ userConfigPath: user, repoRoot });
491
+
492
+ // Assert
493
+ expect(config.integrations.obsidian.vault).toBe("/user/vault"); // user layer survives
494
+ expect(config.integrations.obsidian.exportOn).toBe("message");
495
+ expect(config.integrations.obsidian.folder).toBe("repo-plans"); // repo layer wins
496
+ } finally {
497
+ rmSync(dir, { recursive: true, force: true });
498
+ }
499
+ });
500
+ });
501
+
502
+ describe("quick actions ([[actions]])", () => {
503
+ test("absent config keeps the built-in defaults", () => {
504
+ // Act
505
+ const config = loadConfig({ userConfigPath: "/nonexistent/config.toml" });
506
+
507
+ // Assert
508
+ expect(config.actions).toEqual(DEFAULT_QUICK_ACTIONS);
509
+ expect(config.actions).toHaveLength(8);
510
+ });
511
+
512
+ test("configured actions replace the defaults, with optional metadata", () => {
513
+ // Arrange
514
+ const dir = mkdtempSync(join(tmpdir(), "cueloop-actions-"));
515
+ const path = join(dir, "config.toml");
516
+
517
+ writeFileSync(
518
+ path,
519
+ `[[actions]]\nprompt = "Add a benchmark"\n\n[[actions]]\nprompt = "Guard the edge case"\nmetadata = "null and empty input"\n`,
520
+ );
521
+
522
+ try {
523
+ // Act
524
+ const config = loadConfig({ userConfigPath: path });
525
+
526
+ // Assert
527
+ expect(config.actions).toEqual([
528
+ { prompt: "Add a benchmark" },
529
+ { prompt: "Guard the edge case", metadata: "null and empty input" },
530
+ ]);
531
+ } finally {
532
+ rmSync(dir, { recursive: true, force: true });
533
+ }
534
+ });
535
+
536
+ test("a table missing prompt is skipped; the rest replace the defaults", () => {
537
+ // Arrange
538
+ const dir = mkdtempSync(join(tmpdir(), "cueloop-actions2-"));
539
+ const path = join(dir, "config.toml");
540
+
541
+ writeFileSync(
542
+ path,
543
+ `[[actions]]\nmetadata = "orphan without a prompt"\n\n[[actions]]\nprompt = "Keep this one"\n`,
544
+ );
545
+
546
+ try {
547
+ // Act
548
+ const config = loadConfig({ userConfigPath: path });
549
+
550
+ // Assert
551
+ expect(config.actions).toEqual([{ prompt: "Keep this one" }]);
552
+ } finally {
553
+ rmSync(dir, { recursive: true, force: true });
554
+ }
555
+ });
556
+ });
557
+
558
+ describe("persistActions", () => {
559
+ test("round-trips the action set through [[actions]] and replaces prior ones", () => {
560
+ // Arrange
561
+ const dir = mkdtempSync(join(tmpdir(), "cueloop-persist-actions-"));
562
+ const path = join(dir, "config.toml");
563
+
564
+ writeFileSync(path, `[[actions]]\nprompt = "old one"\n\n[ui]\nauto_close = 3\n`);
565
+
566
+ try {
567
+ // Act
568
+ persistActions(
569
+ [{ prompt: "Zoom out" }, { prompt: "Prototype", metadata: "skip tests and polish" }],
570
+ path,
571
+ );
572
+ const config = loadConfig({ userConfigPath: path });
573
+
574
+ // Assert - the old action is gone, the new set is read back, and [ui] survives
575
+ expect(config.actions).toEqual([
576
+ { prompt: "Zoom out" },
577
+ { prompt: "Prototype", metadata: "skip tests and polish" },
578
+ ]);
579
+ expect(config.ui.autoClose).toBe(3);
580
+ } finally {
581
+ rmSync(dir, { recursive: true, force: true });
582
+ }
583
+ });
584
+ });
585
+
586
+ describe("quickActionBody", () => {
587
+ test("appends the system prompt below the prompt when set", () => {
588
+ // Assert
589
+ expect(quickActionBody({ prompt: "Prototype this", metadata: "skip tests" })).toBe(
590
+ "Prototype this\n\nskip tests",
591
+ );
592
+ expect(quickActionBody({ prompt: "Out of scope" })).toBe("Out of scope");
593
+ });
594
+ });
595
+
596
+ describe("resolveQuickAction", () => {
597
+ test("resolves by 1-based index and by case-insensitive prompt", () => {
598
+ // Assert
599
+ expect(resolveQuickAction(DEFAULT_QUICK_ACTIONS, "1")).toEqual(DEFAULT_QUICK_ACTIONS[0]!);
600
+ expect(resolveQuickAction(DEFAULT_QUICK_ACTIONS, "out of scope")).toEqual(
601
+ DEFAULT_QUICK_ACTIONS[2]!,
602
+ );
603
+ });
604
+
605
+ test("returns undefined for an out-of-range index or an unknown name", () => {
606
+ // Assert
607
+ expect(resolveQuickAction(DEFAULT_QUICK_ACTIONS, "0")).toBeUndefined();
608
+ expect(resolveQuickAction(DEFAULT_QUICK_ACTIONS, "99")).toBeUndefined();
609
+ expect(resolveQuickAction(DEFAULT_QUICK_ACTIONS, "no such action")).toBeUndefined();
610
+ });
611
+ });
612
+
613
+ describe("actionFor", () => {
614
+ test("resolves keys and shifted keys to actions", () => {
615
+ // Assert
616
+ expect(actionFor(DEFAULT_KEYS, "j", false)).toBe("down");
617
+ expect(actionFor(DEFAULT_KEYS, "g", true)).toBe("bottom"); // G
618
+ expect(actionFor(DEFAULT_KEYS, "return", false)).toBe("submit");
619
+ expect(actionFor(DEFAULT_KEYS, "zz", false)).toBeUndefined();
620
+ });
621
+ });
622
+
623
+ describe("[authors] rename map", () => {
624
+ test("loads quoted fingerprint keys", () => {
625
+ // Arrange
626
+ const dir = mkdtempSync(join(tmpdir(), "cueloop-authors-"));
627
+ const path = join(dir, "config.toml");
628
+
629
+ writeFileSync(path, `[authors]\n"SHA256:abc+def/gh" = "Alex"\n`);
630
+
631
+ // Act
632
+ const config = loadConfig({ userConfigPath: path });
633
+
634
+ // Assert
635
+ expect(config.authors["SHA256:abc+def/gh"]).toBe("Alex");
636
+ rmSync(dir, { recursive: true, force: true });
637
+ });
638
+
639
+ test("persistAuthorName round-trips and updates a fingerprint id in place", () => {
640
+ // Arrange
641
+ const dir = mkdtempSync(join(tmpdir(), "cueloop-authors2-"));
642
+ const path = join(dir, "config.toml");
643
+
644
+ // Act / Assert
645
+ persistAuthorName("SHA256:abc+def/gh", "Alex", path);
646
+ expect(loadConfig({ userConfigPath: path }).authors["SHA256:abc+def/gh"]).toBe("Alex");
647
+ persistAuthorName("SHA256:abc+def/gh", "Alexa", path);
648
+ expect(loadConfig({ userConfigPath: path }).authors["SHA256:abc+def/gh"]).toBe("Alexa");
649
+ rmSync(dir, { recursive: true, force: true });
650
+ });
651
+ });
652
+
653
+ describe("the /lgtm default quick action", () => {
654
+ test("expands to a terse approving note", () => {
655
+ const lgtm = DEFAULT_QUICK_ACTIONS.find((action) => action.prompt.toLowerCase() === "lgtm");
656
+
657
+ expect(lgtm).toBeDefined();
658
+ // the reference is /lgtm, so the label is the prompt; it expands like every action (label, then note)
659
+ expect(quickActionBody(lgtm!)).toBe("LGTM\n\nThis looks good to me.");
660
+ });
661
+ });
662
+
663
+ test("Thread agents default off and require a boolean experimental TOML flag", () => {
664
+ const dir = mkdtempSync(join(tmpdir(), "cueloop-agent-config-"));
665
+ const path = join(dir, "config.toml");
666
+
667
+ try {
668
+ expect(loadConfig({ userConfigPath: path }).experimental.threadAgent).toBe(false);
669
+ writeFileSync(path, "[experimental]\nthread_agent = true\n");
670
+ expect(loadConfig({ userConfigPath: path }).experimental.threadAgent).toBe(true);
671
+ writeFileSync(path, '[experimental]\nthread_agent = "true"\n');
672
+ expect(loadConfig({ userConfigPath: path }).experimental.threadAgent).toBe(false);
673
+ } finally {
674
+ rmSync(dir, { recursive: true, force: true });
675
+ }
676
+ });