@cueloop/client 0.1.0-alpha.11 → 0.1.0-alpha.111

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 (360) 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 +63 -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 +383 -0
  12. package/src/annotations/components/DiscussionMarkerRail.stories.tsx +54 -0
  13. package/src/annotations/components/DiscussionMarkerRail.tsx +189 -0
  14. package/src/annotations/discussions.ts +78 -0
  15. package/src/annotations/thread-selection.test.ts +210 -0
  16. package/src/annotations/thread-selection.ts +248 -0
  17. package/src/annotations/use-annotation-surface.tsx +1378 -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 +190 -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.harness-settings.test.tsx +77 -0
  25. package/src/app/App.identity.test.tsx +84 -0
  26. package/src/app/App.plan-render.test.tsx +211 -0
  27. package/src/app/App.plan-review.test.tsx +429 -0
  28. package/src/app/App.prototype-markdown.test.tsx +129 -0
  29. package/src/app/App.prototype.test.tsx +176 -0
  30. package/src/app/App.readonly.test.tsx +274 -0
  31. package/src/app/App.regression.test.tsx +99 -0
  32. package/src/app/App.rendering.test.tsx +134 -0
  33. package/src/app/App.share-dialog.test.tsx +177 -0
  34. package/src/app/App.submit-confirm.test.tsx +267 -0
  35. package/src/app/App.test.tsx +497 -0
  36. package/src/app/App.thread-agent.test.ts +20 -0
  37. package/src/app/App.toast.test.tsx +124 -0
  38. package/src/app/App.tsx +1758 -0
  39. package/src/app/App.visual.test.tsx +60 -0
  40. package/src/app/App.walk.test.tsx +300 -0
  41. package/src/app/App.workbench.test.tsx +1153 -0
  42. package/src/app/app-key-state.ts +61 -0
  43. package/src/app/app-screens.tsx +607 -0
  44. package/src/app/app-view-model.ts +178 -0
  45. package/src/app/components/AppHeader.stories.tsx +50 -0
  46. package/src/app/components/AppHeader.tsx +81 -0
  47. package/src/app/components/AppShell.brand.test.tsx +58 -0
  48. package/src/app/components/AppShell.extensions.test.tsx +191 -0
  49. package/src/app/components/AppShell.stories.tsx +89 -0
  50. package/src/app/components/AppShell.tsx +358 -0
  51. package/src/app/components/AppShell.zoom.test.tsx +149 -0
  52. package/src/app/components/Breadcrumb.stories.tsx +19 -0
  53. package/src/app/components/Breadcrumb.tsx +45 -0
  54. package/src/app/components/MenuBar.stories.tsx +28 -0
  55. package/src/app/components/MenuBar.tsx +44 -0
  56. package/src/app/components/PaneGrid.stories.tsx +84 -0
  57. package/src/app/components/PaneGrid.tsx +128 -0
  58. package/src/app/components/PanelColumn.stories.tsx +25 -0
  59. package/src/app/components/PanelColumn.tsx +93 -0
  60. package/src/app/components/ShellHeader.stories.tsx +50 -0
  61. package/src/app/components/ShellHeader.tsx +137 -0
  62. package/src/app/components/WorkspacePanels.stories.tsx +36 -0
  63. package/src/app/components/WorkspacePanels.tsx +121 -0
  64. package/src/app/components/pages/Workspace.stories.tsx +181 -0
  65. package/src/app/intent-dispatch.test.ts +729 -0
  66. package/src/app/intent-dispatch.ts +713 -0
  67. package/src/app/launch-layout.test.ts +78 -0
  68. package/src/app/launch-layout.ts +57 -0
  69. package/src/app/ready-signal.test.ts +85 -0
  70. package/src/app/ready-signal.ts +54 -0
  71. package/src/app/run.ts +111 -0
  72. package/src/{serve.test.ts → app/serve.test.ts} +28 -12
  73. package/src/app/serve.ts +111 -0
  74. package/src/app/version.ts +5 -0
  75. package/src/appearance/components/theme-context.stories.tsx +46 -0
  76. package/src/appearance/components/theme-context.tsx +37 -0
  77. package/src/appearance/theme-presets.test.ts +51 -0
  78. package/src/appearance/theme-presets.ts +219 -0
  79. package/src/appearance/theme.ts +112 -0
  80. package/src/appearance/visual/contrast.test.ts +35 -0
  81. package/src/appearance/visual/contrast.ts +33 -0
  82. package/src/diff/components/AnnotatableFileView.stories.tsx +42 -0
  83. package/src/diff/components/AnnotatableFileView.test.tsx +160 -0
  84. package/src/diff/components/AnnotatableFileView.tsx +142 -0
  85. package/src/diff/components/BareWorkbenchFileView.stories.tsx +27 -0
  86. package/src/diff/components/BareWorkbenchFileView.tsx +63 -0
  87. package/src/diff/components/DiffContentView.scroll.test.tsx +443 -0
  88. package/src/diff/components/DiffContentView.split.test.tsx +140 -0
  89. package/src/diff/components/DiffContentView.stories.tsx +80 -0
  90. package/src/diff/components/DiffContentView.syntax.test.tsx +85 -0
  91. package/src/diff/components/DiffContentView.tsx +1080 -0
  92. package/src/diff/components/WalkWizard.stories.tsx +100 -0
  93. package/src/diff/components/WalkWizard.tsx +146 -0
  94. package/src/diff/components/diff-content-view-layout.test.ts +198 -0
  95. package/src/diff/components/diff-content-view-layout.ts +87 -0
  96. package/src/diff/diff-fold.test.ts +78 -0
  97. package/src/diff/diff-fold.ts +130 -0
  98. package/src/diff/diff-hunk-curate.test.ts +315 -0
  99. package/src/diff/diff-hunk-curate.ts +155 -0
  100. package/src/diff/diff-intraline.test.ts +239 -0
  101. package/src/diff/diff-intraline.ts +244 -0
  102. package/src/diff/diff-syntax.test.ts +125 -0
  103. package/src/diff/diff-syntax.ts +194 -0
  104. package/src/diff/split-diff.test.ts +110 -0
  105. package/src/diff/split-diff.ts +150 -0
  106. package/src/diff/view-diff.test.ts +325 -0
  107. package/src/diff/view-diff.ts +540 -0
  108. package/src/diff/walk.test.ts +121 -0
  109. package/src/diff/walk.ts +76 -0
  110. package/src/index.ts +32 -2
  111. package/src/integrations/attribution.test.ts +31 -0
  112. package/src/integrations/attribution.ts +44 -0
  113. package/src/integrations/client-extension-registry.test.tsx +61 -0
  114. package/src/integrations/client-extension-registry.ts +163 -0
  115. package/src/integrations/clipboard.test.ts +23 -0
  116. package/src/integrations/clipboard.ts +34 -0
  117. package/src/integrations/components/ExtensionZone.stories.tsx +28 -0
  118. package/src/integrations/components/ExtensionZone.tsx +145 -0
  119. package/src/integrations/components/ShareDialog.agent.test.tsx +52 -0
  120. package/src/integrations/components/ShareDialog.stories.tsx +121 -0
  121. package/src/integrations/components/ShareDialog.tsx +602 -0
  122. package/src/integrations/components/share-dialog-store.ts +59 -0
  123. package/src/integrations/editor.test.ts +149 -0
  124. package/src/integrations/editor.ts +163 -0
  125. package/src/integrations/github-identity.test.ts +59 -0
  126. package/src/integrations/github-identity.ts +42 -0
  127. package/src/integrations/herdr.test.ts +43 -0
  128. package/src/integrations/herdr.ts +40 -0
  129. package/src/integrations/integrations.test.ts +41 -0
  130. package/src/integrations/integrations.ts +32 -0
  131. package/src/integrations/pasted-image.test.ts +67 -0
  132. package/src/integrations/pasted-image.ts +62 -0
  133. package/src/integrations/share.ts +283 -0
  134. package/src/keyboard/components/CompletionOverlay.stories.tsx +37 -0
  135. package/src/keyboard/components/CompletionOverlay.tsx +75 -0
  136. package/src/keyboard/components/HandleChipsInput.stories.tsx +24 -0
  137. package/src/keyboard/components/HandleChipsInput.test.tsx +35 -0
  138. package/src/keyboard/components/HandleChipsInput.tsx +147 -0
  139. package/src/keyboard/components/KeybindsDialog.stories.tsx +31 -0
  140. package/src/keyboard/components/KeybindsDialog.tsx +103 -0
  141. package/src/keyboard/components/NavModeHint.stories.tsx +40 -0
  142. package/src/keyboard/components/NavModeHint.tsx +31 -0
  143. package/src/keyboard/components/SlashComposer.stories.tsx +37 -0
  144. package/src/keyboard/components/SlashComposer.tsx +90 -0
  145. package/src/keyboard/key-bindings.test.ts +156 -0
  146. package/src/keyboard/key-bindings.ts +462 -0
  147. package/src/keyboard/keymap.test.ts +611 -0
  148. package/src/keyboard/keymap.ts +446 -0
  149. package/src/keyboard/skills.test.ts +74 -0
  150. package/src/keyboard/skills.ts +77 -0
  151. package/src/keyboard/slash-palette.test.ts +103 -0
  152. package/src/keyboard/slash-palette.ts +137 -0
  153. package/src/keyboard/thread-chords.test.ts +79 -0
  154. package/src/keyboard/thread-chords.ts +168 -0
  155. package/src/keyboard/use-shared-keyboard.ts +56 -0
  156. package/src/markdown/__snapshots__/view-plan.regression.test.ts.snap +100 -0
  157. package/src/markdown/components/MarkdownGridBlock.stories.tsx +33 -0
  158. package/src/markdown/components/MarkdownGridBlock.tsx +72 -0
  159. package/src/markdown/components/MarkdownThreadEditor.stories.tsx +15 -0
  160. package/src/markdown/components/MarkdownThreadEditor.test.tsx +98 -0
  161. package/src/markdown/components/MarkdownThreadEditor.tsx +165 -0
  162. package/src/markdown/components/ThreadView.selection.test.tsx +954 -0
  163. package/src/markdown/components/ThreadView.slash.test.tsx +146 -0
  164. package/src/markdown/components/ThreadView.stories.tsx +26 -0
  165. package/src/markdown/components/ThreadView.tsx +577 -0
  166. package/src/markdown/mark-runs.ts +125 -0
  167. package/src/markdown/markdown-editor-syntax-style.ts +66 -0
  168. package/src/markdown/markdown-grid.test.ts +101 -0
  169. package/src/markdown/markdown-grid.ts +209 -0
  170. package/src/markdown/markdown-highlight.test.ts +61 -0
  171. package/src/markdown/markdown-highlight.ts +125 -0
  172. package/src/markdown/thread-code-syntax.ts +48 -0
  173. package/src/markdown/use-thread-body-editing.tsx +54 -0
  174. package/src/markdown/view-plan.regression.test.ts +60 -0
  175. package/src/markdown/view-plan.test.ts +545 -0
  176. package/src/markdown/view-plan.ts +1061 -0
  177. package/src/performance/otlp-trace.test.ts +119 -0
  178. package/src/performance/otlp-trace.ts +134 -0
  179. package/src/performance/perf-report.ts +19 -0
  180. package/src/performance/perf-timings.test.ts +18 -0
  181. package/src/performance/perf-timings.ts +40 -0
  182. package/src/prototype/components/PrototypeContentView.stories.tsx +19 -0
  183. package/src/prototype/components/PrototypeContentView.tsx +459 -0
  184. package/src/prototype/prototype-browser.test.ts +81 -0
  185. package/src/prototype/prototype-browser.ts +332 -0
  186. package/src/prototype/prototype-pixels.tsx +35 -0
  187. package/src/settings/components/SettingsDialog.stories.tsx +117 -0
  188. package/src/settings/components/SettingsDialog.tsx +186 -0
  189. package/src/settings/components/SettingsRows.stories.tsx +18 -0
  190. package/src/settings/components/SettingsRows.tsx +112 -0
  191. package/src/settings/components/quick-actions-editor.stories.tsx +58 -0
  192. package/src/settings/components/quick-actions-editor.test.tsx +120 -0
  193. package/src/settings/components/quick-actions-editor.tsx +222 -0
  194. package/src/settings/config-identity.test.ts +78 -0
  195. package/src/settings/config.test.ts +771 -0
  196. package/src/settings/config.ts +722 -0
  197. package/src/settings/use-settings-dialog.test.tsx +172 -0
  198. package/src/settings/use-settings-dialog.tsx +465 -0
  199. package/src/settings/use-thread-harness-setting.test.tsx +87 -0
  200. package/src/settings/use-thread-harness-setting.ts +86 -0
  201. package/src/stories/__snapshots__/stories.test.tsx.snap +2358 -0
  202. package/src/stories/stories-app.tsx +196 -0
  203. package/src/stories/stories.test.tsx +78 -0
  204. package/src/stories/story-fixtures.ts +102 -0
  205. package/src/stories/story-tree.test.ts +108 -0
  206. package/src/stories/story.ts +160 -0
  207. package/src/terminal/ansi-screen.test.ts +28 -0
  208. package/src/terminal/ansi-screen.ts +77 -0
  209. package/src/terminal/components/AnsiScreen.stories.tsx +12 -0
  210. package/src/terminal/components/AnsiScreen.tsx +28 -0
  211. package/src/terminal/components/terminal-pane.test.tsx +40 -0
  212. package/src/terminal/components/terminal-pane.ts +208 -0
  213. package/src/terminal/ghostty-terminal.test.ts +85 -0
  214. package/src/terminal/ghostty-terminal.ts +207 -0
  215. package/src/terminal/kitty-image.test.ts +66 -0
  216. package/src/terminal/kitty-image.ts +140 -0
  217. package/src/terminal/pty.test.ts +81 -0
  218. package/src/terminal/pty.ts +255 -0
  219. package/src/testing/test-support.test.ts +266 -0
  220. package/src/testing/test-support.ts +449 -0
  221. package/src/thread/agent-continuation.test.ts +34 -0
  222. package/src/thread/agent-continuation.ts +13 -0
  223. package/src/thread/agent-transcript.test.ts +161 -0
  224. package/src/thread/agent-transcript.ts +344 -0
  225. package/src/thread/components/AgentConfigControls.stories.tsx +39 -0
  226. package/src/thread/components/AgentConfigControls.test.tsx +153 -0
  227. package/src/thread/components/AgentConfigControls.tsx +121 -0
  228. package/src/thread/components/AgentThreadPane.stories.tsx +105 -0
  229. package/src/thread/components/AgentThreadPane.test.tsx +1248 -0
  230. package/src/thread/components/AgentThreadPane.tsx +458 -0
  231. package/src/thread/components/ThreadFooter.stories.tsx +30 -0
  232. package/src/thread/components/ThreadFooter.test.tsx +49 -0
  233. package/src/thread/components/ThreadFooter.tsx +77 -0
  234. package/src/thread/components/ThreadTree.stories.tsx +46 -0
  235. package/src/thread/components/ThreadTree.test.tsx +41 -0
  236. package/src/thread/components/ThreadTree.tsx +268 -0
  237. package/src/thread/components/thread-tree.test.ts +138 -0
  238. package/src/thread/components/thread-tree.ts +112 -0
  239. package/src/thread/thread-controller.curate.test.ts +504 -0
  240. package/src/thread/thread-controller.derivation.test.ts +62 -0
  241. package/src/thread/thread-controller.live-diff.test.ts +357 -0
  242. package/src/thread/thread-controller.optimistic.test.ts +69 -0
  243. package/src/thread/thread-controller.share.test.ts +618 -0
  244. package/src/thread/thread-controller.switch-cache.test.ts +174 -0
  245. package/src/thread/thread-controller.tree.test.ts +263 -0
  246. package/src/thread/thread-controller.ts +2151 -0
  247. package/src/thread/tree-view.test.ts +128 -0
  248. package/src/thread/tree-view.ts +150 -0
  249. package/src/thread/use-thread-agent.test.tsx +209 -0
  250. package/src/thread/use-thread-agent.ts +122 -0
  251. package/src/ui/components/CodeBlock.stories.tsx +42 -0
  252. package/src/ui/components/CodeBlock.tsx +82 -0
  253. package/src/ui/components/ConfirmCard.stories.tsx +37 -0
  254. package/src/ui/components/ConfirmCard.tsx +151 -0
  255. package/src/ui/components/ConfirmDialog.stories.tsx +21 -0
  256. package/src/ui/components/ConfirmDialog.tsx +72 -0
  257. package/src/ui/components/OverlayScrollbar.stories.tsx +33 -0
  258. package/src/ui/components/OverlayScrollbar.test.tsx +142 -0
  259. package/src/ui/components/OverlayScrollbar.tsx +134 -0
  260. package/src/ui/components/PromptDialog.stories.tsx +36 -0
  261. package/src/ui/components/PromptDialog.test.tsx +65 -0
  262. package/src/ui/components/PromptDialog.tsx +101 -0
  263. package/src/ui/components/RootOverlay.stories.tsx +45 -0
  264. package/src/ui/components/RootOverlay.tsx +50 -0
  265. package/src/ui/components/ScrollArea.stories.tsx +20 -0
  266. package/src/ui/components/ScrollArea.tsx +63 -0
  267. package/src/ui/components/Toast.stories.tsx +19 -0
  268. package/src/ui/components/Toast.tsx +54 -0
  269. package/src/ui/components/Tooltip.stories.tsx +26 -0
  270. package/src/ui/components/Tooltip.tsx +93 -0
  271. package/src/ui/components/menu-control.stories.tsx +35 -0
  272. package/src/ui/components/menu-control.tsx +43 -0
  273. package/src/ui/components/primitives/Button.stories.tsx +32 -0
  274. package/src/ui/components/primitives/Button.tsx +58 -0
  275. package/src/ui/components/primitives/Card.stories.tsx +25 -0
  276. package/src/ui/components/primitives/Card.tsx +62 -0
  277. package/src/ui/components/primitives/Dialog.stories.tsx +24 -0
  278. package/src/ui/components/primitives/Dialog.test.tsx +38 -0
  279. package/src/ui/components/primitives/Dialog.tsx +69 -0
  280. package/src/ui/components/primitives/DialogActions.stories.tsx +20 -0
  281. package/src/ui/components/primitives/DialogActions.tsx +39 -0
  282. package/src/ui/components/primitives/IconButton.stories.tsx +36 -0
  283. package/src/ui/components/primitives/IconButton.tsx +58 -0
  284. package/src/ui/components/primitives/StatusBar.stories.tsx +11 -0
  285. package/src/ui/components/primitives/StatusBar.tsx +23 -0
  286. package/src/ui/components/primitives/Switch.stories.tsx +17 -0
  287. package/src/ui/components/primitives/Switch.tsx +32 -0
  288. package/src/ui/components/primitives/Tabs.stories.tsx +29 -0
  289. package/src/ui/components/primitives/Tabs.tsx +102 -0
  290. package/src/ui/components/primitives/Toolbar.stories.tsx +17 -0
  291. package/src/ui/components/primitives/Toolbar.tsx +14 -0
  292. package/src/ui/components/primitives/Tree.stories.tsx +124 -0
  293. package/src/ui/components/primitives/Tree.tsx +137 -0
  294. package/src/ui/components/primitives/frame.test.ts +12 -0
  295. package/src/ui/components/primitives/frame.ts +17 -0
  296. package/src/ui/components/primitives/icons.ts +61 -0
  297. package/src/ui/components/primitives/tree-model.test.ts +99 -0
  298. package/src/ui/components/primitives/tree-model.ts +144 -0
  299. package/src/ui/components/syntax-highlight.ts +154 -0
  300. package/src/ui/components/truncate-text.ts +7 -0
  301. package/src/ui/components/truncate-title.test.ts +31 -0
  302. package/src/ui/components/truncate-title.ts +16 -0
  303. package/src/ui/use-frame-measure.ts +88 -0
  304. package/src/ui/use-terminal-virtualizer.ts +286 -0
  305. package/src/workbench/components/ChangesColumn.stories.tsx +44 -0
  306. package/src/workbench/components/ChangesColumn.tsx +210 -0
  307. package/src/workbench/components/ChangesFileTree.test.tsx +184 -0
  308. package/src/workbench/components/EditorGrid.split.test.tsx +119 -0
  309. package/src/workbench/components/EditorGrid.stories.tsx +86 -0
  310. package/src/workbench/components/EditorGrid.tabs.test.tsx +118 -0
  311. package/src/workbench/components/EditorGrid.tsx +552 -0
  312. package/src/workbench/components/EditorGrid.zoom.test.tsx +132 -0
  313. package/src/workbench/components/FileContentsView.stories.tsx +16 -0
  314. package/src/workbench/components/FileContentsView.test.tsx +29 -0
  315. package/src/workbench/components/FileContentsView.tsx +104 -0
  316. package/src/workbench/components/GridTabContent.actions.test.tsx +151 -0
  317. package/src/workbench/components/GridTabContent.stories.tsx +59 -0
  318. package/src/workbench/components/GridTabContent.tsx +195 -0
  319. package/src/workbench/components/ProjectTreeView.stories.tsx +25 -0
  320. package/src/workbench/components/ProjectTreeView.test.tsx +239 -0
  321. package/src/workbench/components/ProjectTreeView.tsx +159 -0
  322. package/src/workbench/components/TreePane.stories.tsx +106 -0
  323. package/src/workbench/components/TreePane.tsx +157 -0
  324. package/src/workbench/components/WelcomePlayground.stories.tsx +12 -0
  325. package/src/workbench/components/WelcomePlayground.test.tsx +83 -0
  326. package/src/workbench/components/WelcomePlayground.tsx +138 -0
  327. package/src/workbench/components/editor-grid.test.ts +184 -0
  328. package/src/workbench/components/editor-grid.ts +284 -0
  329. package/src/workbench/components/file-tree.test.ts +60 -0
  330. package/src/workbench/components/file-tree.ts +114 -0
  331. package/src/workbench/use-changes-workbench.ts +210 -0
  332. package/src/workbench/use-remember-layout.ts +33 -0
  333. package/src/workbench/use-repo-changes.ts +93 -0
  334. package/src/workbench/workbench-snapshot.test.ts +99 -0
  335. package/src/workbench/workbench-snapshot.ts +59 -0
  336. package/src/App.diff.test.tsx +0 -99
  337. package/src/App.export.test.tsx +0 -88
  338. package/src/App.plan-review.test.tsx +0 -259
  339. package/src/App.readonly.test.tsx +0 -148
  340. package/src/App.rendering.test.tsx +0 -108
  341. package/src/App.submit-confirm.test.tsx +0 -183
  342. package/src/App.test.tsx +0 -218
  343. package/src/App.tsx +0 -1249
  344. package/src/config.test.ts +0 -116
  345. package/src/config.ts +0 -150
  346. package/src/editor.ts +0 -34
  347. package/src/herdr.test.ts +0 -68
  348. package/src/herdr.ts +0 -34
  349. package/src/keymap.test.ts +0 -270
  350. package/src/keymap.ts +0 -173
  351. package/src/run.ts +0 -30
  352. package/src/serve.ts +0 -75
  353. package/src/session-controller.ts +0 -398
  354. package/src/syntax.test.ts +0 -41
  355. package/src/syntax.ts +0 -88
  356. package/src/theme.ts +0 -46
  357. package/src/view-diff.test.ts +0 -55
  358. package/src/view-diff.ts +0 -86
  359. package/src/view.test.ts +0 -157
  360. package/src/view.ts +0 -364
@@ -0,0 +1,210 @@
1
+ // The right-hand Changes column: the diff's changed files as a directory tree.
2
+ // A click on a file scrolls the diff to that file. Renders nothing when collapsed
3
+ // so the diff reclaims the width. Mirrors the left Threads column on the far side.
4
+ // Co-located here: the state hook that opens it by context and the header toggle.
5
+
6
+ import { ScrollArea } from "../../ui/components/ScrollArea";
7
+ import React, { useMemo, useState } from "react";
8
+ import { useKeyboard } from "@opentui/react";
9
+ import type { DiffFileContents, Thread } from "@cueloop/schema";
10
+ import type { Theme } from "../../appearance/theme";
11
+ import type { DiffRow } from "../../diff/view-diff";
12
+ import { Tree } from "../../ui/components/primitives/Tree";
13
+ import { allFolderIds, flattenTree } from "../../ui/components/primitives/tree-model";
14
+ import { IconButton } from "../../ui/components/primitives/IconButton";
15
+ import { NERD } from "../../ui/components/primitives/icons";
16
+ import { buildFileTree } from "./file-tree";
17
+
18
+ export interface DiffColumnsState {
19
+ changesOpen: boolean;
20
+ toggleChanges: () => void;
21
+ currentFilePath?: string;
22
+ scrollToFile: (path: string) => void;
23
+ }
24
+
25
+ /**
26
+ * Owns the right Changes column for a diff: context opens it when you enter a
27
+ * diff thread, and a file click scrolls the sheet to that file's first row.
28
+ * Resets on session change via the guarded render-time pattern, no effect.
29
+ */
30
+ export function useDiffColumns(params: {
31
+ session: Thread | null;
32
+ rows: DiffRow[];
33
+ cursor: number;
34
+ setCursor: (index: number) => void;
35
+ }): DiffColumnsState {
36
+ const { session, rows, cursor, setCursor } = params;
37
+ const contextDefault = session?.artifact.type === "diff";
38
+ const [changesOpen, setChangesOpen] = useState(contextDefault);
39
+ const [seenSessionId, setSeenSessionId] = useState(session?.id);
40
+
41
+ // entering another thread lets context decide again; within a thread the manual toggle stays
42
+ if (session?.id !== seenSessionId) {
43
+ setSeenSessionId(session?.id);
44
+ setChangesOpen(contextDefault);
45
+ }
46
+
47
+ return {
48
+ changesOpen,
49
+ toggleChanges: () => setChangesOpen((open) => !open),
50
+ currentFilePath: rows[cursor]?.file,
51
+ scrollToFile: (path: string) => {
52
+ const fileRowIndex = rows.findIndex((row) => row.kind === "file" && row.file === path);
53
+
54
+ if (fileRowIndex >= 0) setCursor(fileRowIndex);
55
+ },
56
+ };
57
+ }
58
+
59
+ /** The header toggle for the Changes column; shown only for a diff, where files exist to browse. */
60
+ export function DiffChangesToggle({
61
+ isDiff,
62
+ open,
63
+ onToggle,
64
+ theme,
65
+ }: {
66
+ isDiff: boolean;
67
+ open: boolean;
68
+ onToggle: () => void;
69
+ theme?: Theme;
70
+ }): React.ReactNode {
71
+ if (!isDiff) return null;
72
+
73
+ return (
74
+ <IconButton
75
+ glyph={open ? NERD.sidebarRight : NERD.sidebarRightOff}
76
+ onPress={onToggle}
77
+ marginLeft={2}
78
+ theme={theme}
79
+ />
80
+ );
81
+ }
82
+
83
+ export interface ChangesColumnProps {
84
+ open: boolean;
85
+ files?: readonly DiffFileContents[];
86
+ selectedPath?: string;
87
+ onSelectFile: (path: string, persistent?: boolean) => void;
88
+ commentCounts?: ReadonlyMap<string, number>;
89
+ focused?: boolean;
90
+ width?: number;
91
+ theme?: Theme;
92
+ }
93
+
94
+ /**
95
+ * The changed-files tree on its own, with no surrounding column frame: the body
96
+ * a workbench Project pane renders. A file click scrolls the diff to that file.
97
+ */
98
+ export function ChangesFileTree({
99
+ files = [],
100
+ selectedPath,
101
+ onSelectFile,
102
+ commentCounts,
103
+ focused = false,
104
+ theme,
105
+ }: Omit<ChangesColumnProps, "open" | "width">): React.ReactNode {
106
+ const nodes = useMemo(() => buildFileTree(files, commentCounts), [files, commentCounts]);
107
+ // every folder opens by default so each changed file is reachable; the user
108
+ // only ever names the folders they fold shut, so a new file set stays open
109
+ const [collapsedIds, setCollapsedIds] = useState<ReadonlySet<string>>(new Set());
110
+ const [cursor, setCursor] = useState(0);
111
+ const expandedIds = useMemo(() => {
112
+ const expanded = allFolderIds(nodes);
113
+
114
+ for (const id of collapsedIds) expanded.delete(id);
115
+
116
+ return expanded;
117
+ }, [nodes, collapsedIds]);
118
+ const rows = useMemo(
119
+ () => flattenTree(nodes, { expandedIds, flattenEmptyDirectories: true }),
120
+ [nodes, expandedIds],
121
+ );
122
+ const cursorIndex = Math.min(cursor, Math.max(0, rows.length - 1));
123
+
124
+ const toggle = (id: string): void =>
125
+ setCollapsedIds((current) => {
126
+ const next = new Set(current);
127
+
128
+ if (next.has(id)) next.delete(id);
129
+ else next.add(id);
130
+
131
+ return next;
132
+ });
133
+
134
+ useKeyboard((key) => {
135
+ if (!focused || rows.length === 0) return;
136
+
137
+ if (key.name === "j" || key.name === "down")
138
+ return setCursor(Math.min(cursorIndex + 1, rows.length - 1));
139
+
140
+ if (key.name === "k" || key.name === "up") return setCursor(Math.max(cursorIndex - 1, 0));
141
+
142
+ const row = rows[cursorIndex];
143
+
144
+ if (!row) return;
145
+
146
+ if (key.name === "return" || key.name === "enter")
147
+ return row.isFolder ? toggle(row.id) : onSelectFile(row.id, true);
148
+
149
+ if (key.name === "l" && row.isFolder && collapsedIds.has(row.id)) return toggle(row.id);
150
+
151
+ if (key.name === "h" && row.isFolder && !collapsedIds.has(row.id)) return toggle(row.id);
152
+ });
153
+
154
+ return (
155
+ <ScrollArea
156
+ id="tree-scroll"
157
+ revealId={focused ? rows[cursorIndex]?.id : undefined}
158
+ gestureWheel
159
+ >
160
+ <Tree
161
+ nodes={nodes}
162
+ expandedIds={expandedIds}
163
+ selectedId={focused ? rows[cursorIndex]?.id : selectedPath}
164
+ singleLine
165
+ flattenEmptyDirectories
166
+ showStatus
167
+ onSelect={(id) => {
168
+ const index = rows.findIndex((row) => row.id === id);
169
+
170
+ if (index >= 0) setCursor(index);
171
+
172
+ onSelectFile(id);
173
+ }}
174
+ onDoubleSelect={(id) => onSelectFile(id, true)}
175
+ onToggle={toggle}
176
+ theme={theme}
177
+ />
178
+ </ScrollArea>
179
+ );
180
+ }
181
+
182
+ export function ChangesColumn({
183
+ open,
184
+ files = [],
185
+ selectedPath,
186
+ onSelectFile,
187
+ width = 32,
188
+ theme,
189
+ }: ChangesColumnProps): React.ReactNode {
190
+ if (!open) return null;
191
+
192
+ return (
193
+ <box
194
+ style={{
195
+ width,
196
+ flexDirection: "column",
197
+ borderStyle: "single",
198
+ border: ["left"],
199
+ borderColor: theme?.border,
200
+ }}
201
+ >
202
+ <ChangesFileTree
203
+ files={files}
204
+ selectedPath={selectedPath}
205
+ onSelectFile={onSelectFile}
206
+ theme={theme}
207
+ />
208
+ </box>
209
+ );
210
+ }
@@ -0,0 +1,184 @@
1
+ /** The changed-files tree navigates by keyboard when focused: j/k move the cursor, Enter folds a folder
2
+ * or opens a file. */
3
+
4
+ import { test, expect } from "bun:test";
5
+ import React from "react";
6
+ import { testRender } from "@opentui/react/test-utils";
7
+ import { ScrollBoxRenderable, TextRenderable, type Renderable } from "@opentui/core";
8
+ import type { DiffFileContents } from "@cueloop/schema";
9
+ import { ChangesFileTree } from "./ChangesColumn";
10
+ import { settle, waitForText } from "../../testing/test-support";
11
+ import { DARK } from "../../appearance/theme";
12
+
13
+ function findById(node: Renderable, id: string): Renderable | undefined {
14
+ if (node.id === id) return node;
15
+
16
+ for (const child of node.getChildren()) {
17
+ const found = findById(child, id);
18
+
19
+ if (found) return found;
20
+ }
21
+
22
+ return undefined;
23
+ }
24
+
25
+ const file = (path: string): DiffFileContents => ({
26
+ path,
27
+ oldContents: "",
28
+ newContents: "x",
29
+ status: "modified",
30
+ });
31
+
32
+ const files = [file("src/a.ts"), file("src/b.ts")];
33
+
34
+ test("j moves onto a file and Enter opens it", async () => {
35
+ const opened: string[] = [];
36
+ const setup = await testRender(
37
+ <ChangesFileTree
38
+ files={files}
39
+ onSelectFile={(path) => opened.push(path)}
40
+ focused
41
+ theme={DARK}
42
+ />,
43
+ { width: 40, height: 12 },
44
+ );
45
+
46
+ await settle(setup);
47
+ await waitForText(setup, "a.ts");
48
+
49
+ // folders open by default; cursor starts on the "src" folder, j steps onto a.ts
50
+ setup.mockInput.pressKey("j");
51
+ await settle(setup);
52
+ setup.mockInput.pressKey("RETURN");
53
+ await settle(setup);
54
+
55
+ expect(opened).toEqual(["src/a.ts"]);
56
+
57
+ setup.renderer.destroy();
58
+ });
59
+
60
+ test("Tab leaves the focused tree without opening a file", async () => {
61
+ const opened: string[] = [];
62
+ const setup = await testRender(
63
+ <ChangesFileTree
64
+ files={files}
65
+ onSelectFile={(path) => opened.push(path)}
66
+ focused
67
+ theme={DARK}
68
+ />,
69
+ { width: 40, height: 12 },
70
+ );
71
+
72
+ await waitForText(setup, "a.ts");
73
+ setup.mockInput.pressKey("j");
74
+ setup.mockInput.pressKey("TAB");
75
+ await settle(setup);
76
+ expect(opened).toEqual([]);
77
+ setup.renderer.destroy();
78
+ });
79
+
80
+ test("keyboard navigation reveals the selected changed file", async () => {
81
+ const manyFiles = Array.from({ length: 35 }, (_, index) =>
82
+ file(`file-${String(index).padStart(2, "0")}.ts`),
83
+ );
84
+ const setup = await testRender(
85
+ <ChangesFileTree files={manyFiles} onSelectFile={() => {}} focused theme={DARK} />,
86
+ { width: 28, height: 8 },
87
+ );
88
+
89
+ await waitForText(setup, "file-00.ts");
90
+ const scrollbox = findById(setup.renderer.root, "tree-scroll");
91
+
92
+ if (!(scrollbox instanceof ScrollBoxRenderable)) throw new Error("tree scrollbox missing");
93
+
94
+ const scrollSteps: number[] = [];
95
+
96
+ for (const [index, key] of Array.from({ length: 15 }, () => "j").entries()) {
97
+ const before = scrollbox.scrollTop;
98
+
99
+ setup.mockInput.pressKey(key);
100
+ // eslint-disable-next-line no-await-in-loop
101
+ await settle(setup);
102
+ scrollSteps.push(scrollbox.scrollTop - before);
103
+ const row = scrollbox.content.findDescendantById(
104
+ `tree-row-file-${String(index + 1).padStart(2, "0")}.ts`,
105
+ );
106
+
107
+ expect(row?.y).toBeGreaterThanOrEqual(scrollbox.viewport.y);
108
+ expect(row?.y).toBeLessThan(scrollbox.viewport.y + scrollbox.viewport.height);
109
+ }
110
+
111
+ expect(scrollbox.scrollTop).toBeGreaterThan(0);
112
+ expect(Math.max(...scrollSteps)).toBe(1);
113
+ expect(setup.captureCharFrame()).toContain("file-15.ts");
114
+ setup.renderer.destroy();
115
+ });
116
+
117
+ test("without focus the keyboard opens nothing", async () => {
118
+ const opened: string[] = [];
119
+ const setup = await testRender(
120
+ <ChangesFileTree files={files} onSelectFile={(path) => opened.push(path)} theme={DARK} />,
121
+ { width: 40, height: 12 },
122
+ );
123
+
124
+ await settle(setup);
125
+ await waitForText(setup, "a.ts");
126
+
127
+ setup.mockInput.pressKey("j");
128
+ setup.mockInput.pressKey("RETURN");
129
+ await settle(setup);
130
+ expect(opened).toEqual([]);
131
+
132
+ setup.renderer.destroy();
133
+ });
134
+
135
+ test("a selected long filename has no empty continuation row", async () => {
136
+ for (const width of [33, 32, 31, 30, 29, 28, 27, 26, 25, 24]) {
137
+ const setup = await testRender(
138
+ <ChangesFileTree
139
+ files={[
140
+ file("packages/client/src/App.workbench.test.tsx"),
141
+ file("packages/client/src/next.ts"),
142
+ ]}
143
+ onSelectFile={() => {}}
144
+ focused
145
+ theme={DARK}
146
+ />,
147
+ { width, height: 12 },
148
+ );
149
+
150
+ await settle(setup);
151
+ setup.mockInput.pressKey("j");
152
+ await settle(setup);
153
+ const frame = setup.captureCharFrame().split("\n");
154
+ const selectedLine = frame.findIndex((line) => line.includes("App.wor"));
155
+ const row = findById(
156
+ setup.renderer.root,
157
+ "tree-row-packages/client/src/App.workbench.test.tsx",
158
+ );
159
+
160
+ expect(selectedLine).toBeGreaterThanOrEqual(0);
161
+ expect(frame[selectedLine + 1]).toContain("next.ts");
162
+ expect(row?.height).toBe(1);
163
+ setup.renderer.destroy();
164
+ }
165
+ });
166
+
167
+ test("horizontal name scrolling reveals the final extension beside the status", async () => {
168
+ const path = "src/sidebar-scroll.test.tsx";
169
+ const setup = await testRender(
170
+ <ChangesFileTree files={[file(path)]} onSelectFile={() => {}} focused theme={DARK} />,
171
+ { width: 24, height: 8 },
172
+ );
173
+
174
+ await waitForText(setup, "src");
175
+ const label = findById(setup.renderer.root, `tree-label-${path}`);
176
+
177
+ if (!(label instanceof TextRenderable)) throw new Error("file label missing");
178
+
179
+ expect(label.maxScrollX).toBeGreaterThan(0);
180
+ label.scrollX = label.maxScrollX;
181
+ await settle(setup);
182
+ expect(setup.captureCharFrame()).toContain(".tsx");
183
+ setup.renderer.destroy();
184
+ });
@@ -0,0 +1,119 @@
1
+ import { expect, test } from "bun:test";
2
+ import React from "react";
3
+ import { testRender } from "@opentui/react/test-utils";
4
+ import type { Renderable } from "@opentui/core";
5
+ import { allowEventLoopUpdates } from "../../testing/test-support";
6
+ import { EditorGrid } from "./EditorGrid";
7
+ import { makeGroup, splitGroup, welcomeTab, type EditorNode } from "./editor-grid";
8
+ import { WelcomePlayground } from "./WelcomePlayground";
9
+
10
+ function findById(node: Renderable, id: string): Renderable | undefined {
11
+ if (node.id === id) return node;
12
+
13
+ for (const child of node.getChildren()) {
14
+ const found = findById(child, id);
15
+
16
+ if (found) return found;
17
+ }
18
+
19
+ return undefined;
20
+ }
21
+
22
+ function groupIds(node: EditorNode): string[] {
23
+ if (node.type === "group") return [node.id];
24
+
25
+ return node.children.flatMap(groupIds);
26
+ }
27
+
28
+ test("mixed splits keep each Welcome page inside its own tile", async () => {
29
+ const first = makeGroup([welcomeTab()]);
30
+ let tree: EditorNode = first;
31
+ let focusGroupId = first.id;
32
+
33
+ for (const direction of ["right", "up", "left", "down"] as const) {
34
+ const next = splitGroup(tree, focusGroupId, direction);
35
+
36
+ tree = next.tree;
37
+ focusGroupId = next.focusGroupId;
38
+ }
39
+
40
+ const setup = await testRender(
41
+ <EditorGrid
42
+ tree={tree}
43
+ focusedGroupId={focusGroupId}
44
+ onFocusGroup={() => {}}
45
+ onActivateTab={() => {}}
46
+ onCloseTab={() => {}}
47
+ onSplit={() => {}}
48
+ onZoom={() => {}}
49
+ zoomed={false}
50
+ renderTab={(_, focused) => <WelcomePlayground quickActions={[]} suspended={!focused} />}
51
+ />,
52
+ { width: 120, height: 48 },
53
+ );
54
+
55
+ allowEventLoopUpdates();
56
+ await setup.waitForVisualIdle();
57
+ expect(setup.renderer.listenerCount("frame")).toBeLessThan(10);
58
+ expect(setup.renderer.listenerCount("resize")).toBeLessThan(10);
59
+ expect(setup.renderer.keyInput.listenerCount("keypress")).toBeLessThan(10);
60
+
61
+ for (const id of groupIds(tree)) {
62
+ const group = findById(setup.renderer.root, `editor-group:${id}`);
63
+ const scroll = group && findById(group, "diff-scroll");
64
+
65
+ expect(group).toBeDefined();
66
+ expect(scroll).toBeDefined();
67
+ expect(scroll!.y).toBeGreaterThanOrEqual(group!.y + 2);
68
+ expect(scroll!.y + scroll!.height).toBeLessThanOrEqual(group!.y + group!.height);
69
+ }
70
+
71
+ setup.renderer.destroy();
72
+ });
73
+
74
+ test("Split is disabled at eight tiles and when a tile is too small", async () => {
75
+ const first = makeGroup([welcomeTab()]);
76
+ let tree: EditorNode = first;
77
+ let focusGroupId = first.id;
78
+
79
+ for (const direction of ["right", "up", "left", "down", "right", "up", "left"] as const) {
80
+ const next = splitGroup(tree, focusGroupId, direction);
81
+
82
+ tree = next.tree;
83
+ focusGroupId = next.focusGroupId;
84
+ }
85
+
86
+ for (const [candidate, width, height] of [
87
+ [tree, 240, 100],
88
+ [first, 40, 12],
89
+ ] as const) {
90
+ let splitCalls = 0;
91
+ const setup = await testRender(
92
+ <EditorGrid
93
+ tree={candidate}
94
+ focusedGroupId={first.id}
95
+ onFocusGroup={() => {}}
96
+ onActivateTab={() => {}}
97
+ onCloseTab={() => {}}
98
+ onSplit={() => {
99
+ splitCalls += 1;
100
+ }}
101
+ onZoom={() => {}}
102
+ zoomed={false}
103
+ renderTab={() => <text>body</text>}
104
+ />,
105
+ { width, height },
106
+ );
107
+
108
+ allowEventLoopUpdates();
109
+ await setup.waitForVisualIdle();
110
+ const button = findById(setup.renderer.root, `split-button:${first.id}`);
111
+
112
+ expect(button).toBeDefined();
113
+ await setup.mockMouse.click(button!.x, button!.y);
114
+ await setup.renderOnce();
115
+ expect(setup.captureCharFrame()).not.toContain("right →");
116
+ expect(splitCalls).toBe(0);
117
+ setup.renderer.destroy();
118
+ }
119
+ });
@@ -0,0 +1,86 @@
1
+ import React from "react";
2
+ import { DARK } from "../../appearance/theme";
3
+ import type { Story, StoryMeta } from "../../stories/story";
4
+ import { EditorGrid } from "./EditorGrid";
5
+ import { changesTab, fileTab, makeGroup, splitGroup, type EditorTab } from "./editor-grid";
6
+
7
+ export const meta: StoryMeta = { title: "Surfaces/EditorGrid" };
8
+
9
+ const noop = (): void => {};
10
+
11
+ function renderTab(tab: EditorTab): React.ReactNode {
12
+ return (
13
+ <box style={{ flexDirection: "column", paddingLeft: 1, paddingTop: 1 }}>
14
+ <text fg={DARK.textMuted}>{tab.kind === "changes" ? "the whole diff" : tab.path}</text>
15
+ </box>
16
+ );
17
+ }
18
+
19
+ const oneGroup = makeGroup([changesTab(), fileTab("App.tsx", "src/App.tsx", "diff")]);
20
+ const sourceGroup = makeGroup([fileTab("plugin.json", ".claude-plugin/plugin.json", "contents")]);
21
+ const splitTree = splitGroup(sourceGroup, sourceGroup.id, "right").tree;
22
+
23
+ export const OneGroup: Story = {
24
+ render: () => (
25
+ <EditorGrid
26
+ tree={oneGroup}
27
+ focusedGroupId={oneGroup.id}
28
+ onFocusGroup={noop}
29
+ onActivateTab={noop}
30
+ onCloseTab={noop}
31
+ onSplit={noop}
32
+ onZoom={noop}
33
+ zoomed={false}
34
+ renderTab={renderTab}
35
+ theme={DARK}
36
+ />
37
+ ),
38
+ expectedColors: [DARK.textMuted, DARK.border],
39
+ size: { width: 80, height: 16 },
40
+ };
41
+
42
+ export const SplitGroups: Story = {
43
+ render: () => (
44
+ <EditorGrid
45
+ tree={splitTree}
46
+ focusedGroupId={null}
47
+ onFocusGroup={noop}
48
+ onActivateTab={noop}
49
+ onCloseTab={noop}
50
+ onSplit={noop}
51
+ onZoom={noop}
52
+ zoomed={false}
53
+ renderTab={renderTab}
54
+ theme={DARK}
55
+ />
56
+ ),
57
+ expectedColors: [DARK.textMuted, DARK.border],
58
+ size: { width: 100, height: 16 },
59
+ };
60
+
61
+ /** The rightmost group takes the remainder after whole-cell leading groups and divider cells. */
62
+ export const WholeCellSizing: Story = {
63
+ render: () => (
64
+ <box style={{ flexDirection: "column", width: 100, height: 16 }}>
65
+ <text fg={DARK.text}>Whole-cell split sizing</text>
66
+ <text fg={DARK.textMuted}>100 cells = 49 left + 1 divider + 50 right</text>
67
+ <text fg={DARK.textDim}>Fix the leading width; let the last group take the remainder.</text>
68
+ <box style={{ flexGrow: 1, minHeight: 0 }}>
69
+ <EditorGrid
70
+ tree={splitTree}
71
+ focusedGroupId={null}
72
+ onFocusGroup={noop}
73
+ onActivateTab={noop}
74
+ onCloseTab={noop}
75
+ onSplit={noop}
76
+ onZoom={noop}
77
+ zoomed={false}
78
+ renderTab={renderTab}
79
+ theme={DARK}
80
+ />
81
+ </box>
82
+ </box>
83
+ ),
84
+ expectedColors: [DARK.textMuted, DARK.border],
85
+ size: { width: 100, height: 16 },
86
+ };