@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,44 @@
1
+ import React from "react";
2
+ import type { DiffFileContents } from "@cueloop/schema";
3
+ import { DARK } from "../theme";
4
+ import type { Story, StoryMeta } from "./story";
5
+ import { ChangesColumn } from "./ChangesColumn";
6
+
7
+ export const meta: StoryMeta = { title: "Surfaces/ChangesColumn" };
8
+
9
+ function Frame({ children }: { children: React.ReactNode }): React.ReactNode {
10
+ return <box style={{ width: "100%", height: "100%", flexDirection: "row" }}>{children}</box>;
11
+ }
12
+
13
+ const files: DiffFileContents[] = [
14
+ { path: "packages/client/src/App.tsx", oldContents: "a", newContents: "b", status: "modified" },
15
+ {
16
+ path: "packages/client/src/components/ChangesColumn.tsx",
17
+ oldContents: "",
18
+ newContents: "c",
19
+ status: "added",
20
+ },
21
+ {
22
+ path: "packages/daemon/src/thread-review.ts",
23
+ oldContents: "d",
24
+ newContents: "",
25
+ status: "deleted",
26
+ },
27
+ { path: "README.md", oldContents: "e", newContents: "f", status: "modified" },
28
+ ];
29
+
30
+ export const Changed: Story = {
31
+ render: () => (
32
+ <Frame>
33
+ <ChangesColumn
34
+ open
35
+ files={files}
36
+ selectedPath="packages/client/src/App.tsx"
37
+ onSelectFile={() => {}}
38
+ theme={DARK}
39
+ />
40
+ </Frame>
41
+ ),
42
+ expectedColors: [DARK.accent, DARK.border],
43
+ size: { width: 60, height: 12 },
44
+ };
@@ -0,0 +1,199 @@
1
+ // The right-hand Changes column: the diff's changed files as a directory tree.
2
+ // A click on a file scrolls the diff to that file. Renders nothing when collapsed
3
+ // so the diff reclaims the width. Mirrors the left Threads column on the far side.
4
+ // Co-located here: the state hook that opens it by context and the header toggle.
5
+
6
+ import { ScrollArea } from "./ScrollArea";
7
+ import React, { useMemo, useState } from "react";
8
+ import { useKeyboard } from "@opentui/react";
9
+ import type { DiffFileContents, Thread } from "@cueloop/schema";
10
+ import type { Theme } from "../theme";
11
+ import type { DiffRow } from "../view-diff";
12
+ import { Tree } from "./primitives/Tree";
13
+ import { allFolderIds, flattenTree } from "./primitives/tree-model";
14
+ import { IconButton } from "./primitives/IconButton";
15
+ import { NERD } from "./primitives/icons";
16
+ import { buildFileTree } from "./file-tree";
17
+
18
+ export interface DiffColumnsState {
19
+ changesOpen: boolean;
20
+ toggleChanges: () => void;
21
+ /** The path under the diff cursor, highlighted in the Changes tree. */
22
+ currentFilePath?: string;
23
+ scrollToFile: (path: string) => void;
24
+ }
25
+
26
+ /**
27
+ * Owns the right Changes column for a diff: context opens it when you enter a
28
+ * diff thread, and a file click scrolls the sheet to that file's first row.
29
+ * Resets on session change via the guarded render-time pattern, no effect.
30
+ */
31
+ export function useDiffColumns(params: {
32
+ session: Thread | null;
33
+ rows: DiffRow[];
34
+ cursor: number;
35
+ setCursor: (index: number) => void;
36
+ }): DiffColumnsState {
37
+ const { session, rows, cursor, setCursor } = params;
38
+ const contextDefault = session?.artifact.type === "diff";
39
+ const [changesOpen, setChangesOpen] = useState(contextDefault);
40
+ const [seenSessionId, setSeenSessionId] = useState(session?.id);
41
+
42
+ // entering another thread lets context decide again; within a thread the manual toggle stays
43
+ if (session?.id !== seenSessionId) {
44
+ setSeenSessionId(session?.id);
45
+ setChangesOpen(contextDefault);
46
+ }
47
+
48
+ return {
49
+ changesOpen,
50
+ toggleChanges: () => setChangesOpen((open) => !open),
51
+ currentFilePath: rows[cursor]?.file,
52
+ scrollToFile: (path: string) => {
53
+ const fileRowIndex = rows.findIndex((row) => row.kind === "file" && row.file === path);
54
+
55
+ if (fileRowIndex >= 0) setCursor(fileRowIndex);
56
+ },
57
+ };
58
+ }
59
+
60
+ /** The header toggle for the Changes column; shown only for a diff, where files exist to browse. */
61
+ export function DiffChangesToggle({
62
+ isDiff,
63
+ open,
64
+ onToggle,
65
+ theme,
66
+ }: {
67
+ isDiff: boolean;
68
+ open: boolean;
69
+ onToggle: () => void;
70
+ theme?: Theme;
71
+ }): React.ReactNode {
72
+ if (!isDiff) return null;
73
+
74
+ return (
75
+ <IconButton
76
+ glyph={open ? NERD.sidebarRight : NERD.sidebarRightOff}
77
+ onPress={onToggle}
78
+ marginLeft={2}
79
+ theme={theme}
80
+ />
81
+ );
82
+ }
83
+
84
+ export interface ChangesColumnProps {
85
+ open: boolean;
86
+ files?: readonly DiffFileContents[];
87
+ /** The path whose diff the sheet is scrolled to, highlighted in the tree. */
88
+ selectedPath?: string;
89
+ onSelectFile: (path: string) => void;
90
+ /** Comments per file path, shown as a dot-and-count badge on each entry. */
91
+ commentCounts?: ReadonlyMap<string, number>;
92
+ focused?: boolean;
93
+ width?: number;
94
+ theme?: Theme;
95
+ }
96
+
97
+ /**
98
+ * The changed-files tree on its own, with no surrounding column frame: the body
99
+ * a workbench Project pane renders. A file click scrolls the diff to that file.
100
+ */
101
+ export function ChangesFileTree({
102
+ files = [],
103
+ selectedPath,
104
+ onSelectFile,
105
+ commentCounts,
106
+ focused = false,
107
+ theme,
108
+ }: Omit<ChangesColumnProps, "open" | "width">): React.ReactNode {
109
+ const nodes = useMemo(() => buildFileTree(files, commentCounts), [files, commentCounts]);
110
+ // every folder opens by default so each changed file is reachable; the user
111
+ // only ever names the folders they fold shut, so a new file set stays open
112
+ const [collapsedIds, setCollapsedIds] = useState<ReadonlySet<string>>(new Set());
113
+ const [cursor, setCursor] = useState(0);
114
+ const expandedIds = useMemo(() => {
115
+ const expanded = allFolderIds(nodes);
116
+
117
+ for (const id of collapsedIds) expanded.delete(id);
118
+
119
+ return expanded;
120
+ }, [nodes, collapsedIds]);
121
+ const rows = useMemo(
122
+ () => flattenTree(nodes, { expandedIds, flattenEmptyDirectories: true }),
123
+ [nodes, expandedIds],
124
+ );
125
+ const cursorIndex = Math.min(cursor, Math.max(0, rows.length - 1));
126
+
127
+ const toggle = (id: string): void =>
128
+ setCollapsedIds((current) => {
129
+ const next = new Set(current);
130
+
131
+ if (next.has(id)) next.delete(id);
132
+ else next.add(id);
133
+
134
+ return next;
135
+ });
136
+
137
+ useKeyboard((key) => {
138
+ if (!focused || rows.length === 0) return;
139
+ if (key.name === "j" || key.name === "down")
140
+ return setCursor(Math.min(cursorIndex + 1, rows.length - 1));
141
+ if (key.name === "k" || key.name === "up") return setCursor(Math.max(cursorIndex - 1, 0));
142
+ const row = rows[cursorIndex];
143
+ if (!row) return;
144
+ if (key.name === "tab" || key.name === "return" || key.name === "enter")
145
+ return row.isFolder ? toggle(row.id) : onSelectFile(row.id);
146
+ if (key.name === "l" && row.isFolder && collapsedIds.has(row.id)) return toggle(row.id);
147
+ if (key.name === "h" && row.isFolder && !collapsedIds.has(row.id)) return toggle(row.id);
148
+ });
149
+
150
+ return (
151
+ <ScrollArea>
152
+ <Tree
153
+ nodes={nodes}
154
+ expandedIds={expandedIds}
155
+ selectedId={focused ? rows[cursorIndex]?.id : selectedPath}
156
+ flattenEmptyDirectories
157
+ showStatus
158
+ onSelect={(id) => {
159
+ const index = rows.findIndex((row) => row.id === id);
160
+
161
+ if (index >= 0) setCursor(index);
162
+ onSelectFile(id);
163
+ }}
164
+ onToggle={toggle}
165
+ theme={theme}
166
+ />
167
+ </ScrollArea>
168
+ );
169
+ }
170
+
171
+ export function ChangesColumn({
172
+ open,
173
+ files = [],
174
+ selectedPath,
175
+ onSelectFile,
176
+ width = 32,
177
+ theme,
178
+ }: ChangesColumnProps): React.ReactNode {
179
+ if (!open) return null;
180
+
181
+ return (
182
+ <box
183
+ style={{
184
+ width,
185
+ flexDirection: "column",
186
+ borderStyle: "single",
187
+ border: ["left"],
188
+ borderColor: theme?.border,
189
+ }}
190
+ >
191
+ <ChangesFileTree
192
+ files={files}
193
+ selectedPath={selectedPath}
194
+ onSelectFile={onSelectFile}
195
+ theme={theme}
196
+ />
197
+ </box>
198
+ );
199
+ }
@@ -0,0 +1,61 @@
1
+ /** The changed-files tree navigates by keyboard when focused: j/k move the cursor, tab folds a folder
2
+ * or 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 type { DiffFileContents } from "@cueloop/schema";
8
+ import { ChangesFileTree } from "./ChangesColumn";
9
+ import { settle, waitForText } from "../test-support";
10
+ import { DARK } from "../theme";
11
+
12
+ const file = (path: string): DiffFileContents => ({
13
+ path,
14
+ oldContents: "",
15
+ newContents: "x",
16
+ status: "modified",
17
+ });
18
+
19
+ const files = [file("src/a.ts"), file("src/b.ts")];
20
+
21
+ test("j moves onto a file and tab opens it", async () => {
22
+ const opened: string[] = [];
23
+ const setup = await testRender(
24
+ <ChangesFileTree
25
+ files={files}
26
+ onSelectFile={(path) => opened.push(path)}
27
+ focused
28
+ theme={DARK}
29
+ />,
30
+ { width: 40, height: 12 },
31
+ );
32
+ await settle(setup);
33
+ await waitForText(setup, "a.ts");
34
+
35
+ // folders open by default; cursor starts on the "src" folder, j steps onto a.ts
36
+ setup.mockInput.pressKey("j");
37
+ await settle(setup);
38
+ setup.mockInput.pressKey("TAB");
39
+ await settle(setup);
40
+
41
+ expect(opened).toEqual(["src/a.ts"]);
42
+
43
+ setup.renderer.destroy();
44
+ });
45
+
46
+ test("without focus the keyboard opens nothing", async () => {
47
+ const opened: string[] = [];
48
+ const setup = await testRender(
49
+ <ChangesFileTree files={files} onSelectFile={(path) => opened.push(path)} theme={DARK} />,
50
+ { width: 40, height: 12 },
51
+ );
52
+ await settle(setup);
53
+ await waitForText(setup, "a.ts");
54
+
55
+ setup.mockInput.pressKey("j");
56
+ setup.mockInput.pressKey("TAB");
57
+ await settle(setup);
58
+ expect(opened).toEqual([]);
59
+
60
+ setup.renderer.destroy();
61
+ });
@@ -0,0 +1,42 @@
1
+ import React from "react";
2
+ import { DARK } from "../theme";
3
+ import type { Story, StoryMeta } from "./story";
4
+ import { CodeBlock } from "./CodeBlock";
5
+
6
+ export const meta: StoryMeta = { title: "Primitives/CodeBlock" };
7
+
8
+ const SNIPPET = `export function gate(full: number) {
9
+ // threshold
10
+ return full < 0.6 ? "blocked" : "clear";
11
+ }`;
12
+
13
+ export const Typescript: Story = {
14
+ render: () => <CodeBlock language="ts" content={SNIPPET} />,
15
+ expectedColors: [DARK.elevated],
16
+ };
17
+
18
+ export const CursorAnnotatedEdited: Story = {
19
+ render: () => (
20
+ <CodeBlock
21
+ language="ts"
22
+ content={SNIPPET}
23
+ isCursor
24
+ isAnnotated
25
+ changeTag="edited"
26
+ marginTop={1}
27
+ />
28
+ ),
29
+ expectedColors: [DARK.accent],
30
+ };
31
+
32
+ export const UnknownLanguage: Story = {
33
+ render: () => (
34
+ <CodeBlock language="brainfog" content={"plain verbatim lines\n with indentation"} />
35
+ ),
36
+ };
37
+
38
+ /** A cut code block: struck-through gray content, no [cut] tag. */
39
+ export const Cut: Story = {
40
+ render: () => <CodeBlock language="ts" content={SNIPPET} cut />,
41
+ expectedColors: [DARK.elevated],
42
+ };
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Fenced code block: verbatim lines in a contained elevated box, highlighted
3
+ * by the native code renderable (tree-sitter). Unstyled text draws first, so
4
+ * the content never flashes empty while highlights resolve; unknown languages
5
+ * stay unstyled.
6
+ */
7
+
8
+ import React from "react";
9
+ import { createTextAttributes } from "@opentui/core";
10
+ import type { Theme } from "../theme";
11
+ import { useComponentTheme } from "./theme-context";
12
+ import { filetypeFor, syntaxStyleFor } from "./syntax-highlight";
13
+
14
+ /** A cut code block reads as removed: struck through and grayed, never red. */
15
+ const CUT_ATTRIBUTES = createTextAttributes({ strikethrough: true, dim: true });
16
+
17
+ export interface CodeBlockProps {
18
+ id?: string;
19
+ /** Markdown fence info string ("tsx", "python", ...). */
20
+ language?: string;
21
+ content: string;
22
+ isCursor?: boolean;
23
+ /** Vertical rhythm above the block. */
24
+ marginTop?: number;
25
+ isAnnotated?: boolean;
26
+ /** Working-copy change tag rendered after the language label. */
27
+ changeTag?: "new" | "edited";
28
+ /** A cut (removed) block: struck-through gray content, no tag. */
29
+ cut?: boolean;
30
+ theme?: Theme;
31
+ }
32
+
33
+ export function CodeBlock({
34
+ id,
35
+ language,
36
+ content,
37
+ isCursor = false,
38
+ marginTop = 0,
39
+ isAnnotated = false,
40
+ changeTag,
41
+ cut = false,
42
+ theme,
43
+ }: CodeBlockProps): React.ReactNode {
44
+ const tokens = useComponentTheme(theme);
45
+ const tagColor = changeTag === "new" ? tokens.green : tokens.accent;
46
+
47
+ return (
48
+ <box id={id} style={{ flexDirection: "column", marginTop }}>
49
+ <text>
50
+ <span fg={isCursor ? tokens.accent : tokens.textDim}>{isCursor ? "▎ " : " "}</span>
51
+ <span fg={tokens.textDim}>{language ?? "code"}</span>
52
+ {isAnnotated ? <span fg={tokens.accent}> ◆</span> : null}
53
+ {changeTag ? <span fg={tagColor}> [{changeTag}]</span> : null}
54
+ </text>
55
+ <box
56
+ style={{
57
+ flexDirection: "column",
58
+ backgroundColor: tokens.elevated,
59
+ paddingLeft: 2,
60
+ paddingRight: 2,
61
+ paddingTop: 1,
62
+ paddingBottom: 1,
63
+ marginLeft: 2,
64
+ }}
65
+ >
66
+ {cut ? (
67
+ <text style={{ wrapMode: "none" }} selectable={false}>
68
+ <span fg={tokens.textDim} attributes={CUT_ATTRIBUTES}>
69
+ {content}
70
+ </span>
71
+ </text>
72
+ ) : (
73
+ <code
74
+ content={content}
75
+ filetype={filetypeFor(language)}
76
+ syntaxStyle={syntaxStyleFor(tokens)}
77
+ selectable={false}
78
+ style={{ wrapMode: "none", fg: tokens.textMuted, bg: tokens.elevated }}
79
+ />
80
+ )}
81
+ </box>
82
+ </box>
83
+ );
84
+ }
@@ -0,0 +1,37 @@
1
+ import React from "react";
2
+ import { DARK } from "../theme";
3
+ import type { Story, StoryMeta } from "./story";
4
+ import { CompletionOverlay } from "./CompletionOverlay";
5
+
6
+ export const meta: StoryMeta = { title: "Overlays/CompletionOverlay" };
7
+
8
+ const noop = () => {};
9
+
10
+ export const ApprovedPrompt: Story = {
11
+ render: () => (
12
+ <CompletionOverlay
13
+ message="approved"
14
+ completion={{ phase: "prompt" }}
15
+ status=""
16
+ onClose={noop}
17
+ onBackToPlan={noop}
18
+ onAlways={noop}
19
+ />
20
+ ),
21
+ expectedColors: [DARK.green],
22
+ };
23
+
24
+ export const FeedbackCounting: Story = {
25
+ render: () => (
26
+ <CompletionOverlay
27
+ message="changes_requested"
28
+ completion={{ phase: "counting", remaining: 3 }}
29
+ status="exported to vault/cueloop/plan.md"
30
+ returnsTo="agent/worker-3"
31
+ onClose={noop}
32
+ onBackToPlan={noop}
33
+ onAlways={noop}
34
+ />
35
+ ),
36
+ expectedColors: [DARK.accent],
37
+ };
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Post-submit hand-back overlay: the agent is unblocked, the reviewer either
3
+ * closes now, opts into auto-close, or dismisses back to the resolved view.
4
+ * The latest status line (e.g. the vault-export path) stays visible here.
5
+ */
6
+
7
+ import React from "react";
8
+ import type { MessageOutcome } from "@cueloop/schema";
9
+ import type { Theme } from "../theme";
10
+ import { useComponentTheme } from "./theme-context";
11
+ import { Toolbar } from "./primitives/Toolbar";
12
+ import { Button } from "./primitives/Button";
13
+
14
+ export interface CompletionOverlayProps {
15
+ message: MessageOutcome;
16
+ completion: { phase: "prompt" } | { phase: "counting"; remaining: number };
17
+ /** Latest status line (e.g. the vault-export path) stays visible here. */
18
+ status: string;
19
+ /** Where focus goes on close (the agent's pane), when known. */
20
+ returnsTo?: string;
21
+ onClose: () => void;
22
+ onBackToPlan: () => void;
23
+ onAlways: () => void;
24
+ theme?: Theme;
25
+ }
26
+
27
+ export function CompletionOverlay({
28
+ message,
29
+ completion,
30
+ status,
31
+ returnsTo,
32
+ onClose,
33
+ onBackToPlan,
34
+ onAlways,
35
+ theme,
36
+ }: CompletionOverlayProps): React.ReactNode {
37
+ const tokens = useComponentTheme(theme);
38
+ const approved = message === "approved";
39
+
40
+ return (
41
+ <box
42
+ style={{
43
+ width: "100%",
44
+ height: "100%",
45
+ backgroundColor: tokens.background,
46
+ flexDirection: "column",
47
+ justifyContent: "center",
48
+ alignItems: "center",
49
+ }}
50
+ >
51
+ <text fg={approved ? tokens.green : tokens.accent}>
52
+ {approved ? "review approved" : "feedback sent"}
53
+ </text>
54
+ <text> </text>
55
+ <text fg={tokens.text}>
56
+ The agent has your {approved ? "approval" : "feedback"} and is unblocked.
57
+ </text>
58
+ {returnsTo ? <text fg={tokens.textDim}>returning to {returnsTo} on close</text> : null}
59
+ {status ? <text fg={tokens.textDim}>{status}</text> : null}
60
+ {completion.phase === "counting" ? (
61
+ <text fg={tokens.textDim}>closing in {completion.remaining}s</text>
62
+ ) : null}
63
+ <text> </text>
64
+ <Toolbar>
65
+ <Button variant="solid" marginRight={2} onPress={onClose} theme={theme}>
66
+ {" close "}
67
+ </Button>
68
+ <Button marginRight={2} onPress={onBackToPlan} theme={theme}>
69
+ {" back to plan "}
70
+ </Button>
71
+ <Button onPress={onAlways} theme={theme}>
72
+ {" always "}
73
+ </Button>
74
+ </Toolbar>
75
+ </box>
76
+ );
77
+ }
@@ -0,0 +1,37 @@
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 { ConfirmCard } from "./ConfirmCard";
6
+
7
+ export const meta: StoryMeta = { title: "Cards/ConfirmCard" };
8
+
9
+ const callbacks = {
10
+ onInput: () => {},
11
+ onSelectMessage: () => {},
12
+ onSubmit: () => {},
13
+ onCancel: () => {},
14
+ quickActions: DEFAULT_QUICK_ACTIONS,
15
+ };
16
+
17
+ export const ApproveDefault: Story = {
18
+ render: () => <ConfirmCard message="approved" summary="" {...callbacks} />,
19
+ expectedColors: [DARK.green, DARK.accent],
20
+ size: { width: 40, height: 14 },
21
+ };
22
+
23
+ export const RequestChanges: Story = {
24
+ render: () => (
25
+ <ConfirmCard message="changes_requested" summary="Tighten the steps." {...callbacks} />
26
+ ),
27
+ expectedColors: [DARK.red],
28
+ size: { width: 40, height: 14 },
29
+ };
30
+
31
+ export const WithViewedSummary: Story = {
32
+ render: () => (
33
+ <ConfirmCard message="approved" summary="" viewedSummary="2/3 files viewed" {...callbacks} />
34
+ ),
35
+ expectedColors: [DARK.green, DARK.textDim],
36
+ size: { width: 40, height: 14 },
37
+ };