@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,129 @@
1
+ /**
2
+ * The "/" quick-action palette behind a composer: the skill list a leading
3
+ * slash opens, its prefix > substring > subsequence filter, and the inline
4
+ * "/word" completion offered mid-sentence. Pure; the surfaces render it.
5
+ */
6
+
7
+ import { quickActionBody, type QuickAction } from "../settings/config";
8
+
9
+ export interface SlashItem {
10
+ name: string;
11
+ description: string;
12
+ body: string;
13
+ }
14
+
15
+ export function slashItemsFrom(quickActions: QuickAction[]): SlashItem[] {
16
+ return quickActions.map((action) => ({
17
+ name: action.prompt
18
+ .toLowerCase()
19
+ .replace(/[^a-z0-9]+/g, "-")
20
+ .replace(/^-|-$/g, ""),
21
+ description: action.metadata ?? action.prompt,
22
+ body: quickActionBody(action),
23
+ }));
24
+ }
25
+
26
+ const WORD_BOUNDARY = /[\s\-_./:]/;
27
+
28
+ /** Order-preserving subsequence score, or null when a query character is missing; higher is better. */
29
+ export function scoreMatch(name: string, query: string): number | null {
30
+ if (query.length === 0) return 0;
31
+ const lowerName = name.toLowerCase();
32
+ const lowerQuery = query.toLowerCase();
33
+
34
+ if (lowerName === lowerQuery) return 1000;
35
+ if (lowerName.startsWith(lowerQuery)) return 500 - name.length;
36
+ let score = 0;
37
+ let cursor = 0;
38
+ let run = 0;
39
+
40
+ for (const character of lowerQuery) {
41
+ const found = lowerName.indexOf(character, cursor);
42
+
43
+ if (found === -1) return null;
44
+ run = found === cursor ? run + 1 : 0;
45
+ score += found === cursor ? 5 + run : 1;
46
+ if (found === 0 || WORD_BOUNDARY.test(lowerName[found - 1]!)) score += 10;
47
+ score -= found - cursor;
48
+ cursor = found + 1;
49
+ }
50
+
51
+ return score;
52
+ }
53
+
54
+ export function slashFilter(items: SlashItem[], query: string): SlashItem[] {
55
+ if (query.length === 0) return items;
56
+ const scored: Array<{ item: SlashItem; score: number }> = [];
57
+
58
+ for (const item of items) {
59
+ const score = scoreMatch(item.name, query);
60
+
61
+ if (score !== null) scored.push({ item, score });
62
+ }
63
+
64
+ return scored.toSorted((left, right) => right.score - left.score).map((entry) => entry.item);
65
+ }
66
+
67
+ // a skill or action name: the grammar loadSkills and slashItemsFrom can produce, so a "/name"
68
+ // token closes only on whitespace or another slash, never inside a valid underscore-or-hyphen name
69
+ const SLASH_TOKEN = /(?:^|\s)(\/[a-zA-Z0-9_-]*)$/;
70
+
71
+ /**
72
+ * The "/word" token the caret is writing right now: the run from the last
73
+ * whitespace up to the caret, at the start of the draft or after it. Null once a
74
+ * space closes the token, so each new "/" reopens the palette and skills chain.
75
+ * Caret-aware, so editing an earlier "/name" targets it, not a trailing token.
76
+ */
77
+ export function activeSlashToken(text: string, caret: number): string | null {
78
+ const head = text.slice(0, Math.max(0, Math.min(caret, text.length)));
79
+ const match = SLASH_TOKEN.exec(head);
80
+
81
+ return match ? match[1]! : null;
82
+ }
83
+
84
+ /** True while the whole draft is just a "/query" - the palette owns it, so a blur discards it. */
85
+ export function isStandaloneSlashQuery(text: string): boolean {
86
+ return /^\/\S*$/.test(text.trim());
87
+ }
88
+
89
+ export function insertSlashItem(text: string, caret: number, itemName: string) {
90
+ const token = activeSlashToken(text, caret) ?? "";
91
+ const cut = caret - token.length;
92
+ const insertion = `/${itemName} `;
93
+
94
+ return {
95
+ text: text.slice(0, cut) + insertion + text.slice(caret),
96
+ caret: cut + insertion.length,
97
+ };
98
+ }
99
+
100
+ /**
101
+ * The character ranges of every completed "/name" in the draft that names a real
102
+ * action or skill, so a surface can paint the references. A half-typed "/na" is
103
+ * not a reference yet, so it stays unpainted until the palette completes it.
104
+ */
105
+ export function skillReferenceRanges(
106
+ text: string,
107
+ names: ReadonlySet<string>,
108
+ ): Array<{ start: number; end: number }> {
109
+ const ranges: Array<{ start: number; end: number }> = [];
110
+ const pattern = /(?:^|\s)(\/[a-zA-Z0-9_-]+)/g;
111
+
112
+ for (let match = pattern.exec(text); match !== null; match = pattern.exec(text)) {
113
+ const token = match[1]!;
114
+
115
+ if (!names.has(token.slice(1))) continue;
116
+ const start = match.index + match[0].length - token.length;
117
+
118
+ ranges.push({ start, end: start + token.length });
119
+ }
120
+
121
+ return ranges;
122
+ }
123
+
124
+ /** Quick actions and user skills in one palette; a quick action wins a name collision (it expands). */
125
+ export function mergeSlashItems(actions: SlashItem[], skills: SlashItem[]): SlashItem[] {
126
+ const taken = new Set(actions.map((action) => action.name));
127
+
128
+ return [...actions, ...skills.filter((skill) => !taken.has(skill.name))];
129
+ }
@@ -0,0 +1,79 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { resolveNavKey } from "./thread-chords";
3
+
4
+ const owner = { isOwner: true, resolved: false, treeActive: false };
5
+ const readOnly = { type: "status" as const, message: "observer - read-only" };
6
+ const submitted = { type: "status" as const, message: "review submitted - read-only" };
7
+
8
+ describe("resolveNavKey", () => {
9
+ test("the session commands: submit, edit, share", () => {
10
+ expect(resolveNavKey({ name: "return" }, owner)).toEqual({ type: "openSubmit" });
11
+ expect(resolveNavKey({ name: "enter" }, owner)).toEqual({ type: "openSubmit" });
12
+ expect(resolveNavKey({ name: "e" }, owner)).toEqual({ type: "edit" });
13
+ expect(resolveNavKey({ name: "s" }, owner)).toEqual({ type: "share" });
14
+ });
15
+
16
+ test("a diff row's keys win over the shared letters, then fall through to curation", () => {
17
+ const diff = { ...owner, isDiff: true };
18
+
19
+ expect(resolveNavKey({ name: "x" }, diff)).toEqual({ type: "rejectChange" });
20
+ expect(resolveNavKey({ name: "c" }, diff)).toEqual({ type: "foldFile" });
21
+ expect(resolveNavKey({ name: "d" }, diff)).toEqual({ type: "toggleDiffView" });
22
+ expect(resolveNavKey({ name: "k" }, diff)).toEqual({ type: "walkStart" });
23
+ // a letter with no diff meaning still reaches the curation commands
24
+ expect(resolveNavKey({ name: "n" }, diff)).toEqual({ type: "nextAnnotation" });
25
+ });
26
+
27
+ test("the discussion and curation commands", () => {
28
+ expect(resolveNavKey({ name: "n" }, owner)).toEqual({ type: "nextAnnotation" });
29
+ expect(resolveNavKey({ name: "p" }, owner)).toEqual({ type: "prevAnnotation" });
30
+ expect(resolveNavKey({ name: "backspace" }, owner)).toEqual({ type: "removeAnnotation" });
31
+ expect(resolveNavKey({ name: "r" }, owner)).toEqual({ type: "openRename" });
32
+ expect(resolveNavKey({ name: "x" }, owner)).toEqual({ type: "cut" });
33
+ expect(resolveNavKey({ name: "u" }, owner)).toEqual({ type: "restoreCuration" });
34
+ });
35
+
36
+ test("the tree commands: move on the Tree tab, go, branch, label, fork, hand off", () => {
37
+ const onTree = { ...owner, treeActive: true };
38
+
39
+ expect(resolveNavKey({ name: "n" }, onTree)).toEqual({ type: "treeMove", direction: 1 });
40
+ expect(resolveNavKey({ name: "p" }, onTree)).toEqual({ type: "treeMove", direction: -1 });
41
+ expect(resolveNavKey({ name: "g" }, owner)).toEqual({ type: "treeGo" });
42
+ expect(resolveNavKey({ name: "b" }, owner)).toEqual({ type: "treeBranch" });
43
+ expect(resolveNavKey({ name: "l" }, owner)).toEqual({ type: "treeLabel" });
44
+ expect(resolveNavKey({ name: "f" }, owner)).toEqual({ type: "treeFork" });
45
+ expect(resolveNavKey({ name: "h" }, owner)).toEqual({ type: "treeForkShare" });
46
+ });
47
+
48
+ test("a shift letter resolves as its uppercase, which no command claims, so it returns to typing", () => {
49
+ expect(resolveNavKey({ name: "x", shift: true }, { ...owner, isDiff: true })).toBeNull();
50
+ });
51
+
52
+ test("a collaborator is refused the owner's commands; renaming stays open", () => {
53
+ const collaborator = { ...owner, isOwner: false };
54
+
55
+ expect(resolveNavKey({ name: "return" }, collaborator)).toEqual(readOnly);
56
+ expect(resolveNavKey({ name: "e" }, collaborator)).toEqual(readOnly);
57
+ expect(resolveNavKey({ name: "s" }, collaborator)).toEqual(readOnly);
58
+ expect(resolveNavKey({ name: "x" }, collaborator)).toEqual(readOnly);
59
+ expect(resolveNavKey({ name: "g" }, collaborator)).toEqual(readOnly);
60
+ expect(resolveNavKey({ name: "f" }, collaborator)).toEqual(readOnly);
61
+ expect(resolveNavKey({ name: "r" }, collaborator)).toEqual({ type: "openRename" });
62
+ });
63
+
64
+ test("a resolved review refuses every change and says so; a fork stays open", () => {
65
+ const resolved = { ...owner, resolved: true };
66
+
67
+ expect(resolveNavKey({ name: "return" }, resolved)).toBeNull();
68
+ expect(resolveNavKey({ name: "e" }, resolved)).toEqual(submitted);
69
+ expect(resolveNavKey({ name: "x" }, resolved)).toEqual(submitted);
70
+ expect(resolveNavKey({ name: "b" }, resolved)).toEqual(submitted);
71
+ expect(resolveNavKey({ name: "f" }, resolved)).toEqual({ type: "treeFork" });
72
+ expect(resolveNavKey({ name: "s" }, resolved)).toEqual({ type: "share" });
73
+ });
74
+
75
+ test("an unmapped key returns null, so the surface returns to typing", () => {
76
+ expect(resolveNavKey({ name: "z" }, owner)).toBeNull();
77
+ expect(resolveNavKey({ name: "j" }, owner)).toBeNull();
78
+ });
79
+ });
@@ -0,0 +1,163 @@
1
+ import type { Intent } from "./keymap";
2
+
3
+ export interface NavKey {
4
+ name: string;
5
+ shift?: boolean;
6
+ }
7
+
8
+ export interface ThreadNavContext {
9
+ isOwner: boolean;
10
+ resolved: boolean;
11
+ treeActive: boolean;
12
+ isDiff?: boolean;
13
+ }
14
+
15
+ export interface ChordEntry {
16
+ keys: string;
17
+ label: string;
18
+ }
19
+
20
+ export function sessionCommandEntries(): ChordEntry[] {
21
+ return [
22
+ { keys: "⏎", label: "submit the review" },
23
+ { keys: "e", label: "edit in $EDITOR" },
24
+ { keys: "s", label: "share" },
25
+ ];
26
+ }
27
+
28
+ export function diffCommandEntries(): ChordEntry[] {
29
+ return [
30
+ { keys: "x", label: "reject the change under the caret" },
31
+ { keys: "u", label: "restore the last rejection" },
32
+ { keys: "c", label: "collapse the file to its band" },
33
+ { keys: "d", label: "split / stacked (when wide)" },
34
+ { keys: "k", label: "start the guided walk" },
35
+ ];
36
+ }
37
+
38
+ export function curationCommandEntries(): ChordEntry[] {
39
+ return [
40
+ { keys: "n / p", label: "next / previous card" },
41
+ { keys: "z", label: "fold / unfold the card" },
42
+ { keys: "⌫", label: "delete the card" },
43
+ { keys: "r", label: "rename the author" },
44
+ { keys: "x", label: "cut the block" },
45
+ { keys: "u", label: "restore the last cut" },
46
+ ];
47
+ }
48
+
49
+ export function treeCommandEntries(): ChordEntry[] {
50
+ return [
51
+ { keys: "g", label: "go to the entry" },
52
+ { keys: "b", label: "branch off the tip" },
53
+ { keys: "l", label: "label a checkpoint" },
54
+ { keys: "f", label: "fork the path" },
55
+ { keys: "h", label: "fork and hand off" },
56
+ ];
57
+ }
58
+
59
+ const READ_ONLY: Intent = { type: "status", message: "observer - read-only" };
60
+ const RESOLVED: Intent = { type: "status", message: "review submitted - read-only" };
61
+
62
+ function mutating(intent: Intent, context: ThreadNavContext): Intent {
63
+ if (!context.isOwner) return READ_ONLY;
64
+ if (context.resolved) return RESOLVED;
65
+
66
+ return intent;
67
+ }
68
+
69
+ function resolveDiffKey(name: string, context: ThreadNavContext): Intent | null {
70
+ switch (name) {
71
+ case "x":
72
+ return mutating({ type: "rejectChange" }, context);
73
+ case "c":
74
+ return { type: "foldFile" };
75
+ case "d":
76
+ return { type: "toggleDiffView" };
77
+ case "k":
78
+ return context.resolved ? RESOLVED : { type: "walkStart" };
79
+ default:
80
+ return null;
81
+ }
82
+ }
83
+
84
+ function resolveSessionKey(name: string, context: ThreadNavContext): Intent | null {
85
+ switch (name) {
86
+ case "e":
87
+ return mutating({ type: "edit" }, context);
88
+ case "s":
89
+ return context.isOwner ? { type: "share" } : READ_ONLY;
90
+ default:
91
+ return null;
92
+ }
93
+ }
94
+
95
+ function resolveCurationKey(name: string, context: ThreadNavContext): Intent | null {
96
+ switch (name) {
97
+ case "n":
98
+ return context.treeActive ? { type: "treeMove", direction: 1 } : { type: "nextAnnotation" };
99
+ case "p":
100
+ return context.treeActive ? { type: "treeMove", direction: -1 } : { type: "prevAnnotation" };
101
+ case "g":
102
+ return mutating({ type: "treeGo" }, context);
103
+ case "b":
104
+ return mutating({ type: "treeBranch" }, context);
105
+ case "l":
106
+ return mutating({ type: "treeLabel" }, context);
107
+ case "f":
108
+ return context.isOwner ? { type: "treeFork" } : READ_ONLY;
109
+ case "h":
110
+ return context.isOwner ? { type: "treeForkShare" } : READ_ONLY;
111
+ case "backspace":
112
+ return mutating({ type: "removeAnnotation" }, context);
113
+ case "r":
114
+ return { type: "openRename" };
115
+ case "x":
116
+ return mutating({ type: "cut" }, context);
117
+ case "u":
118
+ return mutating({ type: "restoreCuration" }, context);
119
+ default:
120
+ return null;
121
+ }
122
+ }
123
+
124
+ export interface SessionChordKey {
125
+ name: string;
126
+ ctrl?: boolean;
127
+ meta?: boolean;
128
+ super?: boolean;
129
+ }
130
+
131
+ export function resolveSessionChord(
132
+ key: SessionChordKey,
133
+ context: Pick<ThreadNavContext, "isOwner" | "resolved">,
134
+ ): Intent | null {
135
+ if (key.name === "return" || key.name === "enter") {
136
+ if (!(key.ctrl || key.meta || key.super)) return null;
137
+ if (!context.isOwner) return READ_ONLY;
138
+
139
+ return context.resolved ? null : { type: "openSubmit" };
140
+ }
141
+ if (!key.ctrl) return null;
142
+ if (key.name === "e")
143
+ return context.resolved ? RESOLVED : context.isOwner ? { type: "edit" } : READ_ONLY;
144
+ if (key.name === "s") return context.isOwner ? { type: "share" } : READ_ONLY;
145
+
146
+ return null;
147
+ }
148
+
149
+ export function resolveNavKey(key: NavKey, context: ThreadNavContext): Intent | null {
150
+ const name = key.shift ? key.name.toUpperCase() : key.name;
151
+
152
+ if (name === "return" || name === "enter") {
153
+ if (!context.isOwner) return READ_ONLY;
154
+
155
+ return context.resolved ? null : { type: "openSubmit" };
156
+ }
157
+
158
+ return (
159
+ (context.isDiff ? resolveDiffKey(name, context) : null) ??
160
+ resolveSessionKey(name, context) ??
161
+ resolveCurationKey(name, context)
162
+ );
163
+ }
@@ -0,0 +1,55 @@
1
+ /** One keypress subscription per renderer, even when several editor tiles are mounted. */
2
+
3
+ import { useEffect, useEffectEvent } from "react";
4
+ import { useRenderer } from "@opentui/react";
5
+ import type { KeyEvent } from "@opentui/core";
6
+
7
+ type KeyInput = NonNullable<ReturnType<typeof useRenderer>>["keyInput"];
8
+ type KeyHandler = (key: KeyEvent) => void;
9
+
10
+ interface KeySubscriptions {
11
+ handlers: Set<KeyHandler>;
12
+ notify: KeyHandler;
13
+ }
14
+
15
+ const subscribers = new WeakMap<KeyInput, KeySubscriptions>();
16
+
17
+ function subscribeToKeypress(input: KeyInput, handler: KeyHandler): () => void {
18
+ let subscription = subscribers.get(input);
19
+
20
+ if (!subscription) {
21
+ const handlers = new Set<KeyHandler>();
22
+ const notify = (key: KeyEvent): void => {
23
+ for (const subscriber of handlers) subscriber(key);
24
+ };
25
+
26
+ subscription = { handlers, notify };
27
+ subscribers.set(input, subscription);
28
+ input.on("keypress", notify);
29
+ }
30
+
31
+ const { handlers, notify } = subscription;
32
+
33
+ handlers.add(handler);
34
+
35
+ return () => {
36
+ handlers.delete(handler);
37
+ if (handlers.size > 0) return;
38
+
39
+ input.off("keypress", notify);
40
+ subscribers.delete(input);
41
+ };
42
+ }
43
+
44
+ export function useSharedKeyboard(handler: KeyHandler): void {
45
+ const renderer = useRenderer();
46
+ const stableHandler = useEffectEvent(handler);
47
+
48
+ useEffect(() => {
49
+ const input = renderer?.keyInput;
50
+
51
+ if (!input) return;
52
+
53
+ return subscribeToKeypress(input, stableHandler);
54
+ }, [renderer]);
55
+ }
@@ -0,0 +1,100 @@
1
+ // Bun Snapshot v1, https://bun.sh/docs/test/snapshots
2
+
3
+ exports[`view-plan regression harness (M0) blockRuns output is stable across the representative blocks 1`] = `
4
+ [
5
+ {
6
+ "runs": [
7
+ {
8
+ "role": "plain",
9
+ "start": 0,
10
+ "text": "Heading with ",
11
+ },
12
+ {
13
+ "role": "strong",
14
+ "start": 15,
15
+ "text": "bold",
16
+ },
17
+ ],
18
+ "text": "Heading with **bold**",
19
+ "type": "same",
20
+ },
21
+ {
22
+ "runs": [
23
+ {
24
+ "role": "plain",
25
+ "start": 0,
26
+ "text": "A paragraph with ",
27
+ },
28
+ {
29
+ "role": "strong",
30
+ "start": 19,
31
+ "text": "strong",
32
+ },
33
+ {
34
+ "role": "plain",
35
+ "start": 27,
36
+ "text": ", ",
37
+ },
38
+ {
39
+ "role": "em",
40
+ "start": 30,
41
+ "text": "em",
42
+ },
43
+ {
44
+ "role": "plain",
45
+ "start": 33,
46
+ "text": ", ",
47
+ },
48
+ {
49
+ "role": "code",
50
+ "start": 36,
51
+ "text": "code",
52
+ },
53
+ {
54
+ "role": "plain",
55
+ "start": 41,
56
+ "text": ", ",
57
+ },
58
+ {
59
+ "role": "strike",
60
+ "start": 45,
61
+ "text": "strike",
62
+ },
63
+ {
64
+ "role": "plain",
65
+ "start": 53,
66
+ "text": " and a ",
67
+ },
68
+ {
69
+ "href": "https://example.com",
70
+ "role": "link",
71
+ "start": 61,
72
+ "text": "label",
73
+ },
74
+ {
75
+ "role": "plain",
76
+ "start": 88,
77
+ "text": ".",
78
+ },
79
+ ],
80
+ "text": "A paragraph with **strong**, *em*, \`code\`, ~~strike~~ and a [label](https://example.com).",
81
+ "type": "same",
82
+ },
83
+ {
84
+ "runs": [
85
+ {
86
+ "role": "plain",
87
+ "start": 0,
88
+ "text": "a list item with ",
89
+ },
90
+ {
91
+ "role": "em",
92
+ "start": 18,
93
+ "text": "emphasis",
94
+ },
95
+ ],
96
+ "text": "a list item with *emphasis*",
97
+ "type": "same",
98
+ },
99
+ ]
100
+ `;
@@ -0,0 +1,33 @@
1
+ import React from "react";
2
+ import { DARK } from "../../appearance/theme";
3
+ import { buildDisplay, type DisplayBlock } from "../view-plan";
4
+ import type { Story, StoryMeta } from "../../stories/story";
5
+ import { MarkdownGridBlock } from "./MarkdownGridBlock";
6
+
7
+ export const meta: StoryMeta = { title: "Surfaces/MarkdownGridBlock" };
8
+
9
+ /** The first display block of a kind, for a story that renders one grid in isolation. */
10
+ function firstBlock(markdown: string, kind: DisplayBlock["kind"]): DisplayBlock {
11
+ return buildDisplay(markdown).find((block) => block.kind === kind)!;
12
+ }
13
+
14
+ const TABLE = firstBlock(
15
+ "| Name | Size |\n| :--- | ---: |\n| daemon | 12 |\n| client | 7 |",
16
+ "table",
17
+ );
18
+ const FRONTMATTER = firstBlock(
19
+ "---\ntitle: Migration Plan\nowner: platform team\nstatus: in review\n---\n\n# Plan\n",
20
+ "frontmatter",
21
+ );
22
+
23
+ export const Table: Story = {
24
+ render: () => <MarkdownGridBlock block={TABLE} theme={DARK} contentWidth={60} />,
25
+ expectedColors: [DARK.text, DARK.textDim],
26
+ size: { width: 62, height: 6 },
27
+ };
28
+
29
+ export const Frontmatter: Story = {
30
+ render: () => <MarkdownGridBlock block={FRONTMATTER} theme={DARK} contentWidth={60} />,
31
+ expectedColors: [DARK.text, DARK.textDim],
32
+ size: { width: 62, height: 9 },
33
+ };
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Render a table or frontmatter display block as its grid: a GFM table (bold
3
+ * header over a rule, aligned columns) or a leading-frontmatter key/value box.
4
+ * Layout is computed in markdown-grid; this only paints the positioned segments,
5
+ * dim for borders and bold for the header or key column. The grid annotates as
6
+ * one unit: every row registers the same block span for hit-testing, so a click
7
+ * or drag anchors a comment on it and a mark tints the whole grid.
8
+ */
9
+
10
+ import React from "react";
11
+ import type { MouseEvent as TerminalMouseEvent, TextRenderable } from "@opentui/core";
12
+ import { displayText, type DisplayBlock } from "../view-plan";
13
+ import type { Theme } from "../../appearance/theme";
14
+ import { BOLD } from "../../annotations/annotation-palette";
15
+ import {
16
+ layoutFrontmatterGrid,
17
+ layoutMarkdownTable,
18
+ parseFrontmatterRows,
19
+ parseMarkdownTable,
20
+ type GridLine,
21
+ } from "../markdown-grid";
22
+
23
+ function gridLines(block: DisplayBlock, contentWidth: number): GridLine[] {
24
+ if (block.kind === "table") return layoutMarkdownTable(parseMarkdownTable(displayText(block)));
25
+
26
+ return layoutFrontmatterGrid(parseFrontmatterRows(displayText(block)), contentWidth);
27
+ }
28
+
29
+ export function MarkdownGridBlock({
30
+ block,
31
+ theme,
32
+ contentWidth,
33
+ marked = false,
34
+ markBackdrop,
35
+ registerRow,
36
+ onRowMouseDown,
37
+ }: {
38
+ block: DisplayBlock;
39
+ theme: Theme;
40
+ contentWidth: number;
41
+ /** True while a discussion or held selection marks this grid; tints it so the annotation reads. */
42
+ marked?: boolean;
43
+ markBackdrop?: string;
44
+ /** Registers a grid row's renderable with the annotation surface, all mapping to the one block span. */
45
+ registerRow?: (rowIndex: number) => (renderable: TextRenderable | null) => void;
46
+ onRowMouseDown?: (event: TerminalMouseEvent) => void;
47
+ }): React.ReactNode {
48
+ const lines = gridLines(block, Math.max(12, contentWidth));
49
+
50
+ return (
51
+ <box style={{ flexDirection: "column", backgroundColor: marked ? markBackdrop : undefined }}>
52
+ {lines.map((line, lineIndex) => (
53
+ <box key={`grid-${lineIndex}`} style={{ flexDirection: "row" }}>
54
+ <text selectable={false}>
55
+ <span fg={theme.textDim}>{" "}</span>
56
+ </text>
57
+ <text selectable={false} ref={registerRow?.(lineIndex)} onMouseDown={onRowMouseDown}>
58
+ {line.map((segment, segmentIndex) => (
59
+ <span
60
+ key={segmentIndex}
61
+ fg={segment.dim ? theme.textDim : theme.text}
62
+ attributes={segment.bold ? BOLD : 0}
63
+ >
64
+ {segment.text}
65
+ </span>
66
+ ))}
67
+ </text>
68
+ </box>
69
+ ))}
70
+ </box>
71
+ );
72
+ }
@@ -0,0 +1,15 @@
1
+ import React from "react";
2
+ import { DARK } from "../../appearance/theme";
3
+ import type { Story, StoryMeta } from "../../stories/story";
4
+ import { MarkdownThreadEditor } from "./MarkdownThreadEditor";
5
+
6
+ export const meta: StoryMeta = { title: "Surfaces/MarkdownThreadEditor" };
7
+
8
+ const SAMPLE =
9
+ "# Rollout Plan\n\nShip the store move behind a `flag`. See [docs](https://x.dev).\n\n- first step\n- second step\n";
10
+
11
+ export const Editing: Story = {
12
+ render: () => <MarkdownThreadEditor initialText={SAMPLE} theme={DARK} onExitEditor={() => {}} />,
13
+ expectedColors: [DARK.blue, DARK.textMuted, DARK.textDim],
14
+ size: { width: 62, height: 14 },
15
+ };