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

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 +1406 -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 +787 -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 +29 -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 +78 -0
  241. package/src/launch-layout.ts +62 -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,29 @@
1
+ import { expect, test } from "bun:test";
2
+ import React from "react";
3
+ import { testRender } from "@opentui/react/test-utils";
4
+ import { FileContentsView } from "./FileContentsView";
5
+ import { waitForText } from "../test-support";
6
+ import { DARK } from "../theme";
7
+
8
+ test("renders the loaded file contents", async () => {
9
+ const setup = await testRender(
10
+ <FileContentsView
11
+ path="src/add.ts"
12
+ loadContents={() => Promise.resolve("const answer = 42;\n")}
13
+ theme={DARK}
14
+ />,
15
+ { width: 60, height: 10 },
16
+ );
17
+
18
+ await waitForText(setup, "const answer = 42;");
19
+ });
20
+
21
+ test("shows a centered File deleted message when the file cannot be read", async () => {
22
+ const setup = await testRender(
23
+ <FileContentsView path="missing.ts" loadContents={() => Promise.resolve(null)} theme={DARK} />,
24
+ { width: 60, height: 10 },
25
+ );
26
+
27
+ await waitForText(setup, "File deleted");
28
+ expect(setup.captureCharFrame()).toContain("File deleted");
29
+ });
@@ -0,0 +1,98 @@
1
+ // A Changes/Project file tab in contents mode: a workspace file rendered read-only with line
2
+ // numbers, for surfaces with no thread to anchor notes to (the bare-launch welcome shell).
3
+ // Loads on mount and whenever the path changes; a null read renders a centered "File deleted".
4
+ // Syntax highlighting comes from the native code renderable (tree-sitter), the language
5
+ // auto-detected from the path; unknown languages simply render unstyled.
6
+
7
+ import { ScrollArea } from "./ScrollArea";
8
+ import React, { useEffect, useRef, useState } from "react";
9
+ import type { CodeRenderable } from "@opentui/core";
10
+ import type { Theme } from "../theme";
11
+ import { useComponentTheme } from "./theme-context";
12
+ import { filetypeForPath, syntaxStyleFor } from "./syntax-highlight";
13
+
14
+ export interface FileContentsViewProps {
15
+ path: string;
16
+ loadContents: (path: string) => Promise<string | null>;
17
+ theme?: Theme;
18
+ }
19
+
20
+ /** A finished read for a specific path; `lines` is null when the file could not be read. */
21
+ interface FileLoad {
22
+ path: string;
23
+ lines: string[] | null;
24
+ }
25
+
26
+ export function FileContentsView({
27
+ path,
28
+ loadContents,
29
+ theme,
30
+ }: FileContentsViewProps): React.ReactNode {
31
+ const tokens = useComponentTheme(theme);
32
+ const [loaded, setLoaded] = useState<FileLoad | null>(null);
33
+ // The gutter mirrors the code renderable's line info, so it needs the mounted
34
+ // instance; a state-backed ref rebinds the target once the code renderable exists.
35
+ const [codeTarget, setCodeTarget] = useState<CodeRenderable | null>(null);
36
+ const loadRef = useRef(loadContents);
37
+ useEffect(() => {
38
+ loadRef.current = loadContents;
39
+ });
40
+
41
+ useEffect(() => {
42
+ let alive = true;
43
+ void loadRef.current(path).then(
44
+ (contents) => {
45
+ if (alive) setLoaded({ path, lines: contents === null ? null : contents.split("\n") });
46
+ },
47
+ () => {
48
+ if (alive) setLoaded({ path, lines: null });
49
+ },
50
+ );
51
+
52
+ return () => {
53
+ alive = false;
54
+ };
55
+ }, [path]);
56
+
57
+ if (loaded === null || loaded.path !== path) {
58
+ return (
59
+ <box style={{ flexGrow: 1, paddingLeft: 1, paddingTop: 1 }}>
60
+ <text fg={tokens.textDim}>loading...</text>
61
+ </box>
62
+ );
63
+ }
64
+ if (loaded.lines === null) {
65
+ // a file that no longer reads in the Changes/Project view is one the working tree deleted;
66
+ // the bottom pad lifts the text one row so it lines up with the footer-shortened thread empty state
67
+ return (
68
+ <box
69
+ style={{ flexGrow: 1, alignItems: "center", justifyContent: "center", paddingBottom: 2 }}
70
+ >
71
+ <text fg={tokens.textDim}>File deleted</text>
72
+ </box>
73
+ );
74
+ }
75
+ const content = loaded.lines.join("\n");
76
+
77
+ return (
78
+ <ScrollArea>
79
+ <line-number
80
+ target={codeTarget ?? undefined}
81
+ showLineNumbers
82
+ fg={tokens.textDim}
83
+ minWidth={5}
84
+ paddingRight={1}
85
+ style={{ paddingTop: 1, paddingLeft: 1 }}
86
+ >
87
+ <code
88
+ ref={setCodeTarget}
89
+ content={content}
90
+ filetype={filetypeForPath(path)}
91
+ syntaxStyle={syntaxStyleFor(tokens)}
92
+ selectable={false}
93
+ style={{ wrapMode: "none", fg: tokens.text }}
94
+ />
95
+ </line-number>
96
+ </ScrollArea>
97
+ );
98
+ }
@@ -0,0 +1,59 @@
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 { GridTabContent, type DiffSurfaceProps } from "./GridTabContent";
6
+ import { fixtureDiffRows, fixtureDiffSession } from "./story-fixtures";
7
+ import { changesTab, fileTab } from "./editor-grid";
8
+
9
+ export const meta: StoryMeta = { title: "Surfaces/GridTabContent" };
10
+
11
+ const noop = (): void => {};
12
+
13
+ function surface(): DiffSurfaceProps {
14
+ return {
15
+ session: fixtureDiffSession({ annotations: [] }),
16
+ quickActions: DEFAULT_QUICK_ACTIONS,
17
+ observer: false,
18
+ onAnnotate: noop,
19
+ onReply: noop,
20
+ onUpdateAnnotation: noop,
21
+ onExit: noop,
22
+ };
23
+ }
24
+
25
+ /** The whole-diff Changes tab: every changed file in one scroll container. */
26
+ export const ChangesTab: Story = {
27
+ render: () => (
28
+ <GridTabContent
29
+ tab={changesTab()}
30
+ rows={fixtureDiffRows()}
31
+ surface={surface()}
32
+ rejectedRows={new Set()}
33
+ dimmed={false}
34
+ readFile={async () => null}
35
+ onAddFileComment={noop}
36
+ theme={DARK}
37
+ />
38
+ ),
39
+ expectedColors: [DARK.text],
40
+ size: { width: 80, height: 18 },
41
+ };
42
+
43
+ /** A single-file diff tab: that file's rows alone, indexed on their own. */
44
+ export const FileDiffTab: Story = {
45
+ render: () => (
46
+ <GridTabContent
47
+ tab={fileTab("store.ts", "src/store.ts", "diff")}
48
+ rows={fixtureDiffRows()}
49
+ surface={surface()}
50
+ rejectedRows={new Set()}
51
+ dimmed={false}
52
+ readFile={async () => null}
53
+ onAddFileComment={noop}
54
+ theme={DARK}
55
+ />
56
+ ),
57
+ expectedColors: [DARK.text],
58
+ size: { width: 80, height: 18 },
59
+ };
@@ -0,0 +1,182 @@
1
+ /**
2
+ * The body an editor tab paints: the whole working-tree/artifact diff for the
3
+ * Changes tab, or a single file's diff (or read-only contents) for a file tab.
4
+ * Shared by the thread workbench and the bare-launch shell so both render a
5
+ * changed file the same way, with comments routed back through one surface.
6
+ */
7
+
8
+ import React, { useMemo } from "react";
9
+ import { type Anchor } from "@cueloop/schema";
10
+ import { dimmedTheme, type Theme } from "../theme";
11
+ import { changesMarks, type DiffRow } from "../view-diff";
12
+ import {
13
+ DiffContentView,
14
+ type DiffFoldControls,
15
+ type DiffContentViewProps,
16
+ } from "./DiffContentView";
17
+ import { AnnotatableFileView } from "./AnnotatableFileView";
18
+ import type { EditorTab } from "./editor-grid";
19
+
20
+ /** The inline-commenting props the diff sheet shares with the thread view, wired once by the app. */
21
+ export type DiffSurfaceProps = Pick<
22
+ DiffContentViewProps,
23
+ | "session"
24
+ | "quickActions"
25
+ | "observer"
26
+ | "commentsEnabled"
27
+ | "resolved"
28
+ | "suspended"
29
+ | "onComposingChange"
30
+ | "onObserverBlocked"
31
+ | "onCursorChange"
32
+ | "focusedAnnotationId"
33
+ | "onFocusAnnotation"
34
+ | "onAnnotate"
35
+ | "onReply"
36
+ | "onUpdateAnnotation"
37
+ | "resolveAuthorLabel"
38
+ | "onNavCommand"
39
+ | "onExit"
40
+ >;
41
+
42
+ /** The Changes tab body: the whole diff in one scroll container, or a bare hint when nothing changed. */
43
+ export function ChangesTabBody(props: {
44
+ rows: DiffRow[];
45
+ surface: DiffSurfaceProps;
46
+ rejectedRows: Set<number>;
47
+ fold?: DiffFoldControls;
48
+ fileStats?: ReadonlyMap<string, { additions: number; deletions: number }>;
49
+ split?: boolean;
50
+ dimmed: boolean;
51
+ /** Rows the sibling Thread pane spends on its footer, so this empty hint centers level with it. */
52
+ emptyBottomPadding?: number;
53
+ theme: Theme;
54
+ }): React.ReactNode {
55
+ const session = props.surface.session;
56
+ const marks = useMemo(
57
+ () => changesMarks(session, props.rows, props.surface.focusedAnnotationId),
58
+ [session, props.rows, props.surface.focusedAnnotationId],
59
+ );
60
+
61
+ if (props.rows.length === 0) {
62
+ return (
63
+ <box
64
+ style={{
65
+ flexGrow: 1,
66
+ flexDirection: "column",
67
+ alignItems: "center",
68
+ justifyContent: "center",
69
+ paddingBottom: props.emptyBottomPadding,
70
+ }}
71
+ >
72
+ <text fg={props.theme.textDim}>No changes</text>
73
+ </box>
74
+ );
75
+ }
76
+
77
+ return (
78
+ <DiffContentView
79
+ rows={props.rows}
80
+ marks={marks}
81
+ {...props.surface}
82
+ rejectedRows={props.rejectedRows}
83
+ fold={props.fold}
84
+ fileStats={props.fileStats}
85
+ split={props.split}
86
+ theme={props.dimmed ? dimmedTheme(props.theme) : undefined}
87
+ />
88
+ );
89
+ }
90
+
91
+ /** An editor tab's body: the whole diff for the Changes tab, a file's diff or contents for a file tab. */
92
+ export function GridTabContent(props: {
93
+ tab: EditorTab;
94
+ rows: DiffRow[];
95
+ surface: DiffSurfaceProps;
96
+ rejectedRows: Set<number>;
97
+ fold?: DiffFoldControls;
98
+ fileStats?: ReadonlyMap<string, { additions: number; deletions: number }>;
99
+ split?: boolean;
100
+ dimmed: boolean;
101
+ readFile: (path: string) => Promise<string | null>;
102
+ /** Persist a comment on a project file; the file view built the anchor against its own lines. */
103
+ onAddFileComment: (path: string, anchor: Anchor, body: string) => void;
104
+ /** Rows the sibling Thread pane spends on its footer, so the empty hint centers level with it. */
105
+ emptyBottomPadding?: number;
106
+ theme: Theme;
107
+ }): React.ReactNode {
108
+ const { tab } = props;
109
+ if (tab.kind === "file" && tab.fileView === "contents" && tab.path !== undefined) {
110
+ const filePath = tab.path;
111
+
112
+ return (
113
+ <AnnotatableFileView
114
+ path={filePath}
115
+ loadContents={props.readFile}
116
+ session={props.surface.session}
117
+ quickActions={props.surface.quickActions}
118
+ observer={props.surface.observer}
119
+ resolved={props.surface.resolved}
120
+ suspended={props.surface.suspended}
121
+ focusedAnnotationId={props.surface.focusedAnnotationId}
122
+ onFocusAnnotation={props.surface.onFocusAnnotation}
123
+ onComposingChange={props.surface.onComposingChange}
124
+ onCursorChange={props.surface.onCursorChange}
125
+ onObserverBlocked={props.surface.onObserverBlocked}
126
+ onAddComment={(anchor, body) => props.onAddFileComment(filePath, anchor, body)}
127
+ onReply={props.surface.onReply}
128
+ onUpdateAnnotation={props.surface.onUpdateAnnotation}
129
+ resolveAuthorLabel={props.surface.resolveAuthorLabel}
130
+ onExit={props.surface.onExit}
131
+ theme={props.theme}
132
+ />
133
+ );
134
+ }
135
+ if (tab.kind !== "file") {
136
+ return (
137
+ <ChangesTabBody
138
+ rows={props.rows}
139
+ surface={props.surface}
140
+ rejectedRows={props.rejectedRows}
141
+ fold={props.fold}
142
+ fileStats={props.fileStats}
143
+ split={props.split}
144
+ dimmed={props.dimmed}
145
+ emptyBottomPadding={props.emptyBottomPadding}
146
+ theme={props.theme}
147
+ />
148
+ );
149
+ }
150
+ // a single-file tab shows that file's rows alone, so its row indices are its own: comments and
151
+ // the caret report back in whole-diff indices, and the fold controls (a band to fold) do not apply
152
+ const fileRowIndices = props.rows.flatMap((row, index) => (row.file === tab.path ? [index] : []));
153
+ const rows = fileRowIndices.map((index) => props.rows[index]!);
154
+ const wholeIndex = (rowIndex: number): number => fileRowIndices[rowIndex] ?? rowIndex;
155
+ const rejectedRows = new Set(
156
+ fileRowIndices.flatMap((index, rowIndex) => (props.rejectedRows.has(index) ? [rowIndex] : [])),
157
+ );
158
+
159
+ return (
160
+ <ChangesTabBody
161
+ rows={rows}
162
+ surface={{
163
+ ...props.surface,
164
+ onCursorChange: (rowIndex) => props.surface.onCursorChange?.(wholeIndex(rowIndex)),
165
+ onAnnotate: (span, body) =>
166
+ props.surface.onAnnotate(
167
+ {
168
+ start: { blockIndex: wholeIndex(span.start.blockIndex), char: span.start.char },
169
+ end: { blockIndex: wholeIndex(span.end.blockIndex), char: span.end.char },
170
+ },
171
+ body,
172
+ ),
173
+ }}
174
+ rejectedRows={rejectedRows}
175
+ fileStats={props.fileStats}
176
+ split={props.split}
177
+ dimmed={props.dimmed}
178
+ emptyBottomPadding={props.emptyBottomPadding}
179
+ theme={props.theme}
180
+ />
181
+ );
182
+ }
@@ -0,0 +1,24 @@
1
+ import React from "react";
2
+ import { DARK } from "../theme";
3
+ import type { Story, StoryMeta } from "./story";
4
+ import { HandleChipsInput } from "./HandleChipsInput";
5
+
6
+ export const meta: StoryMeta = { title: "Inputs/HandleChips" };
7
+
8
+ export const WithHandles: Story = {
9
+ render: () => (
10
+ <box style={{ padding: 1, backgroundColor: DARK.elevated }}>
11
+ <HandleChipsInput logins={["octocat", "hubot"]} onChange={() => {}} theme={DARK} />
12
+ </box>
13
+ ),
14
+ size: { width: 48, height: 6 },
15
+ };
16
+
17
+ export const Empty: Story = {
18
+ render: () => (
19
+ <box style={{ padding: 1, backgroundColor: DARK.elevated }}>
20
+ <HandleChipsInput logins={[]} onChange={() => {}} theme={DARK} />
21
+ </box>
22
+ ),
23
+ size: { width: 48, height: 6 },
24
+ };
@@ -0,0 +1,35 @@
1
+ /** Backspace on the empty field pulls the last chip back in to edit, rather than deleting it. */
2
+
3
+ import React, { useState } from "react";
4
+ import { describe, expect, test } from "bun:test";
5
+ import { testRender } from "@opentui/react/test-utils";
6
+ import { DARK } from "../theme";
7
+ import { press, settle } from "../test-support";
8
+ import { HandleChipsInput } from "./HandleChipsInput";
9
+
10
+ /** A controlled host so onChange edits flow back into the rendered list, like the wizard. */
11
+ function Harness({ initial }: { initial: string[] }): React.ReactNode {
12
+ const [logins, setLogins] = useState(initial);
13
+
14
+ return <HandleChipsInput logins={logins} onChange={setLogins} theme={DARK} />;
15
+ }
16
+
17
+ describe("HandleChipsInput", () => {
18
+ test("backspace on the empty field restores the last chip as editable text", async () => {
19
+ // Arrange - one committed chip and an empty input showing its placeholder
20
+ const setup = await testRender(<Harness initial={["octocat"]} />, { width: 48, height: 6 });
21
+
22
+ await settle(setup);
23
+ expect(setup.captureCharFrame()).toContain("handle");
24
+
25
+ // Act - backspace with the input empty
26
+ await press(setup, "backspace");
27
+
28
+ // Assert - the handle is back in the field (placeholder gone), not deleted
29
+ const frame = setup.captureCharFrame();
30
+
31
+ expect(frame).toContain("octocat");
32
+ expect(frame).not.toContain("handle");
33
+ setup.renderer.destroy();
34
+ });
35
+ });
@@ -0,0 +1,145 @@
1
+ /**
2
+ * A tag-style input for GitHub logins: each added handle is an inline chip with
3
+ * a hover × to remove, and a trailing one-line field where enter adds the next
4
+ * and backspace on an empty field drops the last. Controlled - the caller owns
5
+ * the list and receives every change.
6
+ */
7
+
8
+ import React, { useEffect, useRef, useState } from "react";
9
+ import { useKeyboard } 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 { NERD } from "./primitives/icons";
14
+
15
+ // enter adds the handle rather than inserting a newline into the one-line field
16
+ const ADD_KEY_BINDINGS: KeyBinding[] = [{ name: "return", action: "submit" }];
17
+
18
+ function HandleChip({
19
+ login,
20
+ onRemove,
21
+ tokens,
22
+ }: {
23
+ login: string;
24
+ onRemove: () => void;
25
+ tokens: Theme;
26
+ }): React.ReactNode {
27
+ const [hovered, setHovered] = useState(false);
28
+
29
+ return (
30
+ <box
31
+ onMouseOver={() => setHovered(true)}
32
+ onMouseOut={() => setHovered(false)}
33
+ style={{
34
+ flexDirection: "row",
35
+ flexShrink: 0,
36
+ marginRight: 2,
37
+ paddingLeft: 1,
38
+ backgroundColor: tokens.border,
39
+ }}
40
+ >
41
+ <text fg={tokens.text} style={{ wrapMode: "none" }}>{`@${login}`}</text>
42
+ <box
43
+ onMouseUp={(event) => {
44
+ event.stopPropagation();
45
+ onRemove();
46
+ }}
47
+ style={{ paddingLeft: 1, paddingRight: 1 }}
48
+ >
49
+ <text fg={tokens.textDim}>{hovered ? NERD.close : " "}</text>
50
+ </box>
51
+ </box>
52
+ );
53
+ }
54
+
55
+ export interface HandleChipsInputProps {
56
+ logins: string[];
57
+ onChange: (logins: string[]) => void;
58
+ /** Whether this field owns the cursor and backspace; false shows the chips without a blinking caret. */
59
+ focused?: boolean;
60
+ /** Fired on backspace when the field is empty and no chips remain, so the caller can collapse the section. */
61
+ onEmptyBackspace?: () => void;
62
+ theme?: Theme;
63
+ }
64
+
65
+ export function HandleChipsInput({
66
+ logins,
67
+ onChange,
68
+ focused = true,
69
+ onEmptyBackspace,
70
+ theme,
71
+ }: HandleChipsInputProps): React.ReactNode {
72
+ const tokens = useComponentTheme(theme);
73
+ const inputRef = useRef<TextareaRenderable | null>(null);
74
+ // the text the current field generation mounts with; a remount reseeds it
75
+ const [seed, setSeed] = useState("");
76
+ const [inputGeneration, setInputGeneration] = useState(0);
77
+
78
+ useEffect(() => {
79
+ const input = inputRef.current;
80
+
81
+ if (!input || !focused) return;
82
+ input.focus();
83
+ input.cursorOffset = seed.length; // land the caret at the end of the seeded text
84
+ // eslint-disable-next-line react-hooks/exhaustive-deps
85
+ }, [inputGeneration, focused]);
86
+
87
+ const reseed = (text: string): void => {
88
+ setSeed(text);
89
+ setInputGeneration((generation) => generation + 1);
90
+ };
91
+
92
+ const addDraft = (): void => {
93
+ const handle = (inputRef.current?.plainText ?? "").trim().replace(/^@/, "").toLowerCase();
94
+
95
+ if (handle && !logins.includes(handle)) onChange([...logins, handle]);
96
+ reseed("");
97
+ };
98
+
99
+ useKeyboard((key) => {
100
+ if (!focused) return;
101
+ if (key.name !== "backspace" || (inputRef.current?.plainText ?? "").length > 0) return;
102
+ // backspace on an empty field pulls the last chip back in to edit; with none left, collapse
103
+ if (logins.length > 0) {
104
+ const last = logins[logins.length - 1]!;
105
+
106
+ onChange(logins.slice(0, -1));
107
+ reseed(last);
108
+ } else {
109
+ onEmptyBackspace?.();
110
+ }
111
+ });
112
+
113
+ return (
114
+ <box style={{ flexDirection: "row", flexWrap: "wrap", alignItems: "center" }}>
115
+ {logins.map((login) => (
116
+ <HandleChip
117
+ key={login}
118
+ login={login}
119
+ onRemove={() => onChange(logins.filter((entry) => entry !== login))}
120
+ tokens={tokens}
121
+ />
122
+ ))}
123
+ <box style={{ flexDirection: "row", flexShrink: 0 }}>
124
+ <text fg={tokens.textDim}>{"@"}</text>
125
+ <textarea
126
+ key={inputGeneration}
127
+ ref={inputRef}
128
+ focused
129
+ initialValue={seed}
130
+ placeholder="handle"
131
+ keyBindings={ADD_KEY_BINDINGS}
132
+ onSubmit={addDraft}
133
+ style={{
134
+ height: 1,
135
+ width: 16,
136
+ backgroundColor: tokens.elevated,
137
+ focusedBackgroundColor: tokens.elevated,
138
+ textColor: tokens.text,
139
+ focusedTextColor: tokens.text,
140
+ }}
141
+ />
142
+ </box>
143
+ </box>
144
+ );
145
+ }
@@ -0,0 +1,31 @@
1
+ import React from "react";
2
+ import { DARK } from "../theme";
3
+ import type { Story, StoryMeta } from "./story";
4
+ import { KeybindsDialog } from "./KeybindsDialog";
5
+
6
+ export const meta: StoryMeta = { title: "Overlays/KeybindsDialog" };
7
+
8
+ const SECTIONS = [
9
+ {
10
+ title: "Review",
11
+ entries: [
12
+ { keys: "j / k", label: "move" },
13
+ { keys: "v", label: "span" },
14
+ { keys: "c", label: "comment" },
15
+ { keys: "enter", label: "submit" },
16
+ ],
17
+ },
18
+ {
19
+ title: "Submit",
20
+ entries: [
21
+ { keys: "← / →", label: "message" },
22
+ { keys: "esc", label: "cancel" },
23
+ ],
24
+ },
25
+ ];
26
+
27
+ export const Cheatsheet: Story = {
28
+ render: () => <KeybindsDialog sections={SECTIONS} />,
29
+ expectedColors: [DARK.accent],
30
+ size: { width: 70, height: 24 },
31
+ };