@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,19 @@
1
+ import React from "react";
2
+ import { DARK } from "../theme";
3
+ import type { Story, StoryMeta } from "./story";
4
+ import { Toast } from "./Toast";
5
+
6
+ export const meta: StoryMeta = { title: "Overlays/Toast" };
7
+
8
+ export const ShareLink: Story = {
9
+ render: () => <Toast title="share link copied" body="ssh review-4f2a@cueloop.dev" />,
10
+ expectedColors: [DARK.accent],
11
+ size: { width: 60, height: 12 },
12
+ };
13
+
14
+ export const LongMessage: Story = {
15
+ render: () => (
16
+ <Toast body="could not stop sharing: gateway revoke failed - the share may already be gone, try again in a moment" />
17
+ ),
18
+ size: { width: 90, height: 14 },
19
+ };
@@ -0,0 +1,52 @@
1
+ import React from "react";
2
+ import { useTerminalDimensions } from "@opentui/react";
3
+ import type { Theme } from "../theme";
4
+ import { useComponentTheme } from "./theme-context";
5
+ import { Dialog } from "./primitives/Dialog";
6
+
7
+ export interface ToastProps {
8
+ title?: string;
9
+ body: string;
10
+ theme?: Theme;
11
+ }
12
+
13
+ /** A toast never spans the screen: a long message wraps inside this, like the settings dialog. */
14
+ const TOAST_MAX_WIDTH = 64;
15
+
16
+ export function Toast({ title, body, theme }: ToastProps): React.ReactNode {
17
+ const tokens = useComponentTheme(theme);
18
+ const { width: terminalWidth, height: terminalHeight } = useTerminalDimensions();
19
+ const width = Math.min(
20
+ terminalWidth - 6,
21
+ Math.max(28, Math.min(TOAST_MAX_WIDTH, body.length + 4)),
22
+ );
23
+ // the message wraps at the content width, so grow the box to fit the wrapped lines
24
+ const contentWidth = Math.max(1, width - 4);
25
+ const lines = body
26
+ .split("\n")
27
+ .reduce((total, line) => total + Math.max(1, Math.ceil(line.length / contentWidth)), 0);
28
+ const height = Math.min(terminalHeight - 4, Math.max(6, lines + 4));
29
+
30
+ return (
31
+ <Dialog
32
+ isOpen
33
+ title={title ? ` ${title} ` : undefined}
34
+ width={width}
35
+ height={height}
36
+ background={tokens.elevated}
37
+ theme={theme}
38
+ >
39
+ <box
40
+ style={{
41
+ flexDirection: "column",
42
+ flexGrow: 1,
43
+ paddingLeft: 1,
44
+ paddingRight: 1,
45
+ paddingTop: 1,
46
+ }}
47
+ >
48
+ <text fg={tokens.text}>{body}</text>
49
+ </box>
50
+ </Dialog>
51
+ );
52
+ }
@@ -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
+ // accent border and solid submit; the all-viewed line turns green
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.accent, DARK.accentInk],
100
+ };
@@ -0,0 +1,150 @@
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 { DialogActions } from "./primitives/DialogActions";
17
+
18
+ export interface WalkWizardProps {
19
+ files: WalkFile[];
20
+ /** The wizard step; index === files.length renders the end card. */
21
+ index: number;
22
+ viewedPaths: ReadonlySet<string>;
23
+ /** The agent's note for the current file; the gray block renders only when set. */
24
+ note?: string;
25
+ /** Terminal width bounds the card width. */
26
+ terminalWidth: number;
27
+ /** End-card submit: leaves the walk and opens the rail confirm. */
28
+ onSubmitRequest: () => void;
29
+ onBack: () => void;
30
+ theme?: Theme;
31
+ }
32
+
33
+ /** The card never crowds the terminal edge nor stretches past reading width. */
34
+ function wizardWidth(terminalWidth: number): number {
35
+ return Math.min(72, Math.max(24, terminalWidth - 8));
36
+ }
37
+
38
+ /** Note rows from a crude wrap estimate, capped so the block stays a block. */
39
+ function noteRows(note: string, innerWidth: number): number {
40
+ return Math.min(3, Math.max(1, Math.ceil(note.length / Math.max(1, innerWidth))));
41
+ }
42
+
43
+ export function WalkWizard({
44
+ files,
45
+ index,
46
+ viewedPaths,
47
+ note,
48
+ terminalWidth,
49
+ onSubmitRequest,
50
+ onBack,
51
+ theme,
52
+ }: WalkWizardProps): React.ReactNode {
53
+ const tokens = useComponentTheme(theme);
54
+ const width = wizardWidth(terminalWidth);
55
+ const viewed = viewedCount(files, viewedPaths);
56
+ const atEnd = index >= files.length;
57
+ const currentFile = files[index];
58
+
59
+ return (
60
+ <box
61
+ style={{
62
+ position: "absolute",
63
+ left: 0,
64
+ top: 0,
65
+ width: "100%",
66
+ height: "100%",
67
+ alignItems: "center",
68
+ justifyContent: "center",
69
+ flexDirection: "column",
70
+ }}
71
+ >
72
+ <box style={{ width, flexDirection: "column" }}>
73
+ {atEnd || !currentFile ? (
74
+ <Card
75
+ title=" walk complete "
76
+ contentRows={3}
77
+ borderColor={tokens.accent}
78
+ backgroundColor={tokens.elevated}
79
+ theme={theme}
80
+ >
81
+ <text fg={viewed === files.length ? tokens.green : tokens.text}>
82
+ {viewed === files.length
83
+ ? `every file viewed (${viewed}/${files.length})`
84
+ : `${viewed} of ${files.length} files viewed`}
85
+ </text>
86
+ <box style={{ height: 1 }} />
87
+ <DialogActions
88
+ confirmLabel="submit"
89
+ cancelLabel="back"
90
+ onConfirm={onSubmitRequest}
91
+ onCancel={onBack}
92
+ theme={theme}
93
+ />
94
+ </Card>
95
+ ) : (
96
+ <>
97
+ <Card
98
+ title={` file ${index + 1} of ${files.length} · ${viewed} viewed `}
99
+ contentRows={3 + Math.max(1, currentFile.preview.length)}
100
+ borderColor={tokens.accent}
101
+ backgroundColor={tokens.elevated}
102
+ theme={theme}
103
+ >
104
+ <text fg={tokens.text}>{currentFile.path}</text>
105
+ <text>
106
+ <span fg={tokens.green}>{`+${currentFile.added}`}</span>
107
+ <span fg={tokens.textDim}> </span>
108
+ <span fg={tokens.red}>{`-${currentFile.removed}`}</span>
109
+ <span fg={tokens.textDim}>
110
+ {viewedPaths.has(currentFile.path) ? " · viewed" : ""}
111
+ </span>
112
+ </text>
113
+ <box style={{ height: 1 }} />
114
+ {currentFile.preview.length === 0 ? (
115
+ <text fg={tokens.textDim}>(no changed lines)</text>
116
+ ) : (
117
+ currentFile.preview.map((previewLine, previewIndex) => (
118
+ <text
119
+ key={previewIndex}
120
+ style={{ wrapMode: "none" }}
121
+ fg={
122
+ previewLine.sign === "+"
123
+ ? tokens.insertedForeground
124
+ : tokens.deletedForeground
125
+ }
126
+ >
127
+ {previewLine.sign + previewLine.text}
128
+ </text>
129
+ ))
130
+ )}
131
+ </Card>
132
+ {note !== undefined ? (
133
+ <box style={{ marginTop: 1, flexDirection: "column" }}>
134
+ <Card
135
+ title=" agent note "
136
+ contentRows={noteRows(note, width - 4)}
137
+ borderColor={tokens.border}
138
+ backgroundColor={tokens.elevated}
139
+ theme={theme}
140
+ >
141
+ <text fg={tokens.textMuted}>{note}</text>
142
+ </Card>
143
+ </box>
144
+ ) : null}
145
+ </>
146
+ )}
147
+ </box>
148
+ </box>
149
+ );
150
+ }
@@ -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 quickActions={[]} theme={DARK} />,
10
+ expectedColors: [DARK.textDim],
11
+ size: { width: 72, height: 20 },
12
+ };