@cueloop/client 0.1.0-alpha.9 → 0.1.0-alpha.90

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 (316) 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/App.appearance.test.tsx +53 -0
  9. package/src/App.autoclose.test.tsx +77 -51
  10. package/src/App.diff.test.tsx +259 -45
  11. package/src/App.export.test.tsx +58 -41
  12. package/src/App.focus.test.ts +89 -0
  13. package/src/App.identity.test.tsx +78 -0
  14. package/src/App.plan-render.test.tsx +209 -0
  15. package/src/App.plan-review.test.tsx +330 -161
  16. package/src/App.prototype-markdown.test.tsx +128 -0
  17. package/src/App.prototype.test.tsx +176 -0
  18. package/src/App.readonly.test.tsx +129 -67
  19. package/src/App.regression.test.tsx +94 -0
  20. package/src/App.rendering.test.tsx +61 -35
  21. package/src/App.share-dialog.test.tsx +172 -0
  22. package/src/App.submit-confirm.test.tsx +264 -0
  23. package/src/App.test.tsx +402 -126
  24. package/src/App.toast.test.tsx +98 -0
  25. package/src/App.tsx +1402 -1060
  26. package/src/App.visual.test.tsx +57 -0
  27. package/src/App.walk.test.tsx +299 -0
  28. package/src/App.workbench.test.tsx +774 -0
  29. package/src/__snapshots__/view-plan.regression.test.ts.snap +100 -0
  30. package/src/annotation-palette.ts +62 -0
  31. package/src/ansi-screen.test.ts +28 -0
  32. package/src/ansi-screen.ts +72 -0
  33. package/src/app-key-state.ts +61 -0
  34. package/src/app-screens.tsx +577 -0
  35. package/src/app-view-model.ts +168 -0
  36. package/src/attribution.test.ts +31 -0
  37. package/src/attribution.ts +42 -0
  38. package/src/client-extension-registry.test.tsx +61 -0
  39. package/src/client-extension-registry.ts +159 -0
  40. package/src/clipboard.test.ts +23 -0
  41. package/src/clipboard.ts +33 -0
  42. package/src/components/AnnotatableFileView.stories.tsx +42 -0
  43. package/src/components/AnnotatableFileView.test.tsx +102 -0
  44. package/src/components/AnnotatableFileView.tsx +134 -0
  45. package/src/components/AnnotationCards.stories.tsx +86 -0
  46. package/src/components/AnnotationCards.tsx +341 -0
  47. package/src/components/AnsiScreen.stories.tsx +12 -0
  48. package/src/components/AnsiScreen.tsx +28 -0
  49. package/src/components/AppHeader.stories.tsx +50 -0
  50. package/src/components/AppHeader.tsx +83 -0
  51. package/src/components/AppShell.brand.test.tsx +58 -0
  52. package/src/components/AppShell.extensions.test.tsx +191 -0
  53. package/src/components/AppShell.stories.tsx +89 -0
  54. package/src/components/AppShell.tsx +362 -0
  55. package/src/components/AppShell.zoom.test.tsx +135 -0
  56. package/src/components/BareWorkbenchFileView.stories.tsx +27 -0
  57. package/src/components/BareWorkbenchFileView.tsx +64 -0
  58. package/src/components/Breadcrumb.stories.tsx +19 -0
  59. package/src/components/Breadcrumb.tsx +45 -0
  60. package/src/components/ChangesColumn.stories.tsx +44 -0
  61. package/src/components/ChangesColumn.tsx +199 -0
  62. package/src/components/ChangesFileTree.test.tsx +61 -0
  63. package/src/components/CodeBlock.stories.tsx +42 -0
  64. package/src/components/CodeBlock.tsx +84 -0
  65. package/src/components/CompletionOverlay.stories.tsx +37 -0
  66. package/src/components/CompletionOverlay.tsx +77 -0
  67. package/src/components/ConfirmCard.stories.tsx +37 -0
  68. package/src/components/ConfirmCard.tsx +150 -0
  69. package/src/components/ConfirmDialog.stories.tsx +21 -0
  70. package/src/components/ConfirmDialog.tsx +72 -0
  71. package/src/components/DiffContentView.scroll.test.tsx +149 -0
  72. package/src/components/DiffContentView.split.test.tsx +140 -0
  73. package/src/components/DiffContentView.stories.tsx +80 -0
  74. package/src/components/DiffContentView.syntax.test.tsx +82 -0
  75. package/src/components/DiffContentView.tsx +906 -0
  76. package/src/components/DiscussionMarkerRail.stories.tsx +54 -0
  77. package/src/components/DiscussionMarkerRail.tsx +186 -0
  78. package/src/components/EditorGrid.stories.tsx +59 -0
  79. package/src/components/EditorGrid.tabs.test.tsx +118 -0
  80. package/src/components/EditorGrid.tsx +426 -0
  81. package/src/components/EditorGrid.zoom.test.tsx +68 -0
  82. package/src/components/ExtensionZone.stories.tsx +28 -0
  83. package/src/components/ExtensionZone.tsx +145 -0
  84. package/src/components/FileContentsView.stories.tsx +16 -0
  85. package/src/components/FileContentsView.test.tsx +29 -0
  86. package/src/components/FileContentsView.tsx +98 -0
  87. package/src/components/GridTabContent.stories.tsx +59 -0
  88. package/src/components/GridTabContent.tsx +182 -0
  89. package/src/components/HandleChipsInput.stories.tsx +24 -0
  90. package/src/components/HandleChipsInput.test.tsx +35 -0
  91. package/src/components/HandleChipsInput.tsx +145 -0
  92. package/src/components/KeybindsDialog.stories.tsx +31 -0
  93. package/src/components/KeybindsDialog.tsx +103 -0
  94. package/src/components/MarkdownGridBlock.stories.tsx +33 -0
  95. package/src/components/MarkdownGridBlock.tsx +72 -0
  96. package/src/components/MarkdownThreadEditor.stories.tsx +15 -0
  97. package/src/components/MarkdownThreadEditor.test.tsx +98 -0
  98. package/src/components/MarkdownThreadEditor.tsx +162 -0
  99. package/src/components/MenuBar.stories.tsx +28 -0
  100. package/src/components/MenuBar.tsx +44 -0
  101. package/src/components/NavModeHint.stories.tsx +40 -0
  102. package/src/components/NavModeHint.tsx +31 -0
  103. package/src/components/OverlayScrollbar.stories.tsx +33 -0
  104. package/src/components/OverlayScrollbar.test.tsx +139 -0
  105. package/src/components/OverlayScrollbar.tsx +129 -0
  106. package/src/components/PaneGrid.stories.tsx +84 -0
  107. package/src/components/PaneGrid.tsx +128 -0
  108. package/src/components/PanelColumn.stories.tsx +25 -0
  109. package/src/components/PanelColumn.tsx +98 -0
  110. package/src/components/ProjectTreeView.stories.tsx +25 -0
  111. package/src/components/ProjectTreeView.test.tsx +75 -0
  112. package/src/components/ProjectTreeView.tsx +125 -0
  113. package/src/components/PromptDialog.stories.tsx +36 -0
  114. package/src/components/PromptDialog.test.tsx +61 -0
  115. package/src/components/PromptDialog.tsx +99 -0
  116. package/src/components/PrototypeContentView.stories.tsx +19 -0
  117. package/src/components/PrototypeContentView.tsx +446 -0
  118. package/src/components/RootOverlay.stories.tsx +45 -0
  119. package/src/components/RootOverlay.tsx +48 -0
  120. package/src/components/ScrollArea.stories.tsx +20 -0
  121. package/src/components/ScrollArea.tsx +44 -0
  122. package/src/components/SettingsDialog.stories.tsx +117 -0
  123. package/src/components/SettingsDialog.tsx +186 -0
  124. package/src/components/SettingsRows.stories.tsx +18 -0
  125. package/src/components/SettingsRows.tsx +112 -0
  126. package/src/components/ShareDialog.stories.tsx +121 -0
  127. package/src/components/ShareDialog.tsx +552 -0
  128. package/src/components/ShellHeader.stories.tsx +50 -0
  129. package/src/components/ShellHeader.tsx +137 -0
  130. package/src/components/SlashComposer.stories.tsx +37 -0
  131. package/src/components/SlashComposer.tsx +87 -0
  132. package/src/components/ThreadFooter.stories.tsx +30 -0
  133. package/src/components/ThreadFooter.test.tsx +47 -0
  134. package/src/components/ThreadFooter.tsx +76 -0
  135. package/src/components/ThreadTree.stories.tsx +46 -0
  136. package/src/components/ThreadTree.tsx +255 -0
  137. package/src/components/ThreadView.selection.test.tsx +951 -0
  138. package/src/components/ThreadView.slash.test.tsx +146 -0
  139. package/src/components/ThreadView.stories.tsx +26 -0
  140. package/src/components/ThreadView.tsx +525 -0
  141. package/src/components/Toast.stories.tsx +19 -0
  142. package/src/components/Toast.tsx +52 -0
  143. package/src/components/Tooltip.stories.tsx +26 -0
  144. package/src/components/Tooltip.tsx +72 -0
  145. package/src/components/TreePane.stories.tsx +106 -0
  146. package/src/components/TreePane.tsx +157 -0
  147. package/src/components/WalkWizard.stories.tsx +100 -0
  148. package/src/components/WalkWizard.tsx +150 -0
  149. package/src/components/WelcomePlayground.stories.tsx +12 -0
  150. package/src/components/WelcomePlayground.test.tsx +49 -0
  151. package/src/components/WelcomePlayground.tsx +139 -0
  152. package/src/components/WorkspacePanels.stories.tsx +36 -0
  153. package/src/components/WorkspacePanels.tsx +116 -0
  154. package/src/components/__snapshots__/stories.test.tsx.snap +2264 -0
  155. package/src/components/diff-content-view-layout.test.ts +198 -0
  156. package/src/components/diff-content-view-layout.ts +84 -0
  157. package/src/components/editor-grid.test.ts +86 -0
  158. package/src/components/editor-grid.ts +138 -0
  159. package/src/components/file-tree.test.ts +60 -0
  160. package/src/components/file-tree.ts +112 -0
  161. package/src/components/menu-control.stories.tsx +35 -0
  162. package/src/components/menu-control.tsx +43 -0
  163. package/src/components/pages/Workspace.stories.tsx +181 -0
  164. package/src/components/primitives/Button.stories.tsx +32 -0
  165. package/src/components/primitives/Button.tsx +59 -0
  166. package/src/components/primitives/Card.stories.tsx +25 -0
  167. package/src/components/primitives/Card.tsx +63 -0
  168. package/src/components/primitives/Dialog.stories.tsx +24 -0
  169. package/src/components/primitives/Dialog.test.tsx +37 -0
  170. package/src/components/primitives/Dialog.tsx +71 -0
  171. package/src/components/primitives/DialogActions.stories.tsx +20 -0
  172. package/src/components/primitives/DialogActions.tsx +39 -0
  173. package/src/components/primitives/IconButton.stories.tsx +36 -0
  174. package/src/components/primitives/IconButton.tsx +62 -0
  175. package/src/components/primitives/StatusBar.stories.tsx +11 -0
  176. package/src/components/primitives/StatusBar.tsx +23 -0
  177. package/src/components/primitives/Switch.stories.tsx +17 -0
  178. package/src/components/primitives/Switch.tsx +32 -0
  179. package/src/components/primitives/Tabs.stories.tsx +29 -0
  180. package/src/components/primitives/Tabs.tsx +102 -0
  181. package/src/components/primitives/Toolbar.stories.tsx +17 -0
  182. package/src/components/primitives/Toolbar.tsx +14 -0
  183. package/src/components/primitives/Tree.stories.tsx +124 -0
  184. package/src/components/primitives/Tree.tsx +100 -0
  185. package/src/components/primitives/frame.test.ts +12 -0
  186. package/src/components/primitives/frame.ts +17 -0
  187. package/src/components/primitives/icons.ts +61 -0
  188. package/src/components/primitives/tree-model.test.ts +99 -0
  189. package/src/components/primitives/tree-model.ts +137 -0
  190. package/src/components/quick-actions-editor.stories.tsx +58 -0
  191. package/src/components/quick-actions-editor.test.tsx +117 -0
  192. package/src/components/quick-actions-editor.tsx +226 -0
  193. package/src/components/share-dialog-store.ts +58 -0
  194. package/src/components/stories-app.tsx +193 -0
  195. package/src/components/stories.test.tsx +74 -0
  196. package/src/components/story-fixtures.ts +102 -0
  197. package/src/components/story-tree.test.ts +108 -0
  198. package/src/components/story.ts +159 -0
  199. package/src/components/syntax-highlight.ts +133 -0
  200. package/src/components/terminal-pane.test.tsx +40 -0
  201. package/src/components/terminal-pane.ts +199 -0
  202. package/src/components/theme-context.stories.tsx +46 -0
  203. package/src/components/theme-context.tsx +37 -0
  204. package/src/components/thread-tree.test.ts +134 -0
  205. package/src/components/thread-tree.ts +109 -0
  206. package/src/components/truncate-text.ts +7 -0
  207. package/src/components/truncate-title.test.ts +31 -0
  208. package/src/components/truncate-title.ts +15 -0
  209. package/src/config-identity.test.ts +78 -0
  210. package/src/config.test.ts +579 -34
  211. package/src/config.ts +533 -57
  212. package/src/diff-fold.test.ts +78 -0
  213. package/src/diff-fold.ts +126 -0
  214. package/src/diff-hunk-curate.test.ts +315 -0
  215. package/src/diff-hunk-curate.ts +153 -0
  216. package/src/diff-intraline.test.ts +238 -0
  217. package/src/diff-intraline.ts +236 -0
  218. package/src/diff-syntax.test.ts +112 -0
  219. package/src/diff-syntax.ts +179 -0
  220. package/src/discussions.ts +79 -0
  221. package/src/editor.test.ts +149 -0
  222. package/src/editor.ts +139 -9
  223. package/src/ghostty-terminal.test.ts +85 -0
  224. package/src/ghostty-terminal.ts +199 -0
  225. package/src/github-identity.test.ts +58 -0
  226. package/src/github-identity.ts +41 -0
  227. package/src/herdr.test.ts +9 -34
  228. package/src/herdr.ts +25 -21
  229. package/src/index.ts +28 -0
  230. package/src/integrations.test.ts +41 -0
  231. package/src/integrations.ts +32 -0
  232. package/src/intent-dispatch.test.ts +724 -0
  233. package/src/intent-dispatch.ts +690 -0
  234. package/src/key-bindings.test.ts +156 -0
  235. package/src/key-bindings.ts +459 -0
  236. package/src/keymap.test.ts +456 -115
  237. package/src/keymap.ts +315 -81
  238. package/src/kitty-image.test.ts +66 -0
  239. package/src/kitty-image.ts +138 -0
  240. package/src/launch-layout.test.ts +67 -0
  241. package/src/launch-layout.ts +47 -0
  242. package/src/mark-runs.ts +121 -0
  243. package/src/markdown-editor-syntax-style.ts +50 -0
  244. package/src/markdown-grid.test.ts +101 -0
  245. package/src/markdown-grid.ts +203 -0
  246. package/src/markdown-highlight.test.ts +61 -0
  247. package/src/markdown-highlight.ts +122 -0
  248. package/src/pasted-image.test.ts +67 -0
  249. package/src/pasted-image.ts +60 -0
  250. package/src/perf/otlp-trace.test.ts +116 -0
  251. package/src/perf/otlp-trace.ts +130 -0
  252. package/src/perf/perf-report.ts +17 -0
  253. package/src/perf/perf-timings.test.ts +18 -0
  254. package/src/perf/perf-timings.ts +38 -0
  255. package/src/prototype-browser.test.ts +81 -0
  256. package/src/prototype-browser.ts +329 -0
  257. package/src/prototype-pixels.tsx +35 -0
  258. package/src/pty.test.ts +81 -0
  259. package/src/pty.ts +245 -0
  260. package/src/ready-signal.test.ts +85 -0
  261. package/src/ready-signal.ts +52 -0
  262. package/src/run.ts +85 -12
  263. package/src/serve.test.ts +24 -12
  264. package/src/serve.ts +52 -10
  265. package/src/share.ts +237 -0
  266. package/src/skills.test.ts +74 -0
  267. package/src/skills.ts +75 -0
  268. package/src/slash-palette.test.ts +103 -0
  269. package/src/slash-palette.ts +129 -0
  270. package/src/split-diff.test.ts +110 -0
  271. package/src/split-diff.ts +149 -0
  272. package/src/test-support.test.ts +264 -0
  273. package/src/test-support.ts +369 -0
  274. package/src/theme-presets.test.ts +51 -0
  275. package/src/theme-presets.ts +219 -0
  276. package/src/theme.ts +88 -19
  277. package/src/thread-chords.test.ts +79 -0
  278. package/src/thread-chords.ts +163 -0
  279. package/src/thread-code-syntax.ts +45 -0
  280. package/src/thread-controller.curate.test.ts +476 -0
  281. package/src/thread-controller.derivation.test.ts +62 -0
  282. package/src/thread-controller.live-diff.test.ts +322 -0
  283. package/src/thread-controller.optimistic.test.ts +69 -0
  284. package/src/thread-controller.share.test.ts +561 -0
  285. package/src/thread-controller.switch-cache.test.ts +176 -0
  286. package/src/thread-controller.tree.test.ts +261 -0
  287. package/src/thread-controller.ts +2050 -0
  288. package/src/thread-selection.test.ts +210 -0
  289. package/src/thread-selection.ts +243 -0
  290. package/src/tree-view.test.ts +128 -0
  291. package/src/tree-view.ts +153 -0
  292. package/src/use-annotation-surface.tsx +975 -0
  293. package/src/use-changes-workbench.ts +190 -0
  294. package/src/use-frame-measure.ts +41 -0
  295. package/src/use-remember-layout.ts +31 -0
  296. package/src/use-repo-changes.ts +87 -0
  297. package/src/use-settings-dialog.tsx +364 -0
  298. package/src/use-terminal-virtualizer.ts +259 -0
  299. package/src/use-thread-body-editing.tsx +58 -0
  300. package/src/version.ts +5 -0
  301. package/src/view-diff.test.ts +279 -9
  302. package/src/view-diff.ts +460 -24
  303. package/src/view-plan.regression.test.ts +60 -0
  304. package/src/view-plan.test.ts +545 -0
  305. package/src/view-plan.ts +1019 -0
  306. package/src/visual/contrast.test.ts +35 -0
  307. package/src/visual/contrast.ts +33 -0
  308. package/src/walk.test.ts +121 -0
  309. package/src/walk.ts +74 -0
  310. package/src/workbench-snapshot.test.ts +99 -0
  311. package/src/workbench-snapshot.ts +59 -0
  312. package/src/session-controller.ts +0 -398
  313. package/src/syntax.test.ts +0 -41
  314. package/src/syntax.ts +0 -88
  315. package/src/view.test.ts +0 -157
  316. package/src/view.ts +0 -364
@@ -0,0 +1,75 @@
1
+ /** The Project tree navigates by keyboard when focused: j/k move the cursor, tab folds a folder or
2
+ * 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 { ProjectTreeView } from "./ProjectTreeView";
8
+ import { locateText, settle, waitForText } from "../test-support";
9
+ import { DARK } from "../theme";
10
+
11
+ test("tab expands a folder, then j moves down and tab opens the file", async () => {
12
+ const opened: string[] = [];
13
+ const setup = await testRender(
14
+ <ProjectTreeView
15
+ loadFiles={async () => ["src/a.ts", "README.md"]}
16
+ onSelectFile={(path) => opened.push(path)}
17
+ focused
18
+ theme={DARK}
19
+ />,
20
+ { width: 40, height: 12 },
21
+ );
22
+ await settle(setup);
23
+ await waitForText(setup, "src");
24
+
25
+ // cursor starts on the "src" folder; tab expands it
26
+ setup.mockInput.pressKey("TAB");
27
+ await waitForText(setup, "a.ts");
28
+
29
+ // j moves onto the file, tab opens it
30
+ setup.mockInput.pressKey("j");
31
+ await settle(setup);
32
+ setup.mockInput.pressKey("TAB");
33
+ await settle(setup);
34
+
35
+ expect(opened).toEqual(["src/a.ts"]);
36
+
37
+ setup.renderer.destroy();
38
+ });
39
+
40
+ test("without focus the keyboard does nothing", async () => {
41
+ const opened: string[] = [];
42
+ const setup = await testRender(
43
+ <ProjectTreeView
44
+ loadFiles={async () => ["src/a.ts"]}
45
+ onSelectFile={(path) => opened.push(path)}
46
+ theme={DARK}
47
+ />,
48
+ { width: 40, height: 12 },
49
+ );
50
+ await settle(setup);
51
+ await waitForText(setup, "src");
52
+
53
+ setup.mockInput.pressKey("TAB");
54
+ await settle(setup);
55
+ expect(setup.captureCharFrame()).not.toContain("a.ts");
56
+ expect(opened).toEqual([]);
57
+
58
+ setup.renderer.destroy();
59
+ });
60
+
61
+ test("the empty project hint stays centered in the panel", async () => {
62
+ const setup = await testRender(
63
+ <ProjectTreeView loadFiles={async () => []} onSelectFile={() => {}} theme={DARK} />,
64
+ { width: 40, height: 12 },
65
+ );
66
+
67
+ await waitForText(setup, "empty");
68
+ const position = locateText(setup, "empty");
69
+
70
+ expect(position.column).toBeGreaterThanOrEqual(17);
71
+ expect(position.column).toBeLessThanOrEqual(18);
72
+ expect(position.row).toBeGreaterThanOrEqual(5);
73
+ expect(position.row).toBeLessThanOrEqual(6);
74
+ setup.renderer.destroy();
75
+ });
@@ -0,0 +1,125 @@
1
+ // The Project pane in tree mode: the workspace's tracked files as a collapsible directory tree.
2
+ // Folders start collapsed and expand on click; a file click opens it as a tab. The caller keys this
3
+ // on the session id so switching sessions remounts it and never shows the previous repository's paths.
4
+
5
+ import { ScrollArea } from "./ScrollArea";
6
+ import React, { useEffect, useMemo, useRef, useState } from "react";
7
+ import { useKeyboard } from "@opentui/react";
8
+ import type { Theme } from "../theme";
9
+ import { useComponentTheme } from "./theme-context";
10
+ import { Tree } from "./primitives/Tree";
11
+ import { flattenTree } from "./primitives/tree-model";
12
+ import { buildPathTree } from "./file-tree";
13
+
14
+ export interface ProjectTreeViewProps {
15
+ loadFiles: () => Promise<string[]>;
16
+ onSelectFile: (path: string) => void;
17
+ /** The pane owns the keyboard: j/k and arrows move the cursor, tab/enter open or fold. */
18
+ focused?: boolean;
19
+ theme?: Theme;
20
+ }
21
+
22
+ export function ProjectTreeView({
23
+ loadFiles,
24
+ onSelectFile,
25
+ focused = false,
26
+ theme,
27
+ }: ProjectTreeViewProps): React.ReactNode {
28
+ const tokens = useComponentTheme(theme);
29
+ const [paths, setPaths] = useState<readonly string[] | null>(null);
30
+ const [expandedIds, setExpandedIds] = useState<ReadonlySet<string>>(new Set());
31
+ const [cursor, setCursor] = useState(0);
32
+ const nodes = useMemo(() => (paths ? buildPathTree(paths) : []), [paths]);
33
+ const rows = useMemo(() => flattenTree(nodes, { expandedIds }), [nodes, expandedIds]);
34
+ const cursorIndex = Math.min(cursor, Math.max(0, rows.length - 1));
35
+ const loadRef = useRef(loadFiles);
36
+ useEffect(() => {
37
+ loadRef.current = loadFiles;
38
+ });
39
+
40
+ const toggle = (id: string): void =>
41
+ setExpandedIds((current) => {
42
+ const next = new Set(current);
43
+
44
+ if (next.has(id)) next.delete(id);
45
+ else next.add(id);
46
+
47
+ return next;
48
+ });
49
+
50
+ useKeyboard((key) => {
51
+ if (!focused || rows.length === 0) return;
52
+ if (key.name === "j" || key.name === "down")
53
+ return setCursor(Math.min(cursorIndex + 1, rows.length - 1));
54
+ if (key.name === "k" || key.name === "up") return setCursor(Math.max(cursorIndex - 1, 0));
55
+ const row = rows[cursorIndex];
56
+ if (!row) return;
57
+ if (key.name === "tab" || key.name === "return" || key.name === "enter")
58
+ return row.isFolder ? toggle(row.id) : onSelectFile(row.id);
59
+ if (key.name === "l" && row.isFolder && !expandedIds.has(row.id)) return toggle(row.id);
60
+ if (key.name === "h" && row.isFolder && expandedIds.has(row.id)) return toggle(row.id);
61
+ });
62
+
63
+ useEffect(() => {
64
+ let alive = true;
65
+ void loadRef.current().then(
66
+ (files) => {
67
+ if (alive) setPaths(files);
68
+ },
69
+ () => {
70
+ if (alive) setPaths([]);
71
+ },
72
+ );
73
+
74
+ return () => {
75
+ alive = false;
76
+ };
77
+ }, []);
78
+
79
+ if (paths === null) {
80
+ return (
81
+ <box
82
+ style={{
83
+ flexGrow: 1,
84
+ flexDirection: "column",
85
+ alignItems: "center",
86
+ justifyContent: "center",
87
+ }}
88
+ >
89
+ <text fg={tokens.textDim}>loading...</text>
90
+ </box>
91
+ );
92
+ }
93
+ if (paths.length === 0) {
94
+ return (
95
+ <box
96
+ style={{
97
+ flexGrow: 1,
98
+ flexDirection: "column",
99
+ alignItems: "center",
100
+ justifyContent: "center",
101
+ }}
102
+ >
103
+ <text fg={tokens.textDim}>empty</text>
104
+ </box>
105
+ );
106
+ }
107
+
108
+ return (
109
+ <ScrollArea>
110
+ <Tree
111
+ nodes={nodes}
112
+ expandedIds={expandedIds}
113
+ selectedId={focused ? rows[cursorIndex]?.id : undefined}
114
+ onSelect={(id) => {
115
+ const index = rows.findIndex((row) => row.id === id);
116
+
117
+ if (index >= 0) setCursor(index);
118
+ onSelectFile(id);
119
+ }}
120
+ onToggle={toggle}
121
+ theme={theme}
122
+ />
123
+ </ScrollArea>
124
+ );
125
+ }
@@ -0,0 +1,36 @@
1
+ import React from "react";
2
+ import { DARK } from "../theme";
3
+ import type { Story, StoryMeta } from "./story";
4
+ import { PromptDialog } from "./PromptDialog";
5
+
6
+ export const meta: StoryMeta = { title: "Overlays/PromptDialog" };
7
+
8
+ export const RenameAuthor: Story = {
9
+ render: () => (
10
+ <PromptDialog
11
+ isOpen
12
+ title=" Rename author "
13
+ label="Display name for this collaborator:"
14
+ value="Robin"
15
+ placeholder="their name"
16
+ onInput={() => {}}
17
+ />
18
+ ),
19
+ expectedColors: [DARK.elevated, DARK.accent],
20
+ size: { width: 64, height: 16 },
21
+ };
22
+
23
+ export const NameYourself: Story = {
24
+ render: () => (
25
+ <PromptDialog
26
+ isOpen
27
+ title=" Welcome "
28
+ label="Your name (optional) - it attributes the notes you leave:"
29
+ value=""
30
+ placeholder="your name"
31
+ onInput={() => {}}
32
+ />
33
+ ),
34
+ expectedColors: [DARK.elevated],
35
+ size: { width: 64, height: 16 },
36
+ };
@@ -0,0 +1,61 @@
1
+ /** The prompt dialog's footer actions are mouse-clickable: clicking "save" commits and "cancel"
2
+ * dismisses, the same as pressing enter or esc. */
3
+
4
+ import { test, expect } from "bun:test";
5
+ import React from "react";
6
+ import { testRender } from "@opentui/react/test-utils";
7
+ import { PromptDialog } from "./PromptDialog";
8
+ import { locateText, settle, typeText } from "../test-support";
9
+ import { DARK } from "../theme";
10
+
11
+ test("the input is focused on open, so typing lands in the dialog", async () => {
12
+ const inputs: string[] = [];
13
+ const setup = await testRender(
14
+ <PromptDialog
15
+ isOpen
16
+ title=" rename thread "
17
+ label="new title for this thread:"
18
+ value="hi"
19
+ onInput={(text) => inputs.push(text)}
20
+ theme={DARK}
21
+ />,
22
+ { width: 60, height: 12 },
23
+ );
24
+ await settle(setup);
25
+
26
+ await typeText(setup, "!");
27
+ expect(inputs.at(-1)).toBe("hi!");
28
+
29
+ setup.renderer.destroy();
30
+ });
31
+
32
+ test("clicking save fires onSave and clicking cancel fires onCancel", async () => {
33
+ let saved = 0;
34
+ let cancelled = 0;
35
+ const setup = await testRender(
36
+ <PromptDialog
37
+ isOpen
38
+ title=" rename thread "
39
+ label="new title for this thread:"
40
+ value="a title"
41
+ onInput={() => {}}
42
+ onSave={() => (saved += 1)}
43
+ onCancel={() => (cancelled += 1)}
44
+ theme={DARK}
45
+ />,
46
+ { width: 60, height: 12 },
47
+ );
48
+ await settle(setup);
49
+
50
+ const save = locateText(setup, "save");
51
+ await setup.mockMouse.click(save.column, save.row);
52
+ expect(saved).toBe(1);
53
+ expect(cancelled).toBe(0);
54
+
55
+ const cancel = locateText(setup, "cancel");
56
+ await setup.mockMouse.click(cancel.column, cancel.row);
57
+ expect(cancelled).toBe(1);
58
+ expect(saved).toBe(1);
59
+
60
+ setup.renderer.destroy();
61
+ });
@@ -0,0 +1,99 @@
1
+ /**
2
+ * A centered single-field text prompt on the shared Dialog overlay. The focused
3
+ * input owns typing/cursor natively; the reducer's `prompt` overlay drives ⏎
4
+ * save / esc cancel (like the composer). Used to rename an author and to ask a
5
+ * collaborator for their name.
6
+ */
7
+
8
+ import React, { useEffect, useRef } from "react";
9
+ import { useTerminalDimensions } from "@opentui/react";
10
+ import type { KeyBinding, TextareaRenderable } from "@opentui/core";
11
+ import type { Theme } from "../theme";
12
+ import { useComponentTheme } from "./theme-context";
13
+ import { Dialog } from "./primitives/Dialog";
14
+ import { DialogActions } from "./primitives/DialogActions";
15
+
16
+ // ⏎ submits (suppresses the textarea's default newline); the grammar owns save.
17
+ const PROMPT_KEY_BINDINGS: KeyBinding[] = [{ name: "return", action: "submit" }];
18
+
19
+ export interface PromptDialogProps {
20
+ isOpen: boolean;
21
+ title?: string;
22
+ label?: string;
23
+ value: string;
24
+ placeholder?: string;
25
+ onInput: (text: string) => void;
26
+ onSave?: () => void;
27
+ onCancel?: () => void;
28
+ theme?: Theme;
29
+ }
30
+
31
+ export function PromptDialog({
32
+ isOpen,
33
+ title,
34
+ label,
35
+ value,
36
+ placeholder,
37
+ onInput,
38
+ onSave,
39
+ onCancel,
40
+ theme,
41
+ }: PromptDialogProps): React.ReactNode {
42
+ const tokens = useComponentTheme(theme);
43
+ const { width: terminalWidth } = useTerminalDimensions();
44
+ const inputRef = useRef<TextareaRenderable | null>(null);
45
+
46
+ useEffect(() => {
47
+ if (!inputRef.current) return;
48
+ inputRef.current.focus();
49
+ inputRef.current.cursorOffset = value.length;
50
+ // eslint-disable-next-line react-hooks/exhaustive-deps
51
+ }, []);
52
+ if (!isOpen) return null;
53
+
54
+ return (
55
+ <Dialog
56
+ isOpen
57
+ title={title}
58
+ width={Math.min(54, terminalWidth - 6)}
59
+ height={7}
60
+ background={tokens.elevated}
61
+ onDismiss={onCancel}
62
+ theme={theme}
63
+ >
64
+ <box
65
+ style={{
66
+ flexDirection: "column",
67
+ flexGrow: 1,
68
+ paddingLeft: 1,
69
+ paddingRight: 1,
70
+ paddingTop: 1,
71
+ }}
72
+ >
73
+ {label ? <text fg={tokens.textDim}>{label}</text> : null}
74
+ <textarea
75
+ ref={inputRef}
76
+ focused
77
+ initialValue={value}
78
+ placeholder={placeholder}
79
+ keyBindings={PROMPT_KEY_BINDINGS}
80
+ onContentChange={() => onInput(inputRef.current?.plainText ?? "")}
81
+ style={{
82
+ height: 1,
83
+ backgroundColor: tokens.elevated,
84
+ focusedBackgroundColor: tokens.elevated,
85
+ textColor: tokens.text,
86
+ focusedTextColor: tokens.text,
87
+ }}
88
+ />
89
+ <box style={{ flexGrow: 1 }} />
90
+ <DialogActions
91
+ confirmLabel="save"
92
+ onConfirm={() => onSave?.()}
93
+ onCancel={() => onCancel?.()}
94
+ theme={theme}
95
+ />
96
+ </box>
97
+ </Dialog>
98
+ );
99
+ }
@@ -0,0 +1,19 @@
1
+ import React from "react";
2
+ import { DARK } from "../theme";
3
+ import type { Story, StoryMeta } from "./story";
4
+ import { PrototypeContentView } from "./PrototypeContentView";
5
+
6
+ export const meta: StoryMeta = { title: "Surfaces/PrototypeContentView" };
7
+
8
+ // The story renderer reports no kitty graphics, so the view shows its fallback.
9
+ export const Unsupported: Story = {
10
+ render: () => (
11
+ <PrototypeContentView
12
+ prototypePath="/tmp/example.html"
13
+ canComment={true}
14
+ onCommentElement={() => {}}
15
+ />
16
+ ),
17
+ expectedColors: [DARK.textDim],
18
+ size: { width: 80, height: 24 },
19
+ };