@cueloop/client 0.1.0-alpha.7 → 0.1.0-alpha.71

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 (248) 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 +39 -32
  8. package/src/App.appearance.test.tsx +53 -0
  9. package/src/App.autoclose.test.tsx +76 -50
  10. package/src/App.diff.test.tsx +156 -42
  11. package/src/App.export.test.tsx +58 -40
  12. package/src/App.plan-review.test.tsx +363 -0
  13. package/src/App.prototype-markdown.test.tsx +92 -0
  14. package/src/App.prototype.test.tsx +176 -0
  15. package/src/App.readonly.test.tsx +128 -67
  16. package/src/App.rendering.test.tsx +61 -35
  17. package/src/App.submit-confirm.test.tsx +191 -0
  18. package/src/App.test.tsx +310 -114
  19. package/src/App.toast.test.tsx +89 -0
  20. package/src/App.tsx +1043 -798
  21. package/src/App.walk.test.tsx +298 -0
  22. package/src/App.workbench.test.tsx +495 -0
  23. package/src/__snapshots__/view-plan.regression.test.ts.snap +100 -0
  24. package/src/annotation-palette.ts +61 -0
  25. package/src/app-key-state.ts +61 -0
  26. package/src/app-screens.tsx +432 -0
  27. package/src/app-view-model.ts +159 -0
  28. package/src/attribution.test.ts +31 -0
  29. package/src/attribution.ts +42 -0
  30. package/src/clipboard.test.ts +23 -0
  31. package/src/clipboard.ts +33 -0
  32. package/src/components/AnnotatableFileView.stories.tsx +42 -0
  33. package/src/components/AnnotatableFileView.test.tsx +102 -0
  34. package/src/components/AnnotatableFileView.tsx +126 -0
  35. package/src/components/AnnotationCards.stories.tsx +87 -0
  36. package/src/components/AnnotationCards.tsx +303 -0
  37. package/src/components/AppHeader.stories.tsx +50 -0
  38. package/src/components/AppHeader.tsx +83 -0
  39. package/src/components/AppShell.stories.tsx +89 -0
  40. package/src/components/AppShell.tsx +290 -0
  41. package/src/components/AppShell.zoom.test.tsx +135 -0
  42. package/src/components/BareWorkbenchFileView.stories.tsx +27 -0
  43. package/src/components/BareWorkbenchFileView.tsx +64 -0
  44. package/src/components/Breadcrumb.stories.tsx +19 -0
  45. package/src/components/Breadcrumb.tsx +45 -0
  46. package/src/components/ChangesColumn.stories.tsx +39 -0
  47. package/src/components/ChangesColumn.tsx +171 -0
  48. package/src/components/CodeBlock.stories.tsx +42 -0
  49. package/src/components/CodeBlock.tsx +84 -0
  50. package/src/components/CompletionOverlay.stories.tsx +23 -0
  51. package/src/components/CompletionOverlay.tsx +60 -0
  52. package/src/components/ConfirmCard.stories.tsx +35 -0
  53. package/src/components/ConfirmCard.tsx +128 -0
  54. package/src/components/ConfirmDialog.stories.tsx +21 -0
  55. package/src/components/ConfirmDialog.tsx +66 -0
  56. package/src/components/DiffContentView.scroll.test.tsx +121 -0
  57. package/src/components/DiffContentView.split.test.tsx +140 -0
  58. package/src/components/DiffContentView.stories.tsx +80 -0
  59. package/src/components/DiffContentView.syntax.test.tsx +82 -0
  60. package/src/components/DiffContentView.tsx +859 -0
  61. package/src/components/DiscussionMarkerRail.stories.tsx +54 -0
  62. package/src/components/DiscussionMarkerRail.tsx +181 -0
  63. package/src/components/EditorGrid.stories.tsx +59 -0
  64. package/src/components/EditorGrid.tabs.test.tsx +118 -0
  65. package/src/components/EditorGrid.tsx +368 -0
  66. package/src/components/EditorGrid.zoom.test.tsx +68 -0
  67. package/src/components/FileContentsView.stories.tsx +16 -0
  68. package/src/components/FileContentsView.test.tsx +29 -0
  69. package/src/components/FileContentsView.tsx +98 -0
  70. package/src/components/KeybindsDialog.stories.tsx +31 -0
  71. package/src/components/KeybindsDialog.tsx +103 -0
  72. package/src/components/MenuBar.stories.tsx +28 -0
  73. package/src/components/MenuBar.tsx +44 -0
  74. package/src/components/OverlayScrollbar.stories.tsx +33 -0
  75. package/src/components/OverlayScrollbar.test.tsx +139 -0
  76. package/src/components/OverlayScrollbar.tsx +129 -0
  77. package/src/components/PaneGrid.stories.tsx +84 -0
  78. package/src/components/PaneGrid.tsx +128 -0
  79. package/src/components/PanelColumn.stories.tsx +25 -0
  80. package/src/components/PanelColumn.tsx +95 -0
  81. package/src/components/ProjectTreeView.stories.tsx +25 -0
  82. package/src/components/ProjectTreeView.test.tsx +32 -0
  83. package/src/components/ProjectTreeView.tsx +83 -0
  84. package/src/components/PromptDialog.stories.tsx +36 -0
  85. package/src/components/PromptDialog.tsx +87 -0
  86. package/src/components/PrototypeContentView.stories.tsx +19 -0
  87. package/src/components/PrototypeContentView.tsx +446 -0
  88. package/src/components/RootOverlay.stories.tsx +44 -0
  89. package/src/components/RootOverlay.tsx +35 -0
  90. package/src/components/ScrollArea.stories.tsx +20 -0
  91. package/src/components/ScrollArea.tsx +44 -0
  92. package/src/components/SettingsDialog.stories.tsx +117 -0
  93. package/src/components/SettingsDialog.tsx +195 -0
  94. package/src/components/SettingsRows.stories.tsx +18 -0
  95. package/src/components/SettingsRows.tsx +112 -0
  96. package/src/components/ShellHeader.stories.tsx +50 -0
  97. package/src/components/ShellHeader.tsx +137 -0
  98. package/src/components/ThreadFooter.stories.tsx +30 -0
  99. package/src/components/ThreadFooter.test.tsx +31 -0
  100. package/src/components/ThreadFooter.tsx +54 -0
  101. package/src/components/ThreadTree.stories.tsx +46 -0
  102. package/src/components/ThreadTree.tsx +219 -0
  103. package/src/components/ThreadView.selection.test.tsx +812 -0
  104. package/src/components/ThreadView.slash.test.tsx +158 -0
  105. package/src/components/ThreadView.stories.tsx +26 -0
  106. package/src/components/ThreadView.tsx +330 -0
  107. package/src/components/ThreadsSidebar.stories.tsx +36 -0
  108. package/src/components/ThreadsSidebar.tsx +71 -0
  109. package/src/components/Toast.stories.tsx +12 -0
  110. package/src/components/Toast.tsx +45 -0
  111. package/src/components/Tooltip.stories.tsx +26 -0
  112. package/src/components/Tooltip.tsx +72 -0
  113. package/src/components/TreePane.stories.tsx +106 -0
  114. package/src/components/TreePane.tsx +157 -0
  115. package/src/components/WalkWizard.stories.tsx +100 -0
  116. package/src/components/WalkWizard.tsx +152 -0
  117. package/src/components/WelcomePlayground.stories.tsx +12 -0
  118. package/src/components/WelcomePlayground.test.tsx +47 -0
  119. package/src/components/WelcomePlayground.tsx +140 -0
  120. package/src/components/__snapshots__/stories.test.tsx.snap +1845 -0
  121. package/src/components/agent-launcher.stories.tsx +26 -0
  122. package/src/components/agent-launcher.test.tsx +50 -0
  123. package/src/components/agent-launcher.tsx +149 -0
  124. package/src/components/diff-content-view-layout.test.ts +198 -0
  125. package/src/components/diff-content-view-layout.ts +84 -0
  126. package/src/components/editor-grid.test.ts +86 -0
  127. package/src/components/editor-grid.ts +138 -0
  128. package/src/components/fade-title.test.ts +49 -0
  129. package/src/components/fade-title.ts +61 -0
  130. package/src/components/file-tree.test.ts +60 -0
  131. package/src/components/file-tree.ts +112 -0
  132. package/src/components/pages/Workspace.stories.tsx +181 -0
  133. package/src/components/primitives/Button.stories.tsx +32 -0
  134. package/src/components/primitives/Button.tsx +52 -0
  135. package/src/components/primitives/Card.stories.tsx +25 -0
  136. package/src/components/primitives/Card.tsx +60 -0
  137. package/src/components/primitives/Dialog.stories.tsx +24 -0
  138. package/src/components/primitives/Dialog.tsx +66 -0
  139. package/src/components/primitives/IconButton.stories.tsx +36 -0
  140. package/src/components/primitives/IconButton.tsx +62 -0
  141. package/src/components/primitives/StatusBar.stories.tsx +11 -0
  142. package/src/components/primitives/StatusBar.tsx +23 -0
  143. package/src/components/primitives/Tabs.stories.tsx +29 -0
  144. package/src/components/primitives/Tabs.tsx +102 -0
  145. package/src/components/primitives/Toolbar.stories.tsx +17 -0
  146. package/src/components/primitives/Toolbar.tsx +14 -0
  147. package/src/components/primitives/Tree.stories.tsx +124 -0
  148. package/src/components/primitives/Tree.tsx +96 -0
  149. package/src/components/primitives/frame.test.ts +12 -0
  150. package/src/components/primitives/frame.ts +14 -0
  151. package/src/components/primitives/icons.ts +61 -0
  152. package/src/components/primitives/tree-model.test.ts +99 -0
  153. package/src/components/primitives/tree-model.ts +137 -0
  154. package/src/components/quick-actions-editor.stories.tsx +54 -0
  155. package/src/components/quick-actions-editor.test.tsx +49 -0
  156. package/src/components/quick-actions-editor.tsx +150 -0
  157. package/src/components/session-tree.test.ts +134 -0
  158. package/src/components/session-tree.ts +108 -0
  159. package/src/components/stories-app.tsx +184 -0
  160. package/src/components/stories.test.tsx +66 -0
  161. package/src/components/story-fixtures.ts +102 -0
  162. package/src/components/story-tree.test.ts +100 -0
  163. package/src/components/story.ts +142 -0
  164. package/src/components/syntax-highlight.ts +110 -0
  165. package/src/components/terminal-pane.test.tsx +40 -0
  166. package/src/components/terminal-pane.ts +199 -0
  167. package/src/components/theme-context.stories.tsx +46 -0
  168. package/src/components/theme-context.tsx +37 -0
  169. package/src/components/truncate-text.ts +7 -0
  170. package/src/config.test.ts +461 -31
  171. package/src/config.ts +389 -57
  172. package/src/diff-fold.test.ts +78 -0
  173. package/src/diff-fold.ts +126 -0
  174. package/src/diff-hunk-curate.test.ts +315 -0
  175. package/src/diff-hunk-curate.ts +153 -0
  176. package/src/diff-intraline.test.ts +165 -0
  177. package/src/diff-intraline.ts +146 -0
  178. package/src/diff-syntax.test.ts +112 -0
  179. package/src/diff-syntax.ts +179 -0
  180. package/src/discussions.ts +79 -0
  181. package/src/editor.test.ts +149 -0
  182. package/src/editor.ts +139 -9
  183. package/src/ghostty-terminal.test.ts +85 -0
  184. package/src/ghostty-terminal.ts +199 -0
  185. package/src/herdr.test.ts +9 -34
  186. package/src/herdr.ts +25 -21
  187. package/src/index.ts +11 -0
  188. package/src/integrations.test.ts +41 -0
  189. package/src/integrations.ts +32 -0
  190. package/src/intent-dispatch.test.ts +698 -0
  191. package/src/intent-dispatch.ts +700 -0
  192. package/src/key-bindings.test.ts +156 -0
  193. package/src/key-bindings.ts +465 -0
  194. package/src/keymap.test.ts +611 -0
  195. package/src/keymap.ts +410 -0
  196. package/src/kitty-image.test.ts +66 -0
  197. package/src/kitty-image.ts +138 -0
  198. package/src/mark-runs.ts +120 -0
  199. package/src/prototype-browser.test.ts +56 -0
  200. package/src/prototype-browser.ts +307 -0
  201. package/src/prototype-pixels.tsx +35 -0
  202. package/src/pty.test.ts +81 -0
  203. package/src/pty.ts +245 -0
  204. package/src/ready-signal.test.ts +85 -0
  205. package/src/ready-signal.ts +52 -0
  206. package/src/run.ts +30 -9
  207. package/src/serve.test.ts +24 -12
  208. package/src/serve.ts +20 -9
  209. package/src/share.ts +215 -0
  210. package/src/skills.test.ts +74 -0
  211. package/src/skills.ts +68 -0
  212. package/src/slash-palette.test.ts +54 -0
  213. package/src/slash-palette.ts +104 -0
  214. package/src/split-diff.test.ts +110 -0
  215. package/src/split-diff.ts +149 -0
  216. package/src/test-support.test.ts +264 -0
  217. package/src/test-support.ts +355 -0
  218. package/src/theme-presets.test.ts +51 -0
  219. package/src/theme-presets.ts +214 -0
  220. package/src/theme.ts +84 -19
  221. package/src/thread-chords.test.ts +170 -0
  222. package/src/thread-chords.ts +169 -0
  223. package/src/thread-controller.curate.test.ts +438 -0
  224. package/src/thread-controller.live-diff.test.ts +199 -0
  225. package/src/thread-controller.share.test.ts +472 -0
  226. package/src/thread-controller.tree.test.ts +255 -0
  227. package/src/thread-controller.ts +1509 -0
  228. package/src/thread-selection.test.ts +210 -0
  229. package/src/thread-selection.ts +243 -0
  230. package/src/tree-view.test.ts +128 -0
  231. package/src/tree-view.ts +148 -0
  232. package/src/use-annotation-surface.tsx +851 -0
  233. package/src/use-changes-workbench.ts +153 -0
  234. package/src/use-frame-measure.ts +38 -0
  235. package/src/use-settings-dialog.tsx +274 -0
  236. package/src/use-terminal-virtualizer.ts +233 -0
  237. package/src/version.ts +5 -0
  238. package/src/view-diff.test.ts +66 -9
  239. package/src/view-diff.ts +231 -23
  240. package/src/view-plan.regression.test.ts +60 -0
  241. package/src/view-plan.test.ts +406 -0
  242. package/src/view-plan.ts +570 -0
  243. package/src/walk.test.ts +121 -0
  244. package/src/walk.ts +74 -0
  245. package/src/syntax.test.ts +0 -41
  246. package/src/syntax.ts +0 -88
  247. package/src/view.test.ts +0 -157
  248. package/src/view.ts +0 -312
@@ -0,0 +1,26 @@
1
+ import React from "react";
2
+ import { DARK } from "../theme";
3
+ import type { Story, StoryMeta } from "./story";
4
+ import { TooltipProvider } from "./Tooltip";
5
+ import { IconButton } from "./primitives/IconButton";
6
+ import { NERD } from "./primitives/icons";
7
+
8
+ export const meta: StoryMeta = { title: "Layout/Tooltip" };
9
+
10
+ // The tooltip surfaces only on hover, so a static story shows the provider wrapping a labelled control.
11
+ export const HoverControl: Story = {
12
+ render: () => (
13
+ <TooltipProvider theme={DARK}>
14
+ <box style={{ flexDirection: "row", padding: 1, backgroundColor: DARK.panel }}>
15
+ <IconButton
16
+ glyph={NERD.sidebarRight}
17
+ tip="Toggle Right Sidebar"
18
+ onPress={() => {}}
19
+ theme={DARK}
20
+ />
21
+ </box>
22
+ </TooltipProvider>
23
+ ),
24
+ expectedColors: [DARK.textMuted],
25
+ size: { width: 40, height: 4 },
26
+ };
@@ -0,0 +1,72 @@
1
+ // Hover tooltips for the header controls. A tiny header cell clips its own children, so the label is
2
+ // surfaced at the screen root instead: controls report the hovered label and cursor cell, and this layer
3
+ // paints it one row below, nudged left when it would run past the right edge.
4
+
5
+ import React, { createContext, useContext, useMemo, useState } from "react";
6
+ import { useTerminalDimensions } from "@opentui/react";
7
+ import { DARK, type Theme } from "../theme";
8
+
9
+ interface TooltipTarget {
10
+ label: string;
11
+ x: number;
12
+ y: number;
13
+ }
14
+
15
+ interface TooltipApi {
16
+ showTooltip: (label: string, x: number, y: number) => void;
17
+ hideTooltip: () => void;
18
+ }
19
+
20
+ const TooltipContext = createContext<TooltipApi>({
21
+ showTooltip: () => {},
22
+ hideTooltip: () => {},
23
+ });
24
+
25
+ /** Report or clear the hovered control's tooltip; a no-op outside a TooltipProvider. */
26
+ export function useTooltip(): TooltipApi {
27
+ return useContext(TooltipContext);
28
+ }
29
+
30
+ /** Wrap a shell so its header controls can surface hover tooltips at the screen root. */
31
+ export function TooltipProvider({
32
+ theme,
33
+ children,
34
+ }: {
35
+ theme?: Theme;
36
+ children: React.ReactNode;
37
+ }): React.ReactNode {
38
+ const tokens = theme ?? DARK;
39
+ const { width, height } = useTerminalDimensions();
40
+ const [target, setTarget] = useState<TooltipTarget | null>(null);
41
+ const api = useMemo<TooltipApi>(
42
+ () => ({
43
+ showTooltip: (label, x, y) => setTarget({ label, x, y }),
44
+ hideTooltip: () => setTarget(null),
45
+ }),
46
+ [],
47
+ );
48
+
49
+ const boxWidth = target ? target.label.length + 2 : 0;
50
+ const left = target ? Math.max(0, Math.min(target.x, width - boxWidth)) : 0;
51
+ const top = target ? (target.y + 1 <= height - 1 ? target.y + 1 : Math.max(0, target.y - 1)) : 0;
52
+
53
+ return (
54
+ <TooltipContext.Provider value={api}>
55
+ {children}
56
+ {target ? (
57
+ <box
58
+ style={{
59
+ position: "absolute",
60
+ left,
61
+ top,
62
+ paddingLeft: 1,
63
+ paddingRight: 1,
64
+ backgroundColor: tokens.elevated,
65
+ }}
66
+ >
67
+ <text fg={tokens.text}>{target.label}</text>
68
+ </box>
69
+ ) : null}
70
+ </TooltipContext.Provider>
71
+ );
72
+ }
@@ -0,0 +1,106 @@
1
+ import React from "react";
2
+ import { DARK } from "../theme";
3
+ import type { Story, StoryMeta } from "./story";
4
+ import { TreePane } from "./TreePane";
5
+ import type { TreeRow } from "../tree-view";
6
+
7
+ export const meta: StoryMeta = { title: "Surfaces/TreePane" };
8
+
9
+ const callbacks = {
10
+ onSelect: () => {},
11
+ onGo: () => {},
12
+ onBranch: () => {},
13
+ onLabel: () => {},
14
+ onFork: () => {},
15
+ onForkAndShare: () => {},
16
+ };
17
+
18
+ /** rev1 -> comment (labelled, a branch hangs off it) -> the branch's comment -> rev2 as main's tip. */
19
+ const ROWS: TreeRow[] = [
20
+ {
21
+ entryId: "e1",
22
+ depth: 0,
23
+ glyph: "◉",
24
+ text: "revision 1",
25
+ onPath: true,
26
+ tips: [],
27
+ isCurrentTip: false,
28
+ },
29
+ {
30
+ entryId: "e2",
31
+ depth: 0,
32
+ glyph: "·",
33
+ text: "comment",
34
+ onPath: true,
35
+ tips: [],
36
+ isCurrentTip: false,
37
+ label: "start",
38
+ },
39
+ {
40
+ entryId: "e3",
41
+ depth: 1,
42
+ glyph: "·",
43
+ text: "comment",
44
+ onPath: false,
45
+ tips: ["alt"],
46
+ isCurrentTip: false,
47
+ },
48
+ {
49
+ entryId: "e4",
50
+ depth: 0,
51
+ glyph: "◉",
52
+ text: "revision 2",
53
+ onPath: true,
54
+ tips: ["main"],
55
+ isCurrentTip: true,
56
+ },
57
+ ];
58
+
59
+ /** The rail expects a height-constrained parent, like the app's main row. */
60
+ function PaneFrame({ children }: { children: React.ReactNode }): React.ReactNode {
61
+ return (
62
+ <box
63
+ style={{ flexDirection: "column", width: 40, height: "100%", backgroundColor: DARK.panel }}
64
+ >
65
+ {children}
66
+ </box>
67
+ );
68
+ }
69
+
70
+ export const OwnerWithBranch: Story = {
71
+ render: () => (
72
+ <PaneFrame>
73
+ <TreePane rows={ROWS} selectedEntryId="e2" canMove canFork {...callbacks} />
74
+ </PaneFrame>
75
+ ),
76
+ expectedColors: [DARK.accent],
77
+ size: { width: 40, height: 16 },
78
+ };
79
+
80
+ export const CollaboratorReadsOnly: Story = {
81
+ render: () => (
82
+ <PaneFrame>
83
+ <TreePane rows={ROWS} canMove={false} canFork={false} {...callbacks} />
84
+ </PaneFrame>
85
+ ),
86
+ size: { width: 40, height: 16 },
87
+ };
88
+
89
+ /** A resolved review: the owner can still fork, not move. */
90
+ export const ResolvedForkOnly: Story = {
91
+ render: () => (
92
+ <PaneFrame>
93
+ <TreePane rows={ROWS} canMove={false} canFork {...callbacks} />
94
+ </PaneFrame>
95
+ ),
96
+ size: { width: 40, height: 16 },
97
+ };
98
+
99
+ export const NoHistory: Story = {
100
+ render: () => (
101
+ <PaneFrame>
102
+ <TreePane rows={[]} canMove canFork {...callbacks} />
103
+ </PaneFrame>
104
+ ),
105
+ size: { width: 40, height: 8 },
106
+ };
@@ -0,0 +1,157 @@
1
+ /**
2
+ * The rail's Tree tab: the session's history as one row per entry. The active
3
+ * path reads bright, everything else dim; a branch tip carries its name, the
4
+ * current tip a marker, a checkpoint its label. Clicking a row selects it,
5
+ * clicking the selected row goes there; the buttons underneath are the mouse
6
+ * path to the same primitives the option chords reach.
7
+ */
8
+
9
+ import { ScrollArea } from "./ScrollArea";
10
+ import React, { useEffect, useRef } from "react";
11
+ import { createTextAttributes, type ScrollBoxRenderable } from "@opentui/core";
12
+ import type { Theme } from "../theme";
13
+ import type { TreeRow } from "../tree-view";
14
+ import { useComponentTheme } from "./theme-context";
15
+ import { Button } from "./primitives/Button";
16
+ import { Toolbar } from "./primitives/Toolbar";
17
+
18
+ export interface TreePaneProps {
19
+ rows: TreeRow[];
20
+ selectedEntryId?: string;
21
+ /** The owner of an open review may move the tree; everyone else reads it. */
22
+ canMove: boolean;
23
+ /** The owner may fork, a resolved review included. */
24
+ canFork: boolean;
25
+ onSelect: (entryId: string) => void;
26
+ onGo: (entryId: string) => void;
27
+ onBranch: () => void;
28
+ onLabel: () => void;
29
+ onFork: () => void;
30
+ onForkAndShare: () => void;
31
+ theme?: Theme;
32
+ }
33
+
34
+ const DIM = createTextAttributes({ dim: true });
35
+ const BOLD = createTextAttributes({ bold: true });
36
+
37
+ /** The marker column: the current tip stands out, every other row keeps its kind's glyph. */
38
+ function markerFor(row: TreeRow): string {
39
+ return row.isCurrentTip ? "●" : row.glyph;
40
+ }
41
+
42
+ /** What follows the entry's name: the branches whose tip it is, and its checkpoint. */
43
+ function trailerFor(row: TreeRow): string {
44
+ const parts: string[] = [];
45
+
46
+ if (row.tips.length) parts.push(`← ${row.tips.join(", ")}`);
47
+ if (row.label !== undefined) parts.push(`⚑ ${row.label}`);
48
+
49
+ return parts.length ? ` ${parts.join(" ")}` : "";
50
+ }
51
+
52
+ export function TreePane({
53
+ rows,
54
+ selectedEntryId,
55
+ canMove,
56
+ canFork,
57
+ onSelect,
58
+ onGo,
59
+ onBranch,
60
+ onLabel,
61
+ onFork,
62
+ onForkAndShare,
63
+ theme,
64
+ }: TreePaneProps): React.ReactNode {
65
+ const tokens = useComponentTheme(theme);
66
+ const scrollRef = useRef<ScrollBoxRenderable | null>(null);
67
+ const revealId = selectedEntryId ?? rows.find((row) => row.isCurrentTip)?.entryId;
68
+
69
+ // the trunk draws last, so a long history opens with its tip below the fold: keep the row in view
70
+ useEffect(() => {
71
+ if (revealId === undefined) return;
72
+ try {
73
+ scrollRef.current?.scrollChildIntoView(`tree-row-${revealId}`);
74
+ } catch {
75
+ // reveal is best-effort; the selection is already correct
76
+ }
77
+ }, [revealId]);
78
+
79
+ if (rows.length === 0)
80
+ return (
81
+ <box style={{ flexGrow: 1, alignItems: "center", justifyContent: "center" }}>
82
+ <text fg={tokens.textDim}>no history yet</text>
83
+ </box>
84
+ );
85
+
86
+ return (
87
+ <box style={{ flexGrow: 1, flexDirection: "column" }}>
88
+ <ScrollArea scrollRef={scrollRef}>
89
+ {rows.map((row) => {
90
+ const selected = row.entryId === selectedEntryId;
91
+ const foreground = row.onPath ? tokens.text : tokens.textDim;
92
+
93
+ return (
94
+ <box
95
+ key={row.entryId}
96
+ id={`tree-row-${row.entryId}`}
97
+ style={{
98
+ flexDirection: "row",
99
+ paddingLeft: 1 + row.depth * 2,
100
+ backgroundColor: selected ? tokens.elevated : undefined,
101
+ }}
102
+ onMouseUp={() => (selected && canMove ? onGo(row.entryId) : onSelect(row.entryId))}
103
+ >
104
+ <text
105
+ fg={row.isCurrentTip ? tokens.accent : foreground}
106
+ attributes={row.onPath ? undefined : DIM}
107
+ >
108
+ {markerFor(row)} {row.text}
109
+ </text>
110
+ <text fg={tokens.textMuted} attributes={row.label === undefined ? undefined : BOLD}>
111
+ {trailerFor(row)}
112
+ </text>
113
+ </box>
114
+ );
115
+ })}
116
+ </ScrollArea>
117
+ {canMove || canFork ? (
118
+ <box style={{ paddingLeft: 1 }}>
119
+ <Toolbar>
120
+ {canMove ? (
121
+ <Button
122
+ onPress={() => {
123
+ if (selectedEntryId !== undefined) onGo(selectedEntryId);
124
+ }}
125
+ isDisabled={selectedEntryId === undefined}
126
+ marginRight={1}
127
+ theme={theme}
128
+ >
129
+ Go
130
+ </Button>
131
+ ) : null}
132
+ {canMove ? (
133
+ <Button onPress={onBranch} marginRight={1} theme={theme}>
134
+ Branch
135
+ </Button>
136
+ ) : null}
137
+ {canMove ? (
138
+ <Button onPress={onLabel} marginRight={1} theme={theme}>
139
+ Label
140
+ </Button>
141
+ ) : null}
142
+ {canFork ? (
143
+ <Button onPress={onFork} marginRight={1} theme={theme}>
144
+ Fork
145
+ </Button>
146
+ ) : null}
147
+ {canFork ? (
148
+ <Button onPress={onForkAndShare} theme={theme}>
149
+ Fork+share
150
+ </Button>
151
+ ) : null}
152
+ </Toolbar>
153
+ </box>
154
+ ) : null}
155
+ </box>
156
+ );
157
+ }
@@ -0,0 +1,100 @@
1
+ import React from "react";
2
+ import { DARK } from "../theme";
3
+ import type { WalkFile } from "../walk";
4
+ import type { Story, StoryMeta } from "./story";
5
+ import { WalkWizard } from "./WalkWizard";
6
+
7
+ export const meta: StoryMeta = { title: "Overlays/WalkWizard" };
8
+
9
+ const FILES: WalkFile[] = [
10
+ {
11
+ path: "packages/daemon/src/store.ts",
12
+ added: 12,
13
+ removed: 3,
14
+ preview: [
15
+ { sign: "+", text: "const viewed = new Set<string>();" },
16
+ { sign: "+", text: "record.viewedPaths = [...viewed];" },
17
+ { sign: "-", text: "// viewed tracking lives in the client only" },
18
+ ],
19
+ },
20
+ {
21
+ path: "packages/client/src/walk.ts",
22
+ added: 48,
23
+ removed: 0,
24
+ preview: [{ sign: "+", text: "export function firstUnviewedIndex(files, viewed) {" }],
25
+ },
26
+ {
27
+ path: "packages/schema/src/types.ts",
28
+ added: 3,
29
+ removed: 0,
30
+ preview: [{ sign: "+", text: "viewedPaths?: string[];" }],
31
+ },
32
+ ];
33
+
34
+ const noop = (): void => {};
35
+
36
+ export const FirstFile: Story = {
37
+ render: () => (
38
+ <WalkWizard
39
+ files={FILES}
40
+ index={0}
41
+ viewedPaths={new Set()}
42
+ terminalWidth={80}
43
+ onSubmitRequest={noop}
44
+ onBack={noop}
45
+ />
46
+ ),
47
+ // the main card wears the accent border; stats carry the +/- colors
48
+ expectedColors: [
49
+ DARK.accent,
50
+ DARK.green,
51
+ DARK.red,
52
+ DARK.insertedForeground,
53
+ DARK.deletedForeground,
54
+ ],
55
+ };
56
+
57
+ export const WithAgentNote: Story = {
58
+ render: () => (
59
+ <WalkWizard
60
+ files={FILES}
61
+ index={1}
62
+ viewedPaths={new Set(["packages/daemon/src/store.ts"])}
63
+ note="New module: pure helpers that pick the next unviewed file. No IO, unit tested."
64
+ terminalWidth={80}
65
+ onSubmitRequest={noop}
66
+ onBack={noop}
67
+ />
68
+ ),
69
+ // the agent-note block wears the plain gray border token, not the accent
70
+ expectedColors: [DARK.accent, DARK.border, DARK.textMuted],
71
+ };
72
+
73
+ export const EndCard: Story = {
74
+ render: () => (
75
+ <WalkWizard
76
+ files={FILES}
77
+ index={3}
78
+ viewedPaths={new Set(FILES.map((file) => file.path))}
79
+ terminalWidth={80}
80
+ onSubmitRequest={noop}
81
+ onBack={noop}
82
+ />
83
+ ),
84
+ // the end card wears the green border and the solid submit button
85
+ expectedColors: [DARK.green, DARK.accent, DARK.accentInk],
86
+ };
87
+
88
+ export const EndCardPartialPass: Story = {
89
+ render: () => (
90
+ <WalkWizard
91
+ files={FILES}
92
+ index={3}
93
+ viewedPaths={new Set(["packages/daemon/src/store.ts"])}
94
+ terminalWidth={80}
95
+ onSubmitRequest={noop}
96
+ onBack={noop}
97
+ />
98
+ ),
99
+ expectedColors: [DARK.green],
100
+ };
@@ -0,0 +1,152 @@
1
+ /**
2
+ * The guided walk's focused card wizard: one card per changed file
3
+ * floating over the dimmed diff, with a plain step count in the title - no
4
+ * progress bar. Below the main card a separate gray-bordered block carries
5
+ * the submitting agent's note for the file, when one exists. Past the last
6
+ * file the end card offers the submit action directly with honest counts.
7
+ * The keys live in the keymap's walk overlay; this component only renders.
8
+ */
9
+
10
+ import React from "react";
11
+ import type { Theme } from "../theme";
12
+ import { useComponentTheme } from "./theme-context";
13
+ import type { WalkFile } from "../walk";
14
+ import { viewedCount } from "../walk";
15
+ import { Card } from "./primitives/Card";
16
+ import { Button } from "./primitives/Button";
17
+ import { Toolbar } from "./primitives/Toolbar";
18
+
19
+ export interface WalkWizardProps {
20
+ files: WalkFile[];
21
+ /** The wizard step; index === files.length renders the end card. */
22
+ index: number;
23
+ viewedPaths: ReadonlySet<string>;
24
+ /** The agent's note for the current file; the gray block renders only when set. */
25
+ note?: string;
26
+ /** Terminal width bounds the card width. */
27
+ terminalWidth: number;
28
+ /** End-card submit: leaves the walk and opens the rail confirm. */
29
+ onSubmitRequest: () => void;
30
+ onBack: () => void;
31
+ theme?: Theme;
32
+ }
33
+
34
+ /** The card never crowds the terminal edge nor stretches past reading width. */
35
+ function wizardWidth(terminalWidth: number): number {
36
+ return Math.min(72, Math.max(24, terminalWidth - 8));
37
+ }
38
+
39
+ /** Note rows from a crude wrap estimate, capped so the block stays a block. */
40
+ function noteRows(note: string, innerWidth: number): number {
41
+ return Math.min(3, Math.max(1, Math.ceil(note.length / Math.max(1, innerWidth))));
42
+ }
43
+
44
+ export function WalkWizard({
45
+ files,
46
+ index,
47
+ viewedPaths,
48
+ note,
49
+ terminalWidth,
50
+ onSubmitRequest,
51
+ onBack,
52
+ theme,
53
+ }: WalkWizardProps): React.ReactNode {
54
+ const tokens = useComponentTheme(theme);
55
+ const width = wizardWidth(terminalWidth);
56
+ const viewed = viewedCount(files, viewedPaths);
57
+ const atEnd = index >= files.length;
58
+ const currentFile = files[index];
59
+
60
+ return (
61
+ <box
62
+ style={{
63
+ position: "absolute",
64
+ left: 0,
65
+ top: 0,
66
+ width: "100%",
67
+ height: "100%",
68
+ alignItems: "center",
69
+ justifyContent: "center",
70
+ flexDirection: "column",
71
+ }}
72
+ >
73
+ <box style={{ width, flexDirection: "column" }}>
74
+ {atEnd || !currentFile ? (
75
+ <Card
76
+ title=" walk complete "
77
+ contentRows={3}
78
+ borderColor={tokens.green}
79
+ backgroundColor={tokens.elevated}
80
+ theme={theme}
81
+ >
82
+ <text fg={tokens.text}>
83
+ {viewed === files.length
84
+ ? `every file viewed (${viewed}/${files.length})`
85
+ : `${viewed} of ${files.length} files viewed`}
86
+ </text>
87
+ <box style={{ height: 1 }} />
88
+ <Toolbar>
89
+ <Button variant="solid" marginRight={2} onPress={onSubmitRequest} theme={theme}>
90
+ {" Submit review "}
91
+ </Button>
92
+ <Button onPress={onBack} theme={theme}>
93
+ {" [ back "}
94
+ </Button>
95
+ </Toolbar>
96
+ </Card>
97
+ ) : (
98
+ <>
99
+ <Card
100
+ title={` file ${index + 1} of ${files.length} · ${viewed} viewed `}
101
+ contentRows={3 + Math.max(1, currentFile.preview.length)}
102
+ borderColor={tokens.accent}
103
+ backgroundColor={tokens.elevated}
104
+ theme={theme}
105
+ >
106
+ <text fg={tokens.text}>{currentFile.path}</text>
107
+ <text>
108
+ <span fg={tokens.green}>{`+${currentFile.added}`}</span>
109
+ <span fg={tokens.textDim}> </span>
110
+ <span fg={tokens.red}>{`-${currentFile.removed}`}</span>
111
+ <span fg={tokens.textDim}>
112
+ {viewedPaths.has(currentFile.path) ? " · viewed" : ""}
113
+ </span>
114
+ </text>
115
+ <box style={{ height: 1 }} />
116
+ {currentFile.preview.length === 0 ? (
117
+ <text fg={tokens.textDim}>(no changed lines)</text>
118
+ ) : (
119
+ currentFile.preview.map((previewLine, previewIndex) => (
120
+ <text
121
+ key={previewIndex}
122
+ style={{ wrapMode: "none" }}
123
+ fg={
124
+ previewLine.sign === "+"
125
+ ? tokens.insertedForeground
126
+ : tokens.deletedForeground
127
+ }
128
+ >
129
+ {previewLine.sign + previewLine.text}
130
+ </text>
131
+ ))
132
+ )}
133
+ </Card>
134
+ {note !== undefined ? (
135
+ <box style={{ marginTop: 1, flexDirection: "column" }}>
136
+ <Card
137
+ title=" agent note "
138
+ contentRows={noteRows(note, width - 4)}
139
+ borderColor={tokens.border}
140
+ backgroundColor={tokens.elevated}
141
+ theme={theme}
142
+ >
143
+ <text fg={tokens.textMuted}>{note}</text>
144
+ </Card>
145
+ </box>
146
+ ) : null}
147
+ </>
148
+ )}
149
+ </box>
150
+ </box>
151
+ );
152
+ }
@@ -0,0 +1,12 @@
1
+ import React from "react";
2
+ import { DARK } from "../theme";
3
+ import type { Story, StoryMeta } from "./story";
4
+ import { WelcomePlayground } from "./WelcomePlayground";
5
+
6
+ export const meta: StoryMeta = { title: "Surfaces/WelcomePlayground" };
7
+
8
+ export const Welcome: Story = {
9
+ render: () => <WelcomePlayground version="0.1.0-alpha.66" quickActions={[]} theme={DARK} />,
10
+ expectedColors: [DARK.textDim],
11
+ size: { width: 72, height: 20 },
12
+ };
@@ -0,0 +1,47 @@
1
+ /**
2
+ * The welcome page is a real annotation surface, but ephemeral: selecting its copy and typing leaves
3
+ * a comment that shows inline, exactly as a file does, and it lives only in the component - there is
4
+ * no controller or daemon behind it, so nothing is persisted. The copy prompts the two first gestures.
5
+ */
6
+
7
+ import { describe, expect, test } from "bun:test";
8
+ import { testRender } from "@opentui/react/test-utils";
9
+ import React from "react";
10
+ import { WelcomePlayground } from "./WelcomePlayground";
11
+ import { DARK } from "../theme";
12
+ import { dragText, typeText, pressKey, waitForText } from "../test-support";
13
+
14
+ describe("WelcomePlayground", () => {
15
+ test("the copy prompts the select-and-type and the slash gestures", async () => {
16
+ const setup = await testRender(
17
+ <WelcomePlayground version="1.2.3" quickActions={[]} theme={DARK} />,
18
+ { width: 80, height: 24 },
19
+ );
20
+
21
+ await waitForText(setup, "Getting started");
22
+ const frame = setup.captureCharFrame();
23
+
24
+ expect(frame).toContain("start typing to leave your first comment");
25
+ expect(frame).toContain('type "/"');
26
+
27
+ setup.renderer.destroy();
28
+ });
29
+
30
+ test("selecting the copy and typing leaves a comment inline, exactly like a file", async () => {
31
+ const setup = await testRender(
32
+ <WelcomePlayground version="1.2.3" quickActions={[]} theme={DARK} />,
33
+ { width: 80, height: 24 },
34
+ );
35
+
36
+ // Act - select the practice line, type a comment, send it
37
+ await waitForText(setup, "quick brown fox");
38
+ await dragText(setup, "quick brown fox", "quick brown fox", "quick brown fox".length);
39
+ await typeText(setup, "my first note");
40
+ await pressKey(setup, "RETURN", { meta: true });
41
+
42
+ // Assert - the note renders on the surface, no controller or daemon involved
43
+ await waitForText(setup, "my first note");
44
+
45
+ setup.renderer.destroy();
46
+ });
47
+ });