@cueloop/client 0.1.0-alpha.8 → 0.1.0-alpha.81

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 (307) 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 +40 -32
  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 +176 -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 +428 -0
  16. package/src/App.prototype-markdown.test.tsx +92 -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 +222 -0
  23. package/src/App.test.tsx +333 -131
  24. package/src/App.toast.test.tsx +98 -0
  25. package/src/App.tsx +1338 -608
  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 +701 -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 +574 -0
  35. package/src/app-view-model.ts +162 -0
  36. package/src/attribution.test.ts +31 -0
  37. package/src/attribution.ts +42 -0
  38. package/src/clipboard.test.ts +23 -0
  39. package/src/clipboard.ts +33 -0
  40. package/src/components/AnnotatableFileView.stories.tsx +42 -0
  41. package/src/components/AnnotatableFileView.test.tsx +102 -0
  42. package/src/components/AnnotatableFileView.tsx +134 -0
  43. package/src/components/AnnotationCards.stories.tsx +86 -0
  44. package/src/components/AnnotationCards.tsx +341 -0
  45. package/src/components/AnsiScreen.stories.tsx +12 -0
  46. package/src/components/AnsiScreen.tsx +28 -0
  47. package/src/components/AppHeader.stories.tsx +50 -0
  48. package/src/components/AppHeader.tsx +83 -0
  49. package/src/components/AppShell.brand.test.tsx +58 -0
  50. package/src/components/AppShell.stories.tsx +89 -0
  51. package/src/components/AppShell.tsx +329 -0
  52. package/src/components/AppShell.zoom.test.tsx +135 -0
  53. package/src/components/BareWorkbenchFileView.stories.tsx +27 -0
  54. package/src/components/BareWorkbenchFileView.tsx +64 -0
  55. package/src/components/Breadcrumb.stories.tsx +19 -0
  56. package/src/components/Breadcrumb.tsx +45 -0
  57. package/src/components/ChangesColumn.stories.tsx +44 -0
  58. package/src/components/ChangesColumn.tsx +199 -0
  59. package/src/components/ChangesFileTree.test.tsx +61 -0
  60. package/src/components/CodeBlock.stories.tsx +42 -0
  61. package/src/components/CodeBlock.tsx +84 -0
  62. package/src/components/CompletionOverlay.stories.tsx +37 -0
  63. package/src/components/CompletionOverlay.tsx +77 -0
  64. package/src/components/ConfirmCard.stories.tsx +37 -0
  65. package/src/components/ConfirmCard.tsx +153 -0
  66. package/src/components/ConfirmDialog.stories.tsx +21 -0
  67. package/src/components/ConfirmDialog.tsx +72 -0
  68. package/src/components/DiffContentView.scroll.test.tsx +121 -0
  69. package/src/components/DiffContentView.split.test.tsx +140 -0
  70. package/src/components/DiffContentView.stories.tsx +80 -0
  71. package/src/components/DiffContentView.syntax.test.tsx +82 -0
  72. package/src/components/DiffContentView.tsx +876 -0
  73. package/src/components/DiscussionMarkerRail.stories.tsx +54 -0
  74. package/src/components/DiscussionMarkerRail.tsx +186 -0
  75. package/src/components/EditorGrid.stories.tsx +59 -0
  76. package/src/components/EditorGrid.tabs.test.tsx +118 -0
  77. package/src/components/EditorGrid.tsx +426 -0
  78. package/src/components/EditorGrid.zoom.test.tsx +68 -0
  79. package/src/components/FileContentsView.stories.tsx +16 -0
  80. package/src/components/FileContentsView.test.tsx +29 -0
  81. package/src/components/FileContentsView.tsx +98 -0
  82. package/src/components/GridTabContent.stories.tsx +59 -0
  83. package/src/components/GridTabContent.tsx +182 -0
  84. package/src/components/HandleChipsInput.stories.tsx +24 -0
  85. package/src/components/HandleChipsInput.test.tsx +35 -0
  86. package/src/components/HandleChipsInput.tsx +145 -0
  87. package/src/components/KeybindsDialog.stories.tsx +31 -0
  88. package/src/components/KeybindsDialog.tsx +103 -0
  89. package/src/components/MarkdownGridBlock.stories.tsx +33 -0
  90. package/src/components/MarkdownGridBlock.tsx +72 -0
  91. package/src/components/MarkdownThreadEditor.stories.tsx +15 -0
  92. package/src/components/MarkdownThreadEditor.test.tsx +98 -0
  93. package/src/components/MarkdownThreadEditor.tsx +162 -0
  94. package/src/components/MenuBar.stories.tsx +28 -0
  95. package/src/components/MenuBar.tsx +44 -0
  96. package/src/components/NavModeHint.stories.tsx +40 -0
  97. package/src/components/NavModeHint.tsx +31 -0
  98. package/src/components/OverlayScrollbar.stories.tsx +33 -0
  99. package/src/components/OverlayScrollbar.test.tsx +139 -0
  100. package/src/components/OverlayScrollbar.tsx +129 -0
  101. package/src/components/PaneGrid.stories.tsx +84 -0
  102. package/src/components/PaneGrid.tsx +128 -0
  103. package/src/components/PanelColumn.stories.tsx +25 -0
  104. package/src/components/PanelColumn.tsx +98 -0
  105. package/src/components/ProjectTreeView.stories.tsx +25 -0
  106. package/src/components/ProjectTreeView.test.tsx +59 -0
  107. package/src/components/ProjectTreeView.tsx +111 -0
  108. package/src/components/PromptDialog.stories.tsx +36 -0
  109. package/src/components/PromptDialog.test.tsx +61 -0
  110. package/src/components/PromptDialog.tsx +99 -0
  111. package/src/components/PrototypeContentView.stories.tsx +19 -0
  112. package/src/components/PrototypeContentView.tsx +446 -0
  113. package/src/components/RootOverlay.stories.tsx +45 -0
  114. package/src/components/RootOverlay.tsx +48 -0
  115. package/src/components/ScrollArea.stories.tsx +20 -0
  116. package/src/components/ScrollArea.tsx +44 -0
  117. package/src/components/SettingsDialog.stories.tsx +117 -0
  118. package/src/components/SettingsDialog.tsx +186 -0
  119. package/src/components/SettingsRows.stories.tsx +18 -0
  120. package/src/components/SettingsRows.tsx +112 -0
  121. package/src/components/ShareDialog.stories.tsx +121 -0
  122. package/src/components/ShareDialog.tsx +552 -0
  123. package/src/components/ShellHeader.stories.tsx +50 -0
  124. package/src/components/ShellHeader.tsx +137 -0
  125. package/src/components/SlashComposer.stories.tsx +37 -0
  126. package/src/components/SlashComposer.tsx +87 -0
  127. package/src/components/ThreadFooter.stories.tsx +30 -0
  128. package/src/components/ThreadFooter.test.tsx +47 -0
  129. package/src/components/ThreadFooter.tsx +76 -0
  130. package/src/components/ThreadTree.stories.tsx +46 -0
  131. package/src/components/ThreadTree.tsx +255 -0
  132. package/src/components/ThreadView.selection.test.tsx +899 -0
  133. package/src/components/ThreadView.slash.test.tsx +146 -0
  134. package/src/components/ThreadView.stories.tsx +26 -0
  135. package/src/components/ThreadView.tsx +472 -0
  136. package/src/components/Toast.stories.tsx +19 -0
  137. package/src/components/Toast.tsx +52 -0
  138. package/src/components/Tooltip.stories.tsx +26 -0
  139. package/src/components/Tooltip.tsx +72 -0
  140. package/src/components/TreePane.stories.tsx +106 -0
  141. package/src/components/TreePane.tsx +157 -0
  142. package/src/components/WalkWizard.stories.tsx +100 -0
  143. package/src/components/WalkWizard.tsx +150 -0
  144. package/src/components/WelcomePlayground.stories.tsx +12 -0
  145. package/src/components/WelcomePlayground.test.tsx +47 -0
  146. package/src/components/WelcomePlayground.tsx +142 -0
  147. package/src/components/__snapshots__/stories.test.tsx.snap +2238 -0
  148. package/src/components/diff-content-view-layout.test.ts +198 -0
  149. package/src/components/diff-content-view-layout.ts +84 -0
  150. package/src/components/editor-grid.test.ts +86 -0
  151. package/src/components/editor-grid.ts +138 -0
  152. package/src/components/file-tree.test.ts +60 -0
  153. package/src/components/file-tree.ts +112 -0
  154. package/src/components/menu-control.stories.tsx +35 -0
  155. package/src/components/menu-control.tsx +43 -0
  156. package/src/components/pages/Workspace.stories.tsx +181 -0
  157. package/src/components/primitives/Button.stories.tsx +32 -0
  158. package/src/components/primitives/Button.tsx +56 -0
  159. package/src/components/primitives/Card.stories.tsx +25 -0
  160. package/src/components/primitives/Card.tsx +63 -0
  161. package/src/components/primitives/Dialog.stories.tsx +24 -0
  162. package/src/components/primitives/Dialog.test.tsx +37 -0
  163. package/src/components/primitives/Dialog.tsx +71 -0
  164. package/src/components/primitives/DialogActions.stories.tsx +20 -0
  165. package/src/components/primitives/DialogActions.tsx +39 -0
  166. package/src/components/primitives/IconButton.stories.tsx +36 -0
  167. package/src/components/primitives/IconButton.tsx +62 -0
  168. package/src/components/primitives/StatusBar.stories.tsx +11 -0
  169. package/src/components/primitives/StatusBar.tsx +23 -0
  170. package/src/components/primitives/Switch.stories.tsx +17 -0
  171. package/src/components/primitives/Switch.tsx +32 -0
  172. package/src/components/primitives/Tabs.stories.tsx +29 -0
  173. package/src/components/primitives/Tabs.tsx +102 -0
  174. package/src/components/primitives/Toolbar.stories.tsx +17 -0
  175. package/src/components/primitives/Toolbar.tsx +14 -0
  176. package/src/components/primitives/Tree.stories.tsx +124 -0
  177. package/src/components/primitives/Tree.tsx +100 -0
  178. package/src/components/primitives/frame.test.ts +12 -0
  179. package/src/components/primitives/frame.ts +17 -0
  180. package/src/components/primitives/icons.ts +61 -0
  181. package/src/components/primitives/tree-model.test.ts +99 -0
  182. package/src/components/primitives/tree-model.ts +137 -0
  183. package/src/components/quick-actions-editor.stories.tsx +58 -0
  184. package/src/components/quick-actions-editor.test.tsx +117 -0
  185. package/src/components/quick-actions-editor.tsx +226 -0
  186. package/src/components/share-dialog-store.ts +58 -0
  187. package/src/components/stories-app.tsx +193 -0
  188. package/src/components/stories.test.tsx +74 -0
  189. package/src/components/story-fixtures.ts +102 -0
  190. package/src/components/story-tree.test.ts +108 -0
  191. package/src/components/story.ts +159 -0
  192. package/src/components/syntax-highlight.ts +133 -0
  193. package/src/components/terminal-pane.test.tsx +40 -0
  194. package/src/components/terminal-pane.ts +199 -0
  195. package/src/components/theme-context.stories.tsx +46 -0
  196. package/src/components/theme-context.tsx +37 -0
  197. package/src/components/thread-tree.test.ts +134 -0
  198. package/src/components/thread-tree.ts +109 -0
  199. package/src/components/truncate-text.ts +7 -0
  200. package/src/components/truncate-title.test.ts +31 -0
  201. package/src/components/truncate-title.ts +15 -0
  202. package/src/config-identity.test.ts +78 -0
  203. package/src/config.test.ts +517 -33
  204. package/src/config.ts +467 -57
  205. package/src/diff-fold.test.ts +78 -0
  206. package/src/diff-fold.ts +126 -0
  207. package/src/diff-hunk-curate.test.ts +315 -0
  208. package/src/diff-hunk-curate.ts +153 -0
  209. package/src/diff-intraline.test.ts +238 -0
  210. package/src/diff-intraline.ts +236 -0
  211. package/src/diff-syntax.test.ts +112 -0
  212. package/src/diff-syntax.ts +179 -0
  213. package/src/discussions.ts +79 -0
  214. package/src/editor.test.ts +149 -0
  215. package/src/editor.ts +139 -9
  216. package/src/ghostty-terminal.test.ts +85 -0
  217. package/src/ghostty-terminal.ts +199 -0
  218. package/src/github-identity.test.ts +58 -0
  219. package/src/github-identity.ts +41 -0
  220. package/src/herdr.test.ts +9 -34
  221. package/src/herdr.ts +25 -21
  222. package/src/index.ts +13 -0
  223. package/src/integrations.test.ts +41 -0
  224. package/src/integrations.ts +32 -0
  225. package/src/intent-dispatch.test.ts +723 -0
  226. package/src/intent-dispatch.ts +692 -0
  227. package/src/key-bindings.test.ts +156 -0
  228. package/src/key-bindings.ts +459 -0
  229. package/src/keymap.test.ts +461 -113
  230. package/src/keymap.ts +321 -79
  231. package/src/kitty-image.test.ts +66 -0
  232. package/src/kitty-image.ts +138 -0
  233. package/src/launch-layout.test.ts +52 -0
  234. package/src/launch-layout.ts +42 -0
  235. package/src/mark-runs.ts +121 -0
  236. package/src/markdown-editor-syntax-style.ts +50 -0
  237. package/src/markdown-grid.test.ts +101 -0
  238. package/src/markdown-grid.ts +203 -0
  239. package/src/markdown-highlight.test.ts +61 -0
  240. package/src/markdown-highlight.ts +122 -0
  241. package/src/pasted-image.test.ts +67 -0
  242. package/src/pasted-image.ts +60 -0
  243. package/src/perf/otlp-trace.test.ts +116 -0
  244. package/src/perf/otlp-trace.ts +130 -0
  245. package/src/perf/perf-report.ts +17 -0
  246. package/src/perf/perf-timings.test.ts +18 -0
  247. package/src/perf/perf-timings.ts +38 -0
  248. package/src/prototype-browser.test.ts +64 -0
  249. package/src/prototype-browser.ts +329 -0
  250. package/src/prototype-pixels.tsx +35 -0
  251. package/src/pty.test.ts +81 -0
  252. package/src/pty.ts +245 -0
  253. package/src/ready-signal.test.ts +85 -0
  254. package/src/ready-signal.ts +52 -0
  255. package/src/run.ts +78 -13
  256. package/src/serve.test.ts +24 -12
  257. package/src/serve.ts +52 -10
  258. package/src/share.ts +237 -0
  259. package/src/skills.test.ts +74 -0
  260. package/src/skills.ts +75 -0
  261. package/src/slash-palette.test.ts +103 -0
  262. package/src/slash-palette.ts +129 -0
  263. package/src/split-diff.test.ts +110 -0
  264. package/src/split-diff.ts +149 -0
  265. package/src/test-support.test.ts +264 -0
  266. package/src/test-support.ts +369 -0
  267. package/src/theme-presets.test.ts +51 -0
  268. package/src/theme-presets.ts +214 -0
  269. package/src/theme.ts +84 -19
  270. package/src/thread-chords.test.ts +79 -0
  271. package/src/thread-chords.ts +163 -0
  272. package/src/thread-controller.curate.test.ts +450 -0
  273. package/src/thread-controller.derivation.test.ts +62 -0
  274. package/src/thread-controller.live-diff.test.ts +322 -0
  275. package/src/thread-controller.optimistic.test.ts +69 -0
  276. package/src/thread-controller.share.test.ts +561 -0
  277. package/src/thread-controller.switch-cache.test.ts +176 -0
  278. package/src/thread-controller.tree.test.ts +261 -0
  279. package/src/thread-controller.ts +1893 -0
  280. package/src/thread-selection.test.ts +210 -0
  281. package/src/thread-selection.ts +243 -0
  282. package/src/tree-view.test.ts +128 -0
  283. package/src/tree-view.ts +148 -0
  284. package/src/use-annotation-surface.tsx +915 -0
  285. package/src/use-changes-workbench.ts +190 -0
  286. package/src/use-frame-measure.ts +41 -0
  287. package/src/use-remember-layout.ts +31 -0
  288. package/src/use-settings-dialog.tsx +320 -0
  289. package/src/use-terminal-virtualizer.ts +233 -0
  290. package/src/use-thread-body-editing.tsx +58 -0
  291. package/src/version.ts +5 -0
  292. package/src/view-diff.test.ts +137 -9
  293. package/src/view-diff.ts +269 -23
  294. package/src/view-plan.regression.test.ts +60 -0
  295. package/src/view-plan.test.ts +456 -0
  296. package/src/view-plan.ts +719 -0
  297. package/src/visual/contrast.test.ts +35 -0
  298. package/src/visual/contrast.ts +33 -0
  299. package/src/walk.test.ts +121 -0
  300. package/src/walk.ts +74 -0
  301. package/src/workbench-snapshot.test.ts +81 -0
  302. package/src/workbench-snapshot.ts +29 -0
  303. package/src/session-controller.ts +0 -345
  304. package/src/syntax.test.ts +0 -41
  305. package/src/syntax.ts +0 -88
  306. package/src/view.test.ts +0 -157
  307. package/src/view.ts +0 -312
@@ -0,0 +1,99 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { allFolderIds, flattenTree, statusMeta, type TreeNode } from "./tree-model";
3
+
4
+ const TREE: TreeNode[] = [
5
+ {
6
+ id: "src",
7
+ label: "src",
8
+ children: [
9
+ { id: "src/a.ts", label: "a.ts", status: "modified" },
10
+ {
11
+ id: "src/nested",
12
+ label: "nested",
13
+ children: [
14
+ {
15
+ id: "src/nested/only",
16
+ label: "only",
17
+ children: [{ id: "deep", label: "deep.ts", status: "added" }],
18
+ },
19
+ ],
20
+ },
21
+ ],
22
+ },
23
+ { id: "readme", label: "README.md" },
24
+ ];
25
+
26
+ describe("flattenTree", () => {
27
+ test("a collapsed folder shows only itself; expanding it reveals its children", () => {
28
+ // Act
29
+ const collapsed = flattenTree(TREE, { expandedIds: new Set() });
30
+ const rows = flattenTree(TREE, { expandedIds: new Set(["src"]) });
31
+
32
+ // Assert
33
+ expect(collapsed.map((row) => row.label)).toEqual(["src", "README.md"]);
34
+ expect(rows.map((row) => [row.label, row.depth, row.isFolder])).toEqual([
35
+ ["src", 0, true],
36
+ ["a.ts", 1, false],
37
+ ["nested", 1, true],
38
+ ["README.md", 0, false],
39
+ ]);
40
+ expect(rows.find((row) => row.label === "src")!.expanded).toBe(true);
41
+ expect(rows.find((row) => row.label === "a.ts")!.status).toBe("modified");
42
+ });
43
+
44
+ test("flattenEmptyDirectories collapses a single-subfolder chain into one row", () => {
45
+ // Act - src open but the chain itself collapsed (none of its ids expanded)
46
+ const collapsed = flattenTree(TREE, {
47
+ expandedIds: new Set(["src"]),
48
+ flattenEmptyDirectories: true,
49
+ });
50
+ // Act - expanding via any id along the chain opens it
51
+ const expanded = flattenTree(TREE, {
52
+ expandedIds: new Set(["src", "src/nested/only"]),
53
+ flattenEmptyDirectories: true,
54
+ });
55
+
56
+ // Assert
57
+ expect(collapsed.map((row) => row.label)).toEqual(["src", "a.ts", "nested/only", "README.md"]);
58
+ expect(expanded.map((row) => row.label)).toEqual([
59
+ "src",
60
+ "a.ts",
61
+ "nested/only",
62
+ "deep.ts",
63
+ "README.md",
64
+ ]);
65
+ });
66
+
67
+ test("without the flag the chain stays one folder per row", () => {
68
+ // Act
69
+ const rows = flattenTree(TREE, {
70
+ expandedIds: new Set(["src", "src/nested", "src/nested/only"]),
71
+ });
72
+
73
+ // Assert
74
+ expect(rows.map((row) => [row.label, row.depth])).toEqual([
75
+ ["src", 0],
76
+ ["a.ts", 1],
77
+ ["nested", 1],
78
+ ["only", 2],
79
+ ["deep.ts", 3],
80
+ ["README.md", 0],
81
+ ]);
82
+ });
83
+ });
84
+
85
+ describe("allFolderIds", () => {
86
+ test("collects every folder, not the leaves", () => {
87
+ // Assert
88
+ expect([...allFolderIds(TREE)].sort()).toEqual(["src", "src/nested", "src/nested/only"]);
89
+ });
90
+ });
91
+
92
+ describe("statusMeta", () => {
93
+ test("maps each status to a letter and a tone", () => {
94
+ // Assert
95
+ expect(statusMeta("added")).toEqual({ letter: "A", tone: "green" });
96
+ expect(statusMeta("deleted")).toEqual({ letter: "D", tone: "red" });
97
+ expect(statusMeta("ignored")).toEqual({ letter: "I", tone: "muted" });
98
+ });
99
+ });
@@ -0,0 +1,137 @@
1
+ // Headless tree model mirrored from @pierre/trees (DOM-only): share the model, render our own.
2
+
3
+ export type GitStatus = "added" | "modified" | "deleted" | "renamed" | "untracked" | "ignored";
4
+
5
+ export interface TreeNode {
6
+ id: string;
7
+ label: string;
8
+ children?: TreeNode[];
9
+ status?: GitStatus;
10
+ badge?: string;
11
+ icon?: string;
12
+ }
13
+
14
+ export interface FlattenOptions {
15
+ expandedIds: ReadonlySet<string>;
16
+ flattenEmptyDirectories?: boolean;
17
+ }
18
+
19
+ export interface VisibleRow {
20
+ id: string;
21
+ label: string;
22
+ depth: number;
23
+ isFolder: boolean;
24
+ expanded: boolean;
25
+ status?: GitStatus;
26
+ badge?: string;
27
+ icon?: string;
28
+ }
29
+
30
+ function isFolder(node: TreeNode): boolean {
31
+ return node.children !== undefined;
32
+ }
33
+
34
+ interface CollapsedChain {
35
+ label: string;
36
+ tail: TreeNode;
37
+ chainIds: string[];
38
+ }
39
+
40
+ // flattenEmptyDirectories: fold a chain of single-subfolder folders into one row.
41
+ function collapseChain(node: TreeNode): CollapsedChain {
42
+ let tail = node;
43
+ const labels = [node.label];
44
+ const chainIds = [node.id];
45
+
46
+ while (tail.children?.length === 1 && isFolder(tail.children[0]!)) {
47
+ tail = tail.children[0]!;
48
+ labels.push(tail.label);
49
+ chainIds.push(tail.id);
50
+ }
51
+
52
+ return { label: labels.join("/"), tail, chainIds };
53
+ }
54
+
55
+ export function flattenTree(nodes: readonly TreeNode[], options: FlattenOptions): VisibleRow[] {
56
+ const rows: VisibleRow[] = [];
57
+ const walk = (list: readonly TreeNode[], depth: number): void => {
58
+ for (const node of list) {
59
+ if (!isFolder(node)) {
60
+ const leaf: VisibleRow = {
61
+ id: node.id,
62
+ label: node.label,
63
+ depth,
64
+ isFolder: false,
65
+ expanded: false,
66
+ };
67
+
68
+ if (node.status !== undefined) leaf.status = node.status;
69
+ if (node.badge !== undefined) leaf.badge = node.badge;
70
+ if (node.icon !== undefined) leaf.icon = node.icon;
71
+ rows.push(leaf);
72
+ continue;
73
+ }
74
+ const collapsed = options.flattenEmptyDirectories
75
+ ? collapseChain(node)
76
+ : { label: node.label, tail: node, chainIds: [node.id] };
77
+ // open when any id along a collapsed chain is expanded
78
+ const expanded = collapsed.chainIds.some((id) => options.expandedIds.has(id));
79
+ const row: VisibleRow = {
80
+ id: node.id,
81
+ label: collapsed.label,
82
+ depth,
83
+ isFolder: true,
84
+ expanded,
85
+ };
86
+
87
+ if (node.badge !== undefined) row.badge = node.badge;
88
+ if (node.icon !== undefined) row.icon = node.icon;
89
+ rows.push(row);
90
+ if (expanded) walk(collapsed.tail.children ?? [], depth + 1);
91
+ }
92
+ };
93
+
94
+ walk(nodes, 0);
95
+
96
+ return rows;
97
+ }
98
+
99
+ export function allFolderIds(nodes: readonly TreeNode[]): Set<string> {
100
+ const ids = new Set<string>();
101
+ const walk = (list: readonly TreeNode[]): void => {
102
+ for (const node of list) {
103
+ if (isFolder(node)) {
104
+ ids.add(node.id);
105
+ walk(node.children ?? []);
106
+ }
107
+ }
108
+ };
109
+
110
+ walk(nodes);
111
+
112
+ return ids;
113
+ }
114
+
115
+ export type TreeTone = "green" | "blue" | "red" | "muted";
116
+
117
+ export interface StatusMeta {
118
+ letter: string;
119
+ tone: TreeTone;
120
+ }
121
+
122
+ export function statusMeta(status: GitStatus): StatusMeta {
123
+ switch (status) {
124
+ case "added":
125
+ return { letter: "A", tone: "green" };
126
+ case "modified":
127
+ return { letter: "M", tone: "blue" };
128
+ case "deleted":
129
+ return { letter: "D", tone: "red" };
130
+ case "renamed":
131
+ return { letter: "R", tone: "blue" };
132
+ case "untracked":
133
+ return { letter: "U", tone: "green" };
134
+ case "ignored":
135
+ return { letter: "I", tone: "muted" };
136
+ }
137
+ }
@@ -0,0 +1,58 @@
1
+ import React from "react";
2
+ import { DARK } from "../theme";
3
+ import { DEFAULT_QUICK_ACTIONS } from "../config";
4
+ import type { Story, StoryMeta } from "./story";
5
+ import { QuickActionsEditor } from "./quick-actions-editor";
6
+
7
+ export const meta: StoryMeta = { title: "Chrome/QuickActionsEditor" };
8
+
9
+ /** The editor renders inside the settings body column. */
10
+ function BodyFrame({ children }: { children: React.ReactNode }): React.ReactNode {
11
+ return (
12
+ <box style={{ width: 54, height: 22, paddingLeft: 2, backgroundColor: DARK.elevated }}>
13
+ {children}
14
+ </box>
15
+ );
16
+ }
17
+
18
+ const noop = () => {};
19
+
20
+ export const Collapsed: Story = {
21
+ render: () => (
22
+ <BodyFrame>
23
+ <QuickActionsEditor
24
+ actions={DEFAULT_QUICK_ACTIONS}
25
+ selectedIndex={0}
26
+ expandedIndex={null}
27
+ expandedField="prompt"
28
+ onToggleExpand={noop}
29
+ onEditPrompt={() => {}}
30
+ onEditMetadata={noop}
31
+ onReset={noop}
32
+ onAdd={noop}
33
+ theme={DARK}
34
+ />
35
+ </BodyFrame>
36
+ ),
37
+ size: { width: 54, height: 22 },
38
+ };
39
+
40
+ export const RowExpanded: Story = {
41
+ render: () => (
42
+ <BodyFrame>
43
+ <QuickActionsEditor
44
+ actions={DEFAULT_QUICK_ACTIONS}
45
+ selectedIndex={1}
46
+ expandedIndex={1}
47
+ expandedField="prompt"
48
+ onToggleExpand={noop}
49
+ onEditPrompt={() => {}}
50
+ onEditMetadata={noop}
51
+ onReset={noop}
52
+ onAdd={noop}
53
+ theme={DARK}
54
+ />
55
+ </BodyFrame>
56
+ ),
57
+ size: { width: 54, height: 22 },
58
+ };
@@ -0,0 +1,117 @@
1
+ /** The Actions editor scrolls the list, and an expanded row edits both the title and the description. */
2
+
3
+ import { describe, expect, test } from "bun:test";
4
+ import React from "react";
5
+ import { testRender } from "@opentui/react/test-utils";
6
+ import type { QuickAction } from "../config";
7
+ import { locateText, settle, typeText } from "../test-support";
8
+ import { QuickActionsEditor } from "./quick-actions-editor";
9
+
10
+ const MANY: QuickAction[] = Array.from({ length: 14 }, (_, index) => ({
11
+ prompt: `Action number ${index}`,
12
+ }));
13
+
14
+ async function mount(selectedIndex: number) {
15
+ const setup = await testRender(
16
+ <QuickActionsEditor
17
+ actions={MANY}
18
+ selectedIndex={selectedIndex}
19
+ expandedIndex={null}
20
+ expandedField="prompt"
21
+ onToggleExpand={() => {}}
22
+ onEditPrompt={() => {}}
23
+ onEditMetadata={() => {}}
24
+ onReset={() => {}}
25
+ onAdd={() => {}}
26
+ />,
27
+ { width: 60, height: 10 },
28
+ );
29
+
30
+ await settle(setup);
31
+ await settle(setup);
32
+ await settle(setup);
33
+ await settle(setup);
34
+
35
+ return setup;
36
+ }
37
+
38
+ describe("QuickActionsEditor scrolling", () => {
39
+ test("a list too long for the pane clips into the scroll area instead of overlapping rows", async () => {
40
+ const setup = await mount(0);
41
+ const lines = setup.captureCharFrame().split("\n");
42
+
43
+ // the top rows read cleanly on their own lines - the overflow no longer paints rows on top of each other
44
+ expect(lines.some((line) => line.includes("Action number 0"))).toBe(true);
45
+ expect(lines.some((line) => line.includes("Action number 1"))).toBe(true);
46
+ // a bottom row is scrolled out of the clipped pane, not overlaid onto a visible one
47
+ expect(setup.captureCharFrame()).not.toContain("Action number 13");
48
+
49
+ setup.renderer.destroy();
50
+ });
51
+ });
52
+
53
+ describe("editing an expanded action", () => {
54
+ test("an expanded row edits both the title and the description", async () => {
55
+ const prompts: string[] = [];
56
+ const metadatas: string[] = [];
57
+ const setup = await testRender(
58
+ <QuickActionsEditor
59
+ actions={[{ prompt: "Ship it", metadata: "be terse" }]}
60
+ selectedIndex={0}
61
+ expandedIndex={0}
62
+ expandedField="prompt"
63
+ onToggleExpand={() => {}}
64
+ onEditPrompt={(_index, prompt) => prompts.push(prompt)}
65
+ onEditMetadata={(_index, metadata) => metadatas.push(metadata)}
66
+ onReset={() => {}}
67
+ onAdd={() => {}}
68
+ />,
69
+ { width: 60, height: 8 },
70
+ );
71
+
72
+ await settle(setup);
73
+ await settle(setup);
74
+
75
+ await typeText(setup, "!");
76
+ expect(prompts.at(-1)).toBe("Ship it!");
77
+
78
+ setup.mockInput.pressKey("RETURN");
79
+ await settle(setup);
80
+ await typeText(setup, "X");
81
+ expect(metadatas.at(-1)).toBe("be terseX");
82
+
83
+ setup.renderer.destroy();
84
+ });
85
+
86
+ test("clicking the description row opens the editor on the metadata field directly", async () => {
87
+ const toggles: Array<{ index: number; field: "prompt" | "metadata" }> = [];
88
+ const setup = await testRender(
89
+ <QuickActionsEditor
90
+ actions={[{ prompt: "Ship it", metadata: "be terse" }]}
91
+ selectedIndex={0}
92
+ expandedIndex={null}
93
+ expandedField="prompt"
94
+ onToggleExpand={(index, field) => toggles.push({ index, field })}
95
+ onEditPrompt={() => {}}
96
+ onEditMetadata={() => {}}
97
+ onReset={() => {}}
98
+ onAdd={() => {}}
99
+ />,
100
+ { width: 60, height: 8 },
101
+ );
102
+ await settle(setup);
103
+
104
+ // clicking the title opens the title field; clicking the description opens the metadata field
105
+ const title = locateText(setup, "Ship it");
106
+ await setup.mockMouse.click(title.column, title.row);
107
+ const description = locateText(setup, "be terse");
108
+ await setup.mockMouse.click(description.column, description.row);
109
+
110
+ expect(toggles).toEqual([
111
+ { index: 0, field: "prompt" },
112
+ { index: 0, field: "metadata" },
113
+ ]);
114
+
115
+ setup.renderer.destroy();
116
+ });
117
+ });
@@ -0,0 +1,226 @@
1
+ /**
2
+ * The Settings "Actions" body: the quick-action vocabulary editor. Each row is a
3
+ * clickable prompt; the selected-and-expanded row reveals a focused input for the
4
+ * per-action system prompt (the extra guidance appended when the action is used).
5
+ * A reset-to-defaults control and an add-action row bracket the list. Controlled:
6
+ * the app owns the actions, the selection, and which row is expanded.
7
+ */
8
+
9
+ import React, { useEffect, useRef, useState } from "react";
10
+ import type { KeyBinding, ScrollBoxRenderable, TextareaRenderable } from "@opentui/core";
11
+ import type { QuickAction } from "../config";
12
+ import type { Theme } from "../theme";
13
+ import { ScrollArea } from "./ScrollArea";
14
+ import { useComponentTheme } from "./theme-context";
15
+
16
+ // ⏎ never inserts a newline: a system prompt is one line, esc closes the editor.
17
+ const ACTION_INPUT_KEY_BINDINGS: KeyBinding[] = [{ name: "return", action: "submit" }];
18
+
19
+ export interface QuickActionsEditorProps {
20
+ actions: QuickAction[];
21
+ /** The highlighted row; equals the settings body row index. */
22
+ selectedIndex: number;
23
+ /** The row whose system-prompt input is open and focused, or null. */
24
+ expandedIndex: number | null;
25
+ /** Which field the open editor focuses first - set by the clicked row. */
26
+ expandedField: "prompt" | "metadata";
27
+ /** Select and toggle a row's editor open/closed, focusing the given field. */
28
+ onToggleExpand: (index: number, field: "prompt" | "metadata") => void;
29
+ onEditPrompt: (index: number, prompt: string) => void;
30
+ onEditMetadata: (index: number, metadata: string) => void;
31
+ onReset: () => void;
32
+ onAdd: () => void;
33
+ theme?: Theme;
34
+ }
35
+
36
+ export function QuickActionsEditor({
37
+ actions,
38
+ selectedIndex,
39
+ expandedIndex,
40
+ expandedField,
41
+ onToggleExpand,
42
+ onEditPrompt,
43
+ onEditMetadata,
44
+ onReset,
45
+ onAdd,
46
+ theme,
47
+ }: QuickActionsEditorProps): React.ReactNode {
48
+ const tokens = useComponentTheme(theme);
49
+ const scrollRef = useRef<ScrollBoxRenderable | null>(null);
50
+
51
+ useEffect(() => {
52
+ scrollRef.current?.scrollChildIntoView(`action-row-${selectedIndex}`);
53
+ }, [selectedIndex]);
54
+
55
+ return (
56
+ <box style={{ flexDirection: "column", flexGrow: 1 }}>
57
+ <box style={{ flexDirection: "row" }}>
58
+ <box style={{ flexGrow: 1 }} />
59
+ <box onMouseUp={onReset}>
60
+ <text fg={tokens.textDim}>reset</text>
61
+ </box>
62
+ </box>
63
+ <box style={{ height: 1 }} />
64
+ <ScrollArea scrollRef={scrollRef}>
65
+ {actions.map((action, index) => (
66
+ <ActionRow
67
+ key={index}
68
+ rowId={`action-row-${index}`}
69
+ action={action}
70
+ isSelected={index === selectedIndex}
71
+ isExpanded={index === expandedIndex}
72
+ initialField={expandedField}
73
+ onToggleExpand={(field) => onToggleExpand(index, field)}
74
+ onEditPrompt={(prompt) => onEditPrompt(index, prompt)}
75
+ onEditMetadata={(metadata) => onEditMetadata(index, metadata)}
76
+ theme={theme}
77
+ />
78
+ ))}
79
+ <box
80
+ id={`action-row-${actions.length}`}
81
+ style={{ backgroundColor: selectedIndex === actions.length ? tokens.border : undefined }}
82
+ onMouseUp={onAdd}
83
+ >
84
+ <text fg={tokens.textMuted}>+ Add action</text>
85
+ </box>
86
+ </ScrollArea>
87
+ </box>
88
+ );
89
+ }
90
+
91
+ function ActionRow({
92
+ rowId,
93
+ action,
94
+ isSelected,
95
+ isExpanded,
96
+ initialField,
97
+ onToggleExpand,
98
+ onEditPrompt,
99
+ onEditMetadata,
100
+ theme,
101
+ }: {
102
+ rowId: string;
103
+ action: QuickAction;
104
+ isSelected: boolean;
105
+ isExpanded: boolean;
106
+ initialField: "prompt" | "metadata";
107
+ onToggleExpand: (field: "prompt" | "metadata") => void;
108
+ onEditPrompt: (prompt: string) => void;
109
+ onEditMetadata: (metadata: string) => void;
110
+ theme?: Theme;
111
+ }): React.ReactNode {
112
+ const tokens = useComponentTheme(theme);
113
+
114
+ if (isExpanded) {
115
+ return (
116
+ <box id={rowId} style={{ flexDirection: "column" }}>
117
+ <ActionEditor
118
+ action={action}
119
+ isSelected={isSelected}
120
+ initialField={initialField}
121
+ onEditPrompt={onEditPrompt}
122
+ onEditMetadata={onEditMetadata}
123
+ onDone={() => onToggleExpand("prompt")}
124
+ tokens={tokens}
125
+ />
126
+ </box>
127
+ );
128
+ }
129
+
130
+ return (
131
+ <box id={rowId} style={{ flexDirection: "column" }}>
132
+ <box
133
+ style={{ backgroundColor: isSelected ? tokens.border : undefined }}
134
+ onMouseUp={() => onToggleExpand("prompt")}
135
+ >
136
+ <text fg={isSelected ? tokens.text : tokens.textMuted}>{action.prompt}</text>
137
+ </box>
138
+ {action.metadata ? (
139
+ <box onMouseUp={() => onToggleExpand("metadata")}>
140
+ <text fg={tokens.textDim}>{truncateMetadata(action.metadata)}</text>
141
+ </box>
142
+ ) : null}
143
+ </box>
144
+ );
145
+ }
146
+
147
+ /**
148
+ * The two-field editor for an expanded row: the action title over its system
149
+ * prompt. Mounts fresh per expand, so focus starts on the clicked field without a
150
+ * reset effect; ⏎ steps title -> description -> close, and a click focuses either.
151
+ */
152
+ function ActionEditor({
153
+ action,
154
+ isSelected,
155
+ initialField,
156
+ onEditPrompt,
157
+ onEditMetadata,
158
+ onDone,
159
+ tokens,
160
+ }: {
161
+ action: QuickAction;
162
+ isSelected: boolean;
163
+ initialField: "prompt" | "metadata";
164
+ onEditPrompt: (prompt: string) => void;
165
+ onEditMetadata: (metadata: string) => void;
166
+ onDone: () => void;
167
+ tokens: Theme;
168
+ }): React.ReactNode {
169
+ const promptRef = useRef<TextareaRenderable | null>(null);
170
+ const metadataRef = useRef<TextareaRenderable | null>(null);
171
+ const [activeField, setActiveField] = useState<"prompt" | "metadata">(initialField);
172
+
173
+ // the focused field types from its end, not from the caret parked at the start
174
+ useEffect(() => {
175
+ const editor = activeField === "prompt" ? promptRef.current : metadataRef.current;
176
+
177
+ if (editor) editor.cursorOffset = editor.plainText.length;
178
+ }, [activeField]);
179
+
180
+ const fieldStyle = {
181
+ height: 1,
182
+ flexGrow: 1,
183
+ backgroundColor: tokens.elevated,
184
+ focusedBackgroundColor: tokens.elevated,
185
+ textColor: tokens.text,
186
+ focusedTextColor: tokens.text,
187
+ } as const;
188
+
189
+ return (
190
+ <>
191
+ <box
192
+ style={{ flexDirection: "row", backgroundColor: isSelected ? tokens.border : undefined }}
193
+ >
194
+ <textarea
195
+ ref={promptRef}
196
+ focused={activeField === "prompt"}
197
+ initialValue={action.prompt}
198
+ placeholder="action title"
199
+ keyBindings={ACTION_INPUT_KEY_BINDINGS}
200
+ onMouseUp={() => setActiveField("prompt")}
201
+ onSubmit={() => setActiveField("metadata")}
202
+ onContentChange={() => onEditPrompt(promptRef.current?.plainText ?? "")}
203
+ style={fieldStyle}
204
+ />
205
+ </box>
206
+ <box style={{ flexDirection: "row" }}>
207
+ <textarea
208
+ ref={metadataRef}
209
+ focused={activeField === "metadata"}
210
+ initialValue={action.metadata ?? ""}
211
+ placeholder="extra system prompt appended to this comment (optional)"
212
+ keyBindings={ACTION_INPUT_KEY_BINDINGS}
213
+ onMouseUp={() => setActiveField("metadata")}
214
+ onSubmit={onDone}
215
+ onContentChange={() => onEditMetadata(metadataRef.current?.plainText ?? "")}
216
+ style={fieldStyle}
217
+ />
218
+ </box>
219
+ </>
220
+ );
221
+ }
222
+
223
+ /** A one-line preview of the system prompt under a collapsed row. */
224
+ function truncateMetadata(metadata: string): string {
225
+ return metadata.length > 44 ? `${metadata.slice(0, 43)}…` : metadata;
226
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * UI state for the share dialog, held in a zustand store rather than juggled
3
+ * across component state: which category is open (share externally / export),
4
+ * whether the link wizard is showing, and the wizard's draft (name, auth,
5
+ * allowlist). The links themselves live on the thread; this is only the
6
+ * ephemeral surface state.
7
+ */
8
+
9
+ import { createStore } from "zustand/vanilla";
10
+ import { useStore } from "zustand";
11
+
12
+ export type ShareCategory = "external" | "export";
13
+
14
+ export interface ShareWizardDraft {
15
+ /** The link being edited, or null when creating a new one. */
16
+ editingId: string | null;
17
+ name: string;
18
+ requireAuth: boolean;
19
+ allowlist: string[];
20
+ }
21
+
22
+ export interface ShareDialogState {
23
+ category: ShareCategory;
24
+ /** Null = the links list; a draft = the new/edit wizard. */
25
+ wizard: ShareWizardDraft | null;
26
+ setCategory: (category: ShareCategory) => void;
27
+ openWizard: (draft: ShareWizardDraft) => void;
28
+ closeWizard: () => void;
29
+ setName: (name: string) => void;
30
+ setRequireAuth: (requireAuth: boolean) => void;
31
+ setAllowlist: (allowlist: string[]) => void;
32
+ /** Reset to the opening state (links list, external tab) when the dialog opens. */
33
+ reset: () => void;
34
+ }
35
+
36
+ /** A fresh wizard draft, prefilled with the thread name for a new link. */
37
+ export function newWizardDraft(threadName: string): ShareWizardDraft {
38
+ return { editingId: null, name: threadName, requireAuth: false, allowlist: [] };
39
+ }
40
+
41
+ export const shareDialogStore = createStore<ShareDialogState>((set) => ({
42
+ category: "external",
43
+ wizard: null,
44
+ setCategory: (category) => set({ category }),
45
+ openWizard: (wizard) => set({ wizard }),
46
+ closeWizard: () => set({ wizard: null }),
47
+ setName: (name) => set((state) => (state.wizard ? { wizard: { ...state.wizard, name } } : {})),
48
+ setRequireAuth: (requireAuth) =>
49
+ set((state) => (state.wizard ? { wizard: { ...state.wizard, requireAuth } } : {})),
50
+ setAllowlist: (allowlist) =>
51
+ set((state) => (state.wizard ? { wizard: { ...state.wizard, allowlist } } : {})),
52
+ reset: () => set({ category: "external", wizard: null }),
53
+ }));
54
+
55
+ /** Subscribe a component to the share dialog store. */
56
+ export function useShareDialog<T>(selector: (state: ShareDialogState) => T): T {
57
+ return useStore(shareDialogStore, selector);
58
+ }