@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
@@ -0,0 +1,51 @@
1
+ /** The branded transparent theme adapts to the terminal background - a light terminal gets dark text instead of light-on-light - while the opaque palette presets ignore appearance. */
2
+
3
+ import { describe, expect, test } from "bun:test";
4
+ import { DARK, LIGHT } from "./theme";
5
+ import { composeTheme, themeForName } from "./theme-presets";
6
+
7
+ describe("themeForName appearance", () => {
8
+ test("the branded cueloop theme picks the light variant on a light terminal", () => {
9
+ // Assert
10
+ expect(themeForName("cueloop", "dark")).toBe(DARK);
11
+ expect(themeForName("cueloop", "light")).toBe(LIGHT);
12
+ });
13
+
14
+ test("defaults to the dark variant when appearance is unspecified", () => {
15
+ // Assert
16
+ expect(themeForName("cueloop")).toBe(DARK);
17
+ });
18
+
19
+ test("an unknown name falls back to the branded theme, still appearance-aware", () => {
20
+ // Assert
21
+ expect(themeForName("nope", "light")).toBe(LIGHT);
22
+ expect(themeForName("nope", "dark")).toBe(DARK);
23
+ });
24
+
25
+ test("an explicit opaque preset ignores appearance (it paints its own background)", () => {
26
+ // Assert
27
+ expect(themeForName("nord", "light")).toBe(themeForName("nord", "dark"));
28
+ });
29
+ });
30
+
31
+ describe("the branded variants", () => {
32
+ test("both keep the transparent background but the light variant darkens its text", () => {
33
+ // Assert
34
+ expect(DARK.background).toBe("transparent");
35
+ expect(LIGHT.background).toBe("transparent");
36
+ expect(LIGHT.text).toBe("#1c1d24");
37
+ expect(LIGHT.text).not.toBe(DARK.text);
38
+ expect(LIGHT.elevated).not.toBe(DARK.elevated);
39
+ });
40
+ });
41
+
42
+ describe("composeTheme", () => {
43
+ test("resolves the appearance base, then layers per-token overrides on top", () => {
44
+ // Act
45
+ const themed = composeTheme("cueloop", { accent: "#123456" }, "light");
46
+
47
+ // Assert
48
+ expect(themed.text).toBe(LIGHT.text);
49
+ expect(themed.accent).toBe("#123456");
50
+ });
51
+ });
@@ -0,0 +1,219 @@
1
+ /**
2
+ * Named theme presets selectable from Settings and `[ui] theme`. The branded
3
+ * "cueloop" default keeps the transparent terminal-through look; each of the
4
+ * well-known palettes paints its own opaque background so it reads exactly like
5
+ * the published theme. Every preset fills the full Theme token contract, so a
6
+ * `[theme]` per-token override still layers cleanly on top of the chosen base.
7
+ *
8
+ * Each palette maps its first-party spec onto cueloop's semantic slots. Where a
9
+ * palette has no dedicated green (Rosé Pine), additions use its positive accent
10
+ * by that palette's own convention.
11
+ */
12
+
13
+ import { DARK, LIGHT, type Theme } from "./theme";
14
+
15
+ /** Terminal background appearance, reported by the terminal (OSC) or assumed dark. */
16
+ export type Appearance = "dark" | "light";
17
+
18
+ /** The branded transparent theme's variant for the detected terminal background. */
19
+ function brandedTheme(appearance: Appearance): Theme {
20
+ return appearance === "light" ? LIGHT : DARK;
21
+ }
22
+
23
+ /** Selectable theme name; the persisted `[ui] theme` value and cycle key. */
24
+ export type ThemeName =
25
+ | "cueloop"
26
+ | "rose-pine-moon"
27
+ | "catppuccin-mocha"
28
+ | "tokyo-night"
29
+ | "gruvbox-dark"
30
+ | "nord";
31
+
32
+ /** The branded default; the base every layered config starts from. */
33
+ export const DEFAULT_THEME_NAME: ThemeName = "cueloop";
34
+
35
+ /** Rosé Pine Moon - rosepinetheme.com. Foam stands in for additions (no green in the palette). */
36
+ const ROSE_PINE_MOON: Theme = {
37
+ background: "#232136",
38
+ panel: "#232136",
39
+ elevated: "#2a273f",
40
+ border: "#44415a",
41
+ text: "#e0def4",
42
+ textMuted: "#908caa",
43
+ textDim: "#6e6a86",
44
+ accent: "#ea9a97",
45
+ accentInk: "#232136",
46
+ warning: "#f6c177",
47
+ green: "#9ccfd8",
48
+ red: "#eb6f92",
49
+ blue: "#3e8fb0",
50
+ cursorBackground: "#382a2f",
51
+ markCommentBackground: "#382a37",
52
+ insertedForeground: "#9ccfd8",
53
+ deletedForeground: "#eb6f92",
54
+ insertedBackground: "#243a3f",
55
+ insertedEmphasisBackground: "#31555c",
56
+ deletedBackground: "#3a2530",
57
+ deletedEmphasisBackground: "#563643",
58
+ backdrop: "transparent",
59
+ };
60
+
61
+ /** Catppuccin Mocha - catppuccin.com. Mauve is the signature accent. */
62
+ const CATPPUCCIN_MOCHA: Theme = {
63
+ background: "#1e1e2e",
64
+ panel: "#1e1e2e",
65
+ elevated: "#313244",
66
+ border: "#45475a",
67
+ text: "#cdd6f4",
68
+ textMuted: "#a6adc8",
69
+ textDim: "#6c7086",
70
+ accent: "#cba6f7",
71
+ accentInk: "#1e1e2e",
72
+ warning: "#fab387",
73
+ green: "#a6e3a1",
74
+ red: "#f38ba8",
75
+ blue: "#89b4fa",
76
+ cursorBackground: "#2e2740",
77
+ markCommentBackground: "#33222b",
78
+ insertedForeground: "#a6e3a1",
79
+ deletedForeground: "#f38ba8",
80
+ insertedBackground: "#26372b",
81
+ insertedEmphasisBackground: "#375140",
82
+ deletedBackground: "#382530",
83
+ deletedEmphasisBackground: "#553844",
84
+ backdrop: "transparent",
85
+ };
86
+
87
+ /** Tokyo Night - the folke "night" variant. Magenta is the signature accent. */
88
+ const TOKYO_NIGHT: Theme = {
89
+ background: "#1a1b26",
90
+ panel: "#1a1b26",
91
+ elevated: "#292e42",
92
+ border: "#3b4261",
93
+ text: "#c0caf5",
94
+ textMuted: "#a9b1d6",
95
+ textDim: "#565f89",
96
+ accent: "#bb9af7",
97
+ accentInk: "#1a1b26",
98
+ warning: "#e0af68",
99
+ green: "#9ece6a",
100
+ red: "#f7768e",
101
+ blue: "#7aa2f7",
102
+ cursorBackground: "#2b2740",
103
+ markCommentBackground: "#2f2230",
104
+ insertedForeground: "#9ece6a",
105
+ deletedForeground: "#f7768e",
106
+ insertedBackground: "#233028",
107
+ insertedEmphasisBackground: "#33493a",
108
+ deletedBackground: "#32232e",
109
+ deletedEmphasisBackground: "#4d3543",
110
+ backdrop: "transparent",
111
+ };
112
+
113
+ /** Gruvbox Dark - morhetz/gruvbox, medium contrast. Orange is the warm signature accent. */
114
+ const GRUVBOX_DARK: Theme = {
115
+ background: "#282828",
116
+ panel: "#282828",
117
+ elevated: "#3c3836",
118
+ border: "#504945",
119
+ text: "#ebdbb2",
120
+ textMuted: "#d5c4a1",
121
+ textDim: "#928374",
122
+ accent: "#fe8019",
123
+ accentInk: "#282828",
124
+ warning: "#fe8019",
125
+ green: "#b8bb26",
126
+ red: "#fb4934",
127
+ blue: "#83a598",
128
+ cursorBackground: "#3a2f20",
129
+ markCommentBackground: "#3a2823",
130
+ insertedForeground: "#b8bb26",
131
+ deletedForeground: "#fb4934",
132
+ insertedBackground: "#333521",
133
+ insertedEmphasisBackground: "#4a4d2c",
134
+ deletedBackground: "#3c2a24",
135
+ deletedEmphasisBackground: "#5a3a30",
136
+ backdrop: "transparent",
137
+ };
138
+
139
+ /** Nord - nordtheme.com, the official 16-color spec. Frost cyan is the signature accent. */
140
+ const NORD: Theme = {
141
+ background: "#2e3440",
142
+ panel: "#2e3440",
143
+ elevated: "#3b4252",
144
+ border: "#434c5e",
145
+ text: "#eceff4",
146
+ textMuted: "#d8dee9",
147
+ textDim: "#4c566a",
148
+ accent: "#88c0d0",
149
+ accentInk: "#2e3440",
150
+ warning: "#d08770",
151
+ green: "#a3be8c",
152
+ red: "#bf616a",
153
+ blue: "#81a1c1",
154
+ cursorBackground: "#2a3540",
155
+ markCommentBackground: "#3a2f33",
156
+ insertedForeground: "#a3be8c",
157
+ deletedForeground: "#bf616a",
158
+ insertedBackground: "#333d33",
159
+ insertedEmphasisBackground: "#454f42",
160
+ deletedBackground: "#3c2f33",
161
+ deletedEmphasisBackground: "#544045",
162
+ backdrop: "transparent",
163
+ };
164
+
165
+ /** All selectable themes by name; "cueloop" is the branded transparent default. */
166
+ export const THEME_PRESETS: Record<ThemeName, Theme> = {
167
+ cueloop: DARK,
168
+ "rose-pine-moon": ROSE_PINE_MOON,
169
+ "catppuccin-mocha": CATPPUCCIN_MOCHA,
170
+ "tokyo-night": TOKYO_NIGHT,
171
+ "gruvbox-dark": GRUVBOX_DARK,
172
+ nord: NORD,
173
+ };
174
+
175
+ /** Cycle order for the Settings theme row; the registry's insertion order, branded default first. */
176
+ export const THEME_NAMES: ThemeName[] = [
177
+ "cueloop",
178
+ "rose-pine-moon",
179
+ "catppuccin-mocha",
180
+ "tokyo-night",
181
+ "gruvbox-dark",
182
+ "nord",
183
+ ];
184
+
185
+ /** Human labels for the Settings cycle row, keyed by the persisted name. */
186
+ export const THEME_LABELS: Record<ThemeName, string> = {
187
+ cueloop: "cueloop",
188
+ "rose-pine-moon": "Rosé Pine Moon",
189
+ "catppuccin-mocha": "Catppuccin Mocha",
190
+ "tokyo-night": "Tokyo Night",
191
+ "gruvbox-dark": "Gruvbox Dark",
192
+ nord: "Nord",
193
+ };
194
+
195
+ /**
196
+ * Resolve a persisted theme name to its token set. The branded "cueloop" theme
197
+ * (and any unknown name that falls back to it) adapts to the terminal
198
+ * background; the explicit palette presets paint their own opaque background, so
199
+ * they read the same on a light or dark terminal and ignore `appearance`.
200
+ */
201
+ export function themeForName(name: string, appearance: Appearance = "dark"): Theme {
202
+ if (name === "cueloop") return brandedTheme(appearance);
203
+
204
+ return isThemeName(name) ? THEME_PRESETS[name] : brandedTheme(appearance);
205
+ }
206
+
207
+ /** A preset with the user's `[theme]` per-token overrides layered on top - the live-switch equivalent of config load. */
208
+ export function composeTheme(
209
+ name: string,
210
+ overrides: Partial<Theme>,
211
+ appearance: Appearance = "dark",
212
+ ): Theme {
213
+ return { ...themeForName(name, appearance), ...overrides };
214
+ }
215
+
216
+ /** True when the name is a known preset (guards a persisted `[ui] theme` value). */
217
+ export function isThemeName(name: string): name is ThemeName {
218
+ return name in THEME_PRESETS;
219
+ }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Semantic theme tokens. Values carry over from the design-system
3
+ * prototypes (tokens.css). One built-in dark theme for now; the theme
4
+ * registry (palette-derived themes, terminal theme, user overrides) layers
5
+ * on top of these slots without touching consumers.
6
+ */
7
+
8
+ export interface Theme {
9
+ background: string;
10
+ panel: string;
11
+ elevated: string;
12
+ border: string;
13
+ text: string;
14
+ textMuted: string;
15
+ textDim: string;
16
+ accent: string;
17
+ accentInk: string;
18
+ warning: string;
19
+ green: string;
20
+ red: string;
21
+ blue: string;
22
+ cursorBackground: string;
23
+ markCommentBackground: string;
24
+ insertedForeground: string;
25
+ deletedForeground: string;
26
+ insertedBackground: string;
27
+ insertedEmphasisBackground: string;
28
+ deletedBackground: string;
29
+ deletedEmphasisBackground: string;
30
+ backdrop: string;
31
+ }
32
+
33
+ /**
34
+ * The dimmed variant behind the walk wizard: every reading color drops to
35
+ * the dim token so the file list stays legible context, not competition.
36
+ * Layout tokens (backgrounds, cursor) keep their values.
37
+ */
38
+ export function dimmedTheme(theme: Theme): Theme {
39
+ return {
40
+ ...theme,
41
+ text: theme.textDim,
42
+ textMuted: theme.textDim,
43
+ accent: theme.textDim,
44
+ warning: theme.textDim,
45
+ green: theme.textDim,
46
+ red: theme.textDim,
47
+ blue: theme.textDim,
48
+ insertedForeground: theme.textDim,
49
+ deletedForeground: theme.textDim,
50
+ insertedBackground: theme.panel,
51
+ insertedEmphasisBackground: theme.elevated,
52
+ deletedBackground: theme.panel,
53
+ deletedEmphasisBackground: theme.elevated,
54
+ };
55
+ }
56
+
57
+ export const DARK: Theme = {
58
+ /* Unpainted so the terminal background shows through; override in [theme]. */
59
+ background: "transparent",
60
+ panel: "transparent",
61
+ elevated: "#23232f",
62
+ border: "#3a3a4a",
63
+ text: "#e4e6ec",
64
+ textMuted: "#b1b6c4",
65
+ textDim: "#6b7280",
66
+ accent: "#cba6f7",
67
+ accentInk: "#17171a",
68
+ warning: "#e5a45c",
69
+ green: "#62d96b",
70
+ red: "#f08080",
71
+ blue: "#a9c8f5",
72
+ cursorBackground: "#33262a",
73
+ markCommentBackground: "#3d2a2e",
74
+ insertedForeground: "#62d96b",
75
+ deletedForeground: "#ff6b6b",
76
+ insertedBackground: "#16321d",
77
+ insertedEmphasisBackground: "#245c30",
78
+ deletedBackground: "#341c1f",
79
+ deletedEmphasisBackground: "#5a2a2e",
80
+ backdrop: "transparent",
81
+ };
82
+
83
+ /**
84
+ * The branded transparent theme for a LIGHT terminal: same unpainted
85
+ * background, but every reading color darkens so text is legible on a white
86
+ * terminal instead of light-on-light. Chosen when the terminal reports a light
87
+ * background; the dark variant above is the default and the fallback.
88
+ */
89
+ export const LIGHT: Theme = {
90
+ background: "transparent",
91
+ panel: "transparent",
92
+ elevated: "#ececf2",
93
+ border: "#c7c7d2",
94
+ text: "#1c1d24",
95
+ textMuted: "#4b5162",
96
+ textDim: "#868c9c",
97
+ accent: "#5b3ea6",
98
+ accentInk: "#fbfbfa",
99
+ warning: "#a45f12",
100
+ green: "#2f8a3e",
101
+ red: "#c0392b",
102
+ blue: "#2f6fb0",
103
+ cursorBackground: "#e6e2ea",
104
+ markCommentBackground: "#f2e2e8",
105
+ insertedForeground: "#2f8a3e",
106
+ deletedForeground: "#c0392b",
107
+ insertedBackground: "#d8f5dd",
108
+ insertedEmphasisBackground: "#a9e6b6",
109
+ deletedBackground: "#fbdcdc",
110
+ deletedEmphasisBackground: "#f5b7b7",
111
+ backdrop: "transparent",
112
+ };
@@ -0,0 +1,35 @@
1
+ import { expect, test } from "bun:test";
2
+ import { themeForName } from "../theme-presets";
3
+ import { contrastRatio, relativeLuminance, rgbColorFromHex } from "./contrast";
4
+
5
+ test("relativeLuminance ranks black below white", () => {
6
+ expect(relativeLuminance([0, 0, 0])).toBe(0);
7
+ expect(relativeLuminance([255, 255, 255])).toBeCloseTo(1, 5);
8
+ });
9
+
10
+ test("contrastRatio is symmetric and peaks at black on white", () => {
11
+ expect(contrastRatio([0, 0, 0], [255, 255, 255])).toBeCloseTo(21, 0);
12
+ expect(contrastRatio([255, 255, 255], [0, 0, 0])).toBeCloseTo(21, 0);
13
+ expect(contrastRatio([100, 100, 100], [100, 100, 100])).toBeCloseTo(1, 5);
14
+ });
15
+
16
+ // The branded theme is terminal-through, so text sits on a real terminal background, not pure black or
17
+ // white; gate each theme against a representative one so a low-contrast token cannot hide behind an extreme.
18
+ const TERMINAL_BACKGROUND = {
19
+ dark: rgbColorFromHex("#1e1e2e"),
20
+ light: rgbColorFromHex("#f0f0f0"),
21
+ } as const;
22
+ const MIN_NORMAL_TEXT_CONTRAST = 4.5;
23
+
24
+ for (const mode of ["dark", "light"] as const) {
25
+ test(`${mode} theme keeps text, muted text, and accent legible on its terminal`, () => {
26
+ const theme = themeForName("cueloop", mode);
27
+ const background = TERMINAL_BACKGROUND[mode];
28
+
29
+ for (const token of [theme.text, theme.textMuted, theme.accent]) {
30
+ expect(contrastRatio(rgbColorFromHex(token), background)).toBeGreaterThanOrEqual(
31
+ MIN_NORMAL_TEXT_CONTRAST,
32
+ );
33
+ }
34
+ });
35
+ }
@@ -0,0 +1,33 @@
1
+ /** WCAG relative luminance and contrast ratio for the visual-regression accessibility gate. */
2
+
3
+ export type RgbColor = readonly [number, number, number];
4
+
5
+ /** Parse a `#rrggbb` hexadecimal colour into an sRGB triple (channels 0-255). */
6
+ export function rgbColorFromHex(hex: string): RgbColor {
7
+ const value = hex.replace("#", "");
8
+
9
+ return [
10
+ Number.parseInt(value.slice(0, 2), 16),
11
+ Number.parseInt(value.slice(2, 4), 16),
12
+ Number.parseInt(value.slice(4, 6), 16),
13
+ ];
14
+ }
15
+
16
+ /** WCAG relative luminance of an sRGB colour, 0 (black) to 1 (white). */
17
+ export function relativeLuminance(rgb: RgbColor): number {
18
+ const linear = rgb.map((channel) => {
19
+ const ratio = channel / 255;
20
+
21
+ return ratio <= 0.03928 ? ratio / 12.92 : ((ratio + 0.055) / 1.055) ** 2.4;
22
+ });
23
+
24
+ return 0.2126 * linear[0]! + 0.7152 * linear[1]! + 0.0722 * linear[2]!;
25
+ }
26
+
27
+ /** WCAG contrast ratio between two sRGB colours, 1 (identical) to 21 (black on white). */
28
+ export function contrastRatio(foreground: RgbColor, background: RgbColor): number {
29
+ const lighter = Math.max(relativeLuminance(foreground), relativeLuminance(background));
30
+ const darker = Math.min(relativeLuminance(foreground), relativeLuminance(background));
31
+
32
+ return (lighter + 0.05) / (darker + 0.05);
33
+ }
@@ -0,0 +1,42 @@
1
+ import React from "react";
2
+ import { SCHEMA_VERSION, type Thread } from "@cueloop/schema";
3
+ import { DARK } from "../../appearance/theme";
4
+ import type { Story, StoryMeta } from "../../stories/story";
5
+ import { AnnotatableFileView } from "./AnnotatableFileView";
6
+
7
+ export const meta: StoryMeta = { title: "Surfaces/AnnotatableFileView" };
8
+
9
+ const SAMPLE = `export function add(a: number, b: number): number {\n return a + b;\n}\n`;
10
+
11
+ const SESSION: Thread = {
12
+ schemaVersion: SCHEMA_VERSION,
13
+ id: "ses_story",
14
+ workspace: { repoRoot: "/repo", branch: "main" },
15
+ artifact: { type: "plan", content: "# Plan\n", meta: {} },
16
+ revisions: [{ revision: 1, content: "# Plan\n", submittedAt: "2026-01-01T00:00:00Z" }],
17
+ annotations: [],
18
+ message: null,
19
+ status: "pending",
20
+ createdAt: "2026-01-01T00:00:00Z",
21
+ };
22
+
23
+ const noop = (): void => {};
24
+
25
+ export const Loaded: Story = {
26
+ render: () => (
27
+ <AnnotatableFileView
28
+ path="src/add.ts"
29
+ loadContents={() => Promise.resolve(SAMPLE)}
30
+ session={SESSION}
31
+ quickActions={[]}
32
+ observer={false}
33
+ onAddComment={noop}
34
+ onReply={noop}
35
+ onUpdateAnnotation={noop}
36
+ onExit={noop}
37
+ theme={DARK}
38
+ />
39
+ ),
40
+ expectedColors: [DARK.textDim],
41
+ size: { width: 60, height: 12 },
42
+ };
@@ -0,0 +1,157 @@
1
+ /**
2
+ * A project file is its own annotation surface: selecting a line and typing leaves a comment whose
3
+ * anchor quotes that file line, handed up as an Anchor the app persists with a file target.
4
+ */
5
+
6
+ import { describe, expect, mock, test } from "bun:test";
7
+ import type { RGBA } from "@opentui/core";
8
+ import { testRender } from "@opentui/react/test-utils";
9
+ import React from "react";
10
+ import { SCHEMA_VERSION, type Anchor, type Thread } from "@cueloop/schema";
11
+ import { AnnotatableFileView } from "./AnnotatableFileView";
12
+ import { DARK } from "../../appearance/theme";
13
+ import {
14
+ allowEventLoopUpdates,
15
+ dragText,
16
+ typeText,
17
+ pressKey,
18
+ waitForText,
19
+ } from "../../testing/test-support";
20
+
21
+ const SAMPLE = "export function add(a: number, b: number) {\n return a + b;\n}\n";
22
+
23
+ function planSession(): Thread {
24
+ return {
25
+ schemaVersion: SCHEMA_VERSION,
26
+ id: "ses_plan",
27
+ workspace: { repoRoot: "/repo", branch: "main" },
28
+ artifact: { type: "plan", content: "# Plan\n", meta: {} },
29
+ revisions: [{ revision: 1, content: "# Plan\n", submittedAt: "2026-01-01T00:00:00Z" }],
30
+ annotations: [],
31
+ message: null,
32
+ status: "pending",
33
+ createdAt: "2026-01-01T00:00:00Z",
34
+ };
35
+ }
36
+
37
+ const noop = (): void => {};
38
+
39
+ function colorHex(color: RGBA): string {
40
+ const [red, green, blue] = color.toInts();
41
+
42
+ return "#" + [red, green, blue].map((part) => part.toString(16).padStart(2, "0")).join("");
43
+ }
44
+
45
+ describe("AnnotatableFileView", () => {
46
+ for (const { path, source, keyword } of [
47
+ { path: "src/add.ts", source: "export const add = 1;", keyword: "export" },
48
+ { path: "src/add.js", source: "const add = 1;", keyword: "const" },
49
+ ]) {
50
+ test(`highlights ${path} by its detected language`, async () => {
51
+ allowEventLoopUpdates();
52
+ const setup = await testRender(
53
+ <AnnotatableFileView
54
+ path={path}
55
+ loadContents={() => Promise.resolve(source)}
56
+ session={planSession()}
57
+ quickActions={[]}
58
+ observer={false}
59
+ onAddComment={noop}
60
+ onReply={noop}
61
+ onUpdateAnnotation={noop}
62
+ onExit={noop}
63
+ theme={DARK}
64
+ />,
65
+ { width: 64, height: 12 },
66
+ );
67
+ let highlighted = false;
68
+
69
+ for (let attempt = 0; attempt < 60 && !highlighted; attempt++) {
70
+ await setup.renderOnce();
71
+ await new Promise((resolve) => setTimeout(resolve, 20));
72
+ highlighted = setup.captureSpans().lines.some((line) =>
73
+ line.spans
74
+ .filter((span) => colorHex(span.fg) === DARK.accent)
75
+ .map((span) => span.text)
76
+ .join("")
77
+ .includes(keyword),
78
+ );
79
+ }
80
+
81
+ expect(highlighted).toBe(true);
82
+ setup.renderer.destroy();
83
+ }, 25000);
84
+ }
85
+
86
+ test("commenting a file line hands up an anchor that quotes the line", async () => {
87
+ const onAddComment = mock((_anchor: Anchor, _body: string) => {});
88
+ const setup = await testRender(
89
+ <AnnotatableFileView
90
+ path="src/add.ts"
91
+ loadContents={() => Promise.resolve(SAMPLE)}
92
+ session={planSession()}
93
+ quickActions={[]}
94
+ observer={false}
95
+ onAddComment={onAddComment}
96
+ onReply={noop}
97
+ onUpdateAnnotation={noop}
98
+ onExit={noop}
99
+ theme={DARK}
100
+ />,
101
+ { width: 64, height: 12 },
102
+ );
103
+
104
+ // Act - select the middle line, type a comment, send it
105
+ await waitForText(setup, "return a + b");
106
+ await dragText(setup, "return a + b", "return a + b", "return a + b".length);
107
+ await typeText(setup, "why not reduce?");
108
+ await pressKey(setup, "RETURN", { meta: true });
109
+
110
+ // Assert - the surface built an anchor quoting the file line and handed it up with the body
111
+ expect(onAddComment).toHaveBeenCalled();
112
+ const [anchor, body] = onAddComment.mock.calls[0]!;
113
+
114
+ expect(anchor.quote).toContain("return a + b");
115
+ expect(body).toContain("why not reduce?");
116
+
117
+ setup.renderer.destroy();
118
+ });
119
+
120
+ test("a head-side (deleted-line) note never rebinds onto the current file", async () => {
121
+ const session = planSession();
122
+
123
+ // a note left on the removed side of this file: the worktree view must not show it
124
+ session.annotations = [
125
+ {
126
+ id: "h1",
127
+ kind: "comment",
128
+ anchor: { quote: "return a + b", prefix: "", suffix: "" },
129
+ target: { kind: "file", path: "src/add.ts", rev: "head" },
130
+ body: "deleted-side note",
131
+ createdAt: "2026-01-01T00:00:00Z",
132
+ },
133
+ ];
134
+ const setup = await testRender(
135
+ <AnnotatableFileView
136
+ path="src/add.ts"
137
+ loadContents={() => Promise.resolve(SAMPLE)}
138
+ session={session}
139
+ quickActions={[]}
140
+ observer={false}
141
+ onAddComment={noop}
142
+ onReply={noop}
143
+ onUpdateAnnotation={noop}
144
+ onExit={noop}
145
+ theme={DARK}
146
+ />,
147
+ { width: 64, height: 12 },
148
+ );
149
+
150
+ await waitForText(setup, "return a + b");
151
+
152
+ // Assert - the current file shows, but the head-side note is not attached to it
153
+ expect(setup.captureCharFrame()).not.toContain("deleted-side note");
154
+
155
+ setup.renderer.destroy();
156
+ });
157
+ });