@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,72 @@
1
+ /** Interpret an absolute-positioned ANSI byte stream (cursor moves plus printed text) into a grid of rows. */
2
+
3
+ const CSI = "\x1b[";
4
+
5
+ export function ansiScreenToLines(ansi: string, cols: number, rows: number): string[] {
6
+ const grid: string[][] = Array.from({ length: rows }, () =>
7
+ Array.from({ length: cols }, () => " "),
8
+ );
9
+ let row = 0;
10
+ let column = 0;
11
+ let index = 0;
12
+
13
+ const put = (character: string): void => {
14
+ if (row >= 0 && row < rows && column >= 0 && column < cols) grid[row]![column] = character;
15
+ column += 1;
16
+ };
17
+
18
+ while (index < ansi.length) {
19
+ const character = ansi[index]!;
20
+
21
+ if (character === "\x1b") {
22
+ index = consumeEscape(ansi, index, (nextRow, nextColumn) => {
23
+ if (nextRow === "clear") {
24
+ for (const line of grid) line.fill(" ");
25
+ row = 0;
26
+ column = 0;
27
+ } else {
28
+ row = nextRow;
29
+ column = nextColumn;
30
+ }
31
+ });
32
+ continue;
33
+ }
34
+ if (character === "\n") {
35
+ row += 1;
36
+ column = 0;
37
+ } else if (character === "\r") {
38
+ column = 0;
39
+ } else {
40
+ put(character);
41
+ }
42
+ index += 1;
43
+ }
44
+
45
+ return grid.map((line) => line.join("").replace(/\s+$/, ""));
46
+ }
47
+
48
+ type EscapeEffect = (row: number | "clear", column: number) => void;
49
+
50
+ function consumeEscape(ansi: string, start: number, apply: EscapeEffect): number {
51
+ if (ansi[start + 1] === "]") {
52
+ const bell = ansi.indexOf("\x07", start);
53
+
54
+ return bell === -1 ? ansi.length : bell + 1;
55
+ }
56
+ if (ansi.slice(start, start + CSI.length) !== CSI) return start + 1;
57
+ let index = start + CSI.length;
58
+
59
+ while (index < ansi.length && !/[A-Za-z]/.test(ansi[index]!)) index += 1;
60
+ const final = ansi[index];
61
+ const parameters = ansi.slice(start + CSI.length, index);
62
+
63
+ if (final === "H" || final === "f") {
64
+ const [rowParameter = "1", columnParameter = "1"] = parameters.split(";");
65
+
66
+ apply(Math.max(0, Number(rowParameter) - 1), Math.max(0, Number(columnParameter) - 1));
67
+ } else if (final === "J" && parameters === "2") {
68
+ apply("clear", 0);
69
+ }
70
+
71
+ return index + 1;
72
+ }
@@ -0,0 +1,12 @@
1
+ import React from "react";
2
+ import type { Story, StoryMeta } from "../../stories/story";
3
+ import { AnsiScreen } from "./AnsiScreen";
4
+
5
+ export const meta: StoryMeta = { title: "Primitives/AnsiScreen" };
6
+
7
+ const POSITIONED = "\x1b[2;4HHello from\x1b[4;6Hraw, positioned ANSI";
8
+
9
+ export const Positioned: Story = {
10
+ render: () => <AnsiScreen ansi={POSITIONED} cols={40} rows={6} />,
11
+ size: { width: 44, height: 8 },
12
+ };
@@ -0,0 +1,28 @@
1
+ /** Paints a raw ANSI screen (cursor-positioned text) as a terminal would, for previewing SSH surfaces. */
2
+
3
+ import React from "react";
4
+ import type { Theme } from "../../appearance/theme";
5
+ import { ansiScreenToLines } from "../ansi-screen";
6
+ import { useComponentTheme } from "../../appearance/components/theme-context";
7
+
8
+ export interface AnsiScreenProps {
9
+ ansi: string;
10
+ cols: number;
11
+ rows: number;
12
+ theme?: Theme;
13
+ }
14
+
15
+ export function AnsiScreen({ ansi, cols, rows, theme }: AnsiScreenProps): React.ReactNode {
16
+ const tokens = useComponentTheme(theme);
17
+ const lines = ansiScreenToLines(ansi, cols, rows);
18
+
19
+ return (
20
+ <box style={{ flexDirection: "column" }}>
21
+ {lines.map((line, index) => (
22
+ <text key={index} fg={tokens.text} selectable={false} style={{ wrapMode: "none" }}>
23
+ {line === "" ? " " : line}
24
+ </text>
25
+ ))}
26
+ </box>
27
+ );
28
+ }
@@ -0,0 +1,40 @@
1
+ /** The embedded terminal renderable runs a real PTY child through Ghostty's VT and paints its screen; a live `printf` child must show up in the OpenTUI frame. Skipped where no prebuilt libghostty-vt ships for the platform. */
2
+
3
+ import { describe, expect, test } from "bun:test";
4
+ import React from "react";
5
+ import { testRender } from "@opentui/react/test-utils";
6
+ import { waitForText } from "../../testing/test-support";
7
+ import {
8
+ embeddedTerminalAvailable,
9
+ registerTerminalPane,
10
+ TerminalPaneRenderable,
11
+ } from "./terminal-pane";
12
+
13
+ registerTerminalPane();
14
+
15
+ describe("TerminalPaneRenderable", () => {
16
+ test.skipIf(!embeddedTerminalAvailable())(
17
+ "paints a live child process's output into the box",
18
+ async () => {
19
+ // Arrange / Act - a child that prints a known line
20
+ const setup = await testRender(
21
+ React.createElement("terminalPane", {
22
+ command: "/bin/sh",
23
+ args: ["-c", "printf 'HELLO-EMBED\\n'; sleep 5"],
24
+ style: { width: 40, height: 6 },
25
+ }),
26
+ { width: 40, height: 6 },
27
+ );
28
+
29
+ // Assert - the child's output reaches the painted frame via Ghostty
30
+ await waitForText(setup, "HELLO-EMBED");
31
+ setup.renderer.destroy();
32
+ },
33
+ );
34
+
35
+ test("exposes availability + the renderable class", () => {
36
+ // Assert - the module loads and reports platform support honestly
37
+ expect([true, false]).toContain(embeddedTerminalAvailable());
38
+ expect(TerminalPaneRenderable).toBeDefined();
39
+ });
40
+ });
@@ -0,0 +1,196 @@
1
+ /**
2
+ * An OpenTUI renderable that runs a real child process (a shell, cc, pi, codex)
3
+ * on a PTY and paints its live screen into the box - the terminal-in-the-rail
4
+ * primitive. It wires the forkpty shim (child + tty, ../pty) to a Ghostty VT emulator
5
+ * (ghostty-terminal.ts) and blits the emulator's cell grid every frame via
6
+ * OptimizedBuffer.setCell. Register once with `registerTerminalPane`, then use
7
+ * `<terminalPane command="claude" ... />` in the OpenTUI React tree.
8
+ */
9
+
10
+ import { Renderable, RGBA, createTextAttributes, type RenderContext } from "@opentui/core";
11
+ import type { OptimizedBuffer } from "@opentui/core";
12
+ import type { RenderableOptions } from "@opentui/core";
13
+ import { extend } from "@opentui/react";
14
+ import { spawn, ptyAvailable, type IPty } from "../pty";
15
+ import {
16
+ loadGhosttyTerminals,
17
+ type GhosttyColor,
18
+ type GhosttyTerminal,
19
+ type GhosttyTerminalFactory,
20
+ } from "../ghostty-terminal";
21
+
22
+ /** One shared VT library load; null when this platform ships no prebuilt dylib. */
23
+ let ghosttyFactory: GhosttyTerminalFactory | null | undefined;
24
+
25
+ function factory(): GhosttyTerminalFactory | null {
26
+ if (ghosttyFactory === undefined) ghosttyFactory = loadGhosttyTerminals();
27
+
28
+ return ghosttyFactory;
29
+ }
30
+
31
+ /** Whether the embedded terminal can run here: both native shims ship for this
32
+ * platform (the Ghostty VT renderer and the forkpty PTY). */
33
+ export function embeddedTerminalAvailable(): boolean {
34
+ return factory() !== null && ptyAvailable();
35
+ }
36
+
37
+ /** Props for `<terminalPane>`: the child to run plus its cwd/env and a plan-context seed. */
38
+ export interface TerminalPaneOptions extends RenderableOptions {
39
+ command?: string;
40
+ args?: string[];
41
+ cwd?: string;
42
+ env?: Record<string, string>;
43
+ seedText?: string;
44
+ onExit?: (exitCode: number) => void;
45
+ }
46
+
47
+ function terminalEnvironment(env: NodeJS.ProcessEnv) {
48
+ return Object.fromEntries(
49
+ Object.entries(env).filter((entry): entry is [string, string] => entry[1] !== undefined),
50
+ );
51
+ }
52
+
53
+ const encoder = new TextEncoder();
54
+ const DEFAULT_FG = RGBA.fromInts(208, 208, 208, 255);
55
+ const TRANSPARENT = RGBA.fromValues(0, 0, 0, 0);
56
+ /** Glyph color under the block cursor (dark ink on the light cursor block). */
57
+ const CURSOR_INK = RGBA.fromInts(20, 20, 24, 255);
58
+
59
+ /** Runs a PTY child and paints its Ghostty-rendered screen into this box. */
60
+ export class TerminalPaneRenderable extends Renderable {
61
+ private pty: IPty | null = null;
62
+ private vt: GhosttyTerminal | null = null;
63
+ private cols = 0;
64
+ private rows = 0;
65
+ // The seed is held until the child's first output, so it lands in a ready
66
+ // prompt rather than racing the harness's startup (readline/TUI not up yet).
67
+ private pendingSeed: string | undefined;
68
+ private readonly opts: TerminalPaneOptions;
69
+
70
+ constructor(ctx: RenderContext, options: TerminalPaneOptions) {
71
+ super(ctx, options);
72
+ this.opts = options;
73
+ this.focusable = true;
74
+ }
75
+
76
+ /** Start the child + emulator once real layout dimensions are known. */
77
+ private start(cols: number, rows: number): void {
78
+ const terminals = factory();
79
+
80
+ if (!terminals) return;
81
+ const vt = terminals.create(cols, rows);
82
+
83
+ if (!vt) return; // no emulator, no visible screen - do not spawn a blind child
84
+ this.vt = vt;
85
+ this.cols = cols;
86
+ this.rows = rows;
87
+ this.pendingSeed = this.opts.seedText;
88
+ this.pty = spawn(this.opts.command ?? process.env.SHELL ?? "/bin/sh", this.opts.args ?? [], {
89
+ name: "xterm-256color",
90
+ cols,
91
+ rows,
92
+ cwd: this.opts.cwd ?? process.cwd(),
93
+ env: this.opts.env ?? terminalEnvironment(process.env),
94
+ });
95
+ this.pty.onData((data) => {
96
+ // the shim streams a UTF-8-decoded string (split multibyte is handled); a
97
+ // rare non-UTF-8 byte arrives as U+FFFD - acceptable for agent TUIs.
98
+ this.vt?.write(encoder.encode(data));
99
+ if (this.pendingSeed !== undefined) {
100
+ this.pty?.write(this.pendingSeed);
101
+ this.pendingSeed = undefined;
102
+ }
103
+ this.requestRender();
104
+ });
105
+ this.pty.onExit(({ exitCode }) => this.opts.onExit?.(exitCode));
106
+ }
107
+
108
+ /** Send raw bytes to the child (a key's escape sequence, or pasted text). */
109
+ write(data: string): void {
110
+ this.pty?.write(data);
111
+ }
112
+
113
+ protected onResize(width: number, height: number): void {
114
+ if (width <= 0 || height <= 0) return;
115
+ if (!this.pty) {
116
+ this.start(width, height);
117
+
118
+ return;
119
+ }
120
+ if (width === this.cols && height === this.rows) return;
121
+ this.cols = width;
122
+ this.rows = height;
123
+ this.vt?.resize(width, height);
124
+ this.pty.resize(width, height);
125
+ }
126
+
127
+ protected renderSelf(buffer: OptimizedBuffer): void {
128
+ if (!this.vt) return;
129
+ for (let y = 0; y < this.height; y++) {
130
+ for (let x = 0; x < this.width; x++) {
131
+ const cell = this.vt.readCell(x, y);
132
+
133
+ // Skip an empty normal-width cell; the buffer already starts blank.
134
+ if (cell && cell.width === 0 && cell.codepoint === 0) continue;
135
+ const char = cell && cell.codepoint ? String.fromCodePoint(cell.codepoint) : " ";
136
+ let fg = cell ? resolveColor(cell.fg, DEFAULT_FG) : DEFAULT_FG;
137
+ let bg = cell ? resolveColor(cell.bg, TRANSPARENT) : TRANSPARENT;
138
+
139
+ if (cell?.inverse) [fg, bg] = [bg, fg];
140
+ const attributes = cell
141
+ ? createTextAttributes({
142
+ bold: cell.bold,
143
+ italic: cell.italic,
144
+ underline: cell.underline,
145
+ dim: cell.faint,
146
+ strikethrough: cell.strikethrough,
147
+ })
148
+ : 0;
149
+
150
+ buffer.setCell(this.x + x, this.y + y, char, fg, bg, attributes);
151
+ }
152
+ }
153
+ // cursor is active-area coords; equal to viewport at the bottom (the live
154
+ // typing case). It can drift if the child scrolls the viewport back.
155
+ const cursor = this.vt.readCursor();
156
+
157
+ if (cursor.visible && cursor.x < this.width && cursor.y < this.height) {
158
+ const under = this.vt.readCell(cursor.x, cursor.y);
159
+ const char = under && under.codepoint ? String.fromCodePoint(under.codepoint) : " ";
160
+
161
+ // a block cursor: paint the cell with fg/bg swapped
162
+ buffer.setCell(this.x + cursor.x, this.y + cursor.y, char, CURSOR_INK, DEFAULT_FG, 0);
163
+ }
164
+ }
165
+
166
+ /** Kill the child and free the VT. Idempotent - the detach path calls this
167
+ * directly because the React reconciler removes a child without destroying it. */
168
+ shutdown(): void {
169
+ this.pty?.kill();
170
+ this.vt?.free();
171
+ this.pty = null;
172
+ this.vt = null;
173
+ }
174
+
175
+ protected destroySelf(): void {
176
+ this.shutdown();
177
+ super.destroySelf();
178
+ }
179
+ }
180
+
181
+ /** Map a Ghostty cell color to an OpenTUI RGBA, using `fallback` for the terminal default. */
182
+ function resolveColor(color: GhosttyColor, fallback: RGBA): RGBA {
183
+ if (color.kind === "rgb") return RGBA.fromInts(color.r, color.g, color.b, 255);
184
+ if (color.kind === "palette") return RGBA.fromIndex(color.index);
185
+
186
+ return fallback;
187
+ }
188
+
189
+ let registered = false;
190
+
191
+ /** Register `<terminalPane>` with the OpenTUI React reconciler (idempotent). */
192
+ export function registerTerminalPane(): void {
193
+ if (registered) return;
194
+ registered = true;
195
+ extend({ terminalPane: TerminalPaneRenderable });
196
+ }
@@ -0,0 +1,85 @@
1
+ /** The libghostty-vt FFI wrapper round-trips VT: fed bytes come back as decoded cells with colors and attributes. Skipped where no prebuilt dylib ships for the platform. */
2
+
3
+ import { describe, expect, test } from "bun:test";
4
+ import { loadGhosttyTerminals } from "./ghostty-terminal";
5
+
6
+ const factory = loadGhosttyTerminals();
7
+
8
+ describe("ghostty-terminal FFI", () => {
9
+ test("reports platform support without throwing", () => {
10
+ // Assert - null on unsupported platforms, a factory where a dylib ships
11
+ expect(factory === null || factory.create !== undefined).toBe(true);
12
+ });
13
+
14
+ test.skipIf(!factory)("writes VT bytes and reads back decoded cells", () => {
15
+ // Arrange
16
+ const term = factory!.create(80, 24)!;
17
+
18
+ expect(term).not.toBeNull();
19
+
20
+ // Act - plain text then bold + palette-green "GO"
21
+ term.write(new TextEncoder().encode("hi\x1b[1;32mGO\x1b[0m"));
22
+ const cells = [0, 1, 2, 3].map((x) => term.readCell(x, 0));
23
+
24
+ // Assert - glyphs decode, the row reads as text, and the green run is bold on ANSI palette index 2
25
+ expect(
26
+ cells.map((c) => (c?.codepoint ? String.fromCodePoint(c.codepoint) : " ")).join(""),
27
+ ).toBe("hiGO");
28
+ expect(term.rowText(0, 80)).toBe("hiGO");
29
+ expect(cells[2]!.bold).toBe(true);
30
+ expect(cells[2]!.fg).toEqual({ kind: "palette", index: 2 });
31
+ term.free();
32
+ });
33
+
34
+ test.skipIf(!factory)("tracks the cursor position as text is written", () => {
35
+ // Arrange
36
+ const term = factory!.create(80, 24)!;
37
+
38
+ // Act
39
+ term.write(new TextEncoder().encode("abc"));
40
+
41
+ // Assert - cursor advanced past the three glyphs on row 0
42
+ const cursor = term.readCursor();
43
+
44
+ expect(cursor.y).toBe(0);
45
+ expect(cursor.x).toBe(3);
46
+ term.free();
47
+ });
48
+
49
+ test.skipIf(!factory)(
50
+ "preserves the native screen contract across resize and styled text",
51
+ () => {
52
+ // Arrange
53
+ const term = factory!.create(8, 2)!;
54
+
55
+ // Act - resize first, then write RGB colors, every supported style, and a wide glyph
56
+ term.resize(6, 3);
57
+ term.write(
58
+ new TextEncoder().encode(
59
+ "\x1b[38;2;12;34;56;48;2;78;90;123;1;3;4;7;9mA\x1b[0m\x1b[2mF\x1b[0m\u754c",
60
+ ),
61
+ );
62
+
63
+ // Assert - the flat native ABI retains cell data and rejects coordinates outside the viewport
64
+ expect(term.readCell(0, 0)).toEqual({
65
+ codepoint: 0x41,
66
+ fg: { kind: "rgb", r: 12, g: 34, b: 56 },
67
+ bg: { kind: "rgb", r: 78, g: 90, b: 123 },
68
+ width: 0,
69
+ bold: true,
70
+ italic: true,
71
+ underline: true,
72
+ inverse: true,
73
+ faint: false,
74
+ strikethrough: true,
75
+ });
76
+ expect(term.readCell(1, 0)?.faint).toBe(true);
77
+ expect(term.readCell(2, 0)?.codepoint).toBe("\u754c".codePointAt(0));
78
+ expect(term.readCell(2, 0)?.width).toBe(1);
79
+ expect(term.readCell(3, 0)?.width).toBe(2);
80
+ expect(term.readCell(6, 0)).toBeNull();
81
+ expect(term.readCell(0, 3)).toBeNull();
82
+ term.free();
83
+ },
84
+ );
85
+ });
@@ -0,0 +1,197 @@
1
+ /**
2
+ * A live in-process terminal emulator backed by Ghostty's VT core (libghostty-vt)
3
+ * through a flat FFI shim (native/src/shim.zig). Feed it a child process's PTY
4
+ * output with `write`, then read the rendered screen cell-by-cell with `readCell`
5
+ * to paint it anywhere - this is what lets a real `cc`/shell render inside an
6
+ * OpenTUI box. Load with `loadGhosttyTerminals`, which returns null when no
7
+ * prebuilt dylib exists for this platform (callers fall back to a herdr split).
8
+ */
9
+
10
+ import { dlopen, FFIType, ptr } from "bun:ffi";
11
+ import { existsSync } from "node:fs";
12
+ import { join } from "node:path";
13
+
14
+ /** How a cell's color arrived from Ghostty: the terminal default, an ANSI palette index, or true RGB. */
15
+ export type GhosttyColor =
16
+ | { kind: "default" }
17
+ | { kind: "palette"; index: number }
18
+ | { kind: "rgb"; r: number; g: number; b: number };
19
+
20
+ /** The cursor's viewport position and whether it should be drawn. */
21
+ export interface GhosttyCursor {
22
+ x: number;
23
+ y: number;
24
+ visible: boolean;
25
+ }
26
+
27
+ /** One decoded screen cell: its glyph, colors, cell width, and text decorations. */
28
+ export interface GhosttyCell {
29
+ codepoint: number;
30
+ fg: GhosttyColor;
31
+ bg: GhosttyColor;
32
+ width: number;
33
+ bold: boolean;
34
+ italic: boolean;
35
+ underline: boolean;
36
+ inverse: boolean;
37
+ faint: boolean;
38
+ strikethrough: boolean;
39
+ }
40
+
41
+ const CVT_SYMBOLS = {
42
+ cvt_new: { args: [FFIType.u16, FFIType.u16], returns: FFIType.ptr },
43
+ cvt_free: { args: [FFIType.ptr], returns: FFIType.void },
44
+ cvt_write: { args: [FFIType.ptr, FFIType.ptr, FFIType.u64], returns: FFIType.void },
45
+ cvt_resize: { args: [FFIType.ptr, FFIType.u16, FFIType.u16], returns: FFIType.void },
46
+ cvt_cell: { args: [FFIType.ptr, FFIType.u16, FFIType.u32, FFIType.ptr], returns: FFIType.i32 },
47
+ cvt_cursor: { args: [FFIType.ptr, FFIType.ptr], returns: FFIType.void },
48
+ } as const;
49
+
50
+ type CvtLib = ReturnType<typeof dlopen<typeof CVT_SYMBOLS>>["symbols"];
51
+
52
+ /** The prebuilt shim dylib for this platform, or null when none ships for it. */
53
+ function nativeLibraryPath(): string | null {
54
+ const suffix = process.platform === "darwin" ? "dylib" : "so";
55
+ const dir = `${process.platform}-${process.arch}`;
56
+ const path = join(import.meta.dir, "..", "..", "native", dir, `libcueloopvt.${suffix}`);
57
+
58
+ return existsSync(path) ? path : null;
59
+ }
60
+
61
+ /** One shared load of the VT library; null when no dylib ships or it failed to load. */
62
+ let ghosttyFactory: GhosttyTerminalFactory | null | undefined;
63
+
64
+ /**
65
+ * Open the Ghostty VT library for this platform, or return null when the prebuilt
66
+ * dylib is missing (the caller degrades to a herdr split instead of embedding).
67
+ * The library is opened once per process; later calls return the same factory.
68
+ */
69
+ export function loadGhosttyTerminals(): GhosttyTerminalFactory | null {
70
+ if (ghosttyFactory !== undefined) return ghosttyFactory;
71
+ const path = nativeLibraryPath();
72
+
73
+ if (!path) return (ghosttyFactory = null);
74
+ try {
75
+ const lib = dlopen(path, CVT_SYMBOLS);
76
+
77
+ ghosttyFactory = new GhosttyTerminalFactory(lib.symbols);
78
+ } catch (error) {
79
+ // The file exists but failed to load (bad arch, missing symbol) - a real
80
+ // fault, not the expected no-prebuilt case; surface it, then degrade.
81
+ console.error(`cueloop: failed to load ${path}:`, error);
82
+ ghosttyFactory = null;
83
+ }
84
+
85
+ return ghosttyFactory;
86
+ }
87
+
88
+ /** Creates `GhosttyTerminal`s that share one loaded copy of the VT library. */
89
+ export class GhosttyTerminalFactory {
90
+ constructor(private readonly lib: CvtLib) {}
91
+ /** A fresh terminal sized to cols x rows, or null if allocation failed. */
92
+ create(cols: number, rows: number): GhosttyTerminal | null {
93
+ const handle = this.lib.cvt_new(cols, rows);
94
+
95
+ return handle ? new GhosttyTerminal(this.lib, handle) : null;
96
+ }
97
+ }
98
+
99
+ // The flat CvtCell struct the shim writes: u32 codepoint, fg rgba-ish (4 bytes),
100
+ // bg (4 bytes), u8 width, u8 flags, u16 pad = 16 bytes.
101
+ const CELL_STRUCT_BYTES = 16;
102
+ const FLAG_BOLD = 1;
103
+ const FLAG_ITALIC = 2;
104
+ const FLAG_UNDERLINE = 4;
105
+ const FLAG_INVERSE = 8;
106
+ const FLAG_FAINT = 16;
107
+ const FLAG_STRIKETHROUGH = 32;
108
+
109
+ /** A live Ghostty terminal: feed PTY bytes, read the screen, resize, free. */
110
+ export class GhosttyTerminal {
111
+ private readonly out = new Uint8Array(CELL_STRUCT_BYTES);
112
+ private readonly outView = new DataView(this.out.buffer);
113
+ private readonly cursorOut = new Uint8Array(8);
114
+ private freed = false;
115
+
116
+ constructor(
117
+ private readonly lib: CvtLib,
118
+ private readonly handle: NonNullable<ReturnType<CvtLib["cvt_new"]>>,
119
+ ) {}
120
+
121
+ /** Process a chunk of the child's PTY output into the screen state. */
122
+ write(bytes: Uint8Array): void {
123
+ if (this.freed || bytes.length === 0) return;
124
+ this.lib.cvt_write(this.handle, ptr(bytes), BigInt(bytes.length));
125
+ }
126
+
127
+ /** Resize the screen (and the child's tty, via the caller) to cols x rows. */
128
+ resize(cols: number, rows: number): void {
129
+ if (this.freed) return;
130
+ this.lib.cvt_resize(this.handle, cols, rows);
131
+ }
132
+
133
+ /** The rendered cell at viewport (x, y), or null when out of range. */
134
+ readCell(x: number, y: number): GhosttyCell | null {
135
+ if (this.freed) return null;
136
+ if (this.lib.cvt_cell(this.handle, x, y, ptr(this.out)) !== 0) return null;
137
+ const flags = this.out[13]!;
138
+
139
+ return {
140
+ codepoint: this.outView.getUint32(0, true),
141
+ fg: decodeColor(this.out[7]!, this.out[4]!, this.out[5]!, this.out[6]!),
142
+ bg: decodeColor(this.out[11]!, this.out[8]!, this.out[9]!, this.out[10]!),
143
+ width: this.out[12]!,
144
+ bold: (flags & FLAG_BOLD) !== 0,
145
+ italic: (flags & FLAG_ITALIC) !== 0,
146
+ underline: (flags & FLAG_UNDERLINE) !== 0,
147
+ inverse: (flags & FLAG_INVERSE) !== 0,
148
+ faint: (flags & FLAG_FAINT) !== 0,
149
+ strikethrough: (flags & FLAG_STRIKETHROUGH) !== 0,
150
+ };
151
+ }
152
+
153
+ /**
154
+ * Row `y` as text: blank cells read as spaces, the trailing half of a wide
155
+ * glyph is skipped, and trailing spaces are trimmed.
156
+ */
157
+ rowText(y: number, cols: number): string {
158
+ let line = "";
159
+
160
+ for (let x = 0; x < cols; x++) {
161
+ const cell = this.readCell(x, y);
162
+
163
+ if (cell === null || cell.width === 2) continue;
164
+ line += cell.codepoint === 0 ? " " : String.fromCodePoint(cell.codepoint);
165
+ }
166
+
167
+ return line.trimEnd();
168
+ }
169
+
170
+ /** The cursor's current viewport position and visibility. */
171
+ readCursor(): GhosttyCursor {
172
+ if (this.freed) return { x: 0, y: 0, visible: false };
173
+ this.lib.cvt_cursor(this.handle, ptr(this.cursorOut));
174
+ const view = new DataView(this.cursorOut.buffer);
175
+
176
+ return {
177
+ x: view.getUint16(0, true),
178
+ y: view.getUint16(2, true),
179
+ visible: this.cursorOut[4] === 1,
180
+ };
181
+ }
182
+
183
+ /** Release the terminal; further calls are no-ops. Call once on teardown. */
184
+ free(): void {
185
+ if (this.freed) return;
186
+ this.freed = true;
187
+ this.lib.cvt_free(this.handle);
188
+ }
189
+ }
190
+
191
+ /** Decode the shim's (kind, r, g, b) color triple; palette carries its index in r. */
192
+ function decodeColor(kind: number, r: number, g: number, b: number): GhosttyColor {
193
+ if (kind === 2) return { kind: "rgb", r, g, b };
194
+ if (kind === 1) return { kind: "palette", index: r };
195
+
196
+ return { kind: "default" };
197
+ }