@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,54 @@
1
+ import React from "react";
2
+ import type { Annotation } from "@cueloop/schema";
3
+ import { DARK } from "../theme";
4
+ import type { Discussion } from "../discussions";
5
+ import type { TextSpan } from "../thread-selection";
6
+ import { RootOverlayProvider } from "./RootOverlay";
7
+ import { DiscussionMarkerRail } from "./DiscussionMarkerRail";
8
+ import type { Story, StoryMeta } from "./story";
9
+
10
+ export const meta: StoryMeta = { title: "Chrome/DiscussionMarkerRail" };
11
+
12
+ function comment(id: string, body: string): Annotation {
13
+ return {
14
+ id,
15
+ kind: "comment",
16
+ anchor: { quote: body, prefix: "", suffix: "" },
17
+ body,
18
+ createdAt: "2026-09-08T00:00:00.000Z",
19
+ };
20
+ }
21
+
22
+ function discussion(row: number, body: string): Discussion {
23
+ const span: TextSpan = { start: { blockIndex: row, char: 0 }, end: { blockIndex: row, char: 4 } };
24
+
25
+ return {
26
+ key: `${row}`,
27
+ rootId: `a${row}`,
28
+ blockIndex: row,
29
+ span,
30
+ annotations: [comment(`a${row}`, body)],
31
+ };
32
+ }
33
+
34
+ const discussions = [
35
+ discussion(2, "Map needs an eviction story."),
36
+ discussion(5, "why a Map here?"),
37
+ discussion(9, "name this constant"),
38
+ ];
39
+
40
+ // The centered dot column: one dot per discussion, drawn on a surface's right edge.
41
+ export const Dots: Story = {
42
+ render: () => (
43
+ <RootOverlayProvider>
44
+ <box
45
+ style={{ width: 24, height: 10, flexDirection: "row", backgroundColor: DARK.background }}
46
+ >
47
+ <box style={{ flexGrow: 1 }} />
48
+ <DiscussionMarkerRail discussions={discussions} spanQuote={() => "x"} onJump={() => {}} />
49
+ </box>
50
+ </RootOverlayProvider>
51
+ ),
52
+ expectedColors: [DARK.textDim],
53
+ size: { width: 24, height: 10 },
54
+ };
@@ -0,0 +1,186 @@
1
+ /**
2
+ * The right-edge discussion navigator: a vertically-centered column of dots, one
3
+ * per discussion, that highlights on hover, shows a floating preview, and jumps
4
+ * on click. The plan thread view and each diff editor group render one on their
5
+ * right edge, so comment navigation reads the same on prose and on code. The
6
+ * preview draws at the app root (OpenTUI has no z-index) so no pane border slices it.
7
+ */
8
+
9
+ import React, { useEffect, useRef, useState, type RefObject } from "react";
10
+ import type { BoxRenderable, ScrollBoxRenderable } from "@opentui/core";
11
+ import type { Theme } from "../theme";
12
+ import type { TextSpan } from "../thread-selection";
13
+ import type { Discussion } from "../discussions";
14
+ import { useFrameMeasure } from "../use-frame-measure";
15
+ import { useComponentTheme } from "./theme-context";
16
+ import { useRootOverlay } from "./RootOverlay";
17
+ import { OverlayScrollbar } from "./OverlayScrollbar";
18
+
19
+ interface HoveredMarker {
20
+ key: string;
21
+ screenY: number;
22
+ anchorX: number;
23
+ }
24
+
25
+ function ScrollMarkers({
26
+ discussions,
27
+ hovered,
28
+ focused,
29
+ tokens,
30
+ onHover,
31
+ onJump,
32
+ }: {
33
+ discussions: Discussion[];
34
+ hovered: string | null;
35
+ focused: string | null;
36
+ tokens: Theme;
37
+ onHover: (marker: HoveredMarker | null) => void;
38
+ onJump: (key: string) => void;
39
+ }): React.ReactNode {
40
+ const railRef = useRef<BoxRenderable | null>(null);
41
+ const height = useFrameMeasure(
42
+ () => railRef.current?.height ?? 0,
43
+ (left, right) => left === right,
44
+ 0,
45
+ );
46
+
47
+ const firstRow = Math.max(0, Math.floor((height - discussions.length) / 2));
48
+ const rowFor = (index: number): number => firstRow + index;
49
+ const indexAt = (row: number): number | null => {
50
+ const index = row - firstRow;
51
+
52
+ return index >= 0 && index < discussions.length ? index : null;
53
+ };
54
+ const hoveredIndex = discussions.findIndex((discussion) => discussion.key === hovered);
55
+ const focusedIndex = discussions.findIndex((discussion) => discussion.key === focused);
56
+ const isActive = (index: number): boolean => index === hoveredIndex || index === focusedIndex;
57
+ const colorFor = (index: number): string => {
58
+ if (isActive(index)) return tokens.text;
59
+ if (hoveredIndex >= 0 && discussions.length >= 5 && Math.abs(index - hoveredIndex) === 1) {
60
+ return tokens.textMuted;
61
+ }
62
+
63
+ return tokens.textDim;
64
+ };
65
+
66
+ return (
67
+ <box
68
+ ref={railRef}
69
+ style={{ width: 3, flexShrink: 0, flexDirection: "column" }}
70
+ onMouseMove={(event) => {
71
+ const railY = railRef.current?.y ?? 0;
72
+ const index = indexAt(event.y - railY);
73
+
74
+ onHover(
75
+ index === null
76
+ ? null
77
+ : {
78
+ key: discussions[index]!.key,
79
+ // screen coordinates so the preview can render at the app root, above every pane rule
80
+ screenY: railY + rowFor(index),
81
+ anchorX: railRef.current?.x ?? 0,
82
+ },
83
+ );
84
+ }}
85
+ onMouseOut={() => onHover(null)}
86
+ onMouseDown={(event) => {
87
+ const index = indexAt(event.y - (railRef.current?.y ?? 0));
88
+
89
+ if (index !== null) onJump(discussions[index]!.key);
90
+ }}
91
+ >
92
+ {discussions.map((discussion, index) => (
93
+ <text
94
+ key={discussion.key}
95
+ selectable={false}
96
+ style={{ position: "absolute", top: rowFor(index), left: 0 }}
97
+ fg={colorFor(index)}
98
+ >
99
+ {(isActive(index) ? "●" : "○").padStart(2)}
100
+ </text>
101
+ ))}
102
+ </box>
103
+ );
104
+ }
105
+
106
+ export interface DiscussionMarkerRailProps {
107
+ discussions: Discussion[];
108
+ spanQuote: (span: TextSpan) => string;
109
+ focusedKey?: string | null;
110
+ onJump: (key: string) => void;
111
+ scrollbox?: RefObject<ScrollBoxRenderable | null>;
112
+ theme?: Theme;
113
+ }
114
+
115
+ export function DiscussionMarkerRail({
116
+ discussions,
117
+ spanQuote,
118
+ focusedKey = null,
119
+ onJump,
120
+ scrollbox,
121
+ theme,
122
+ }: DiscussionMarkerRailProps): React.ReactNode {
123
+ const tokens = useComponentTheme(theme);
124
+ const [hovered, setHovered] = useState<HoveredMarker | null>(null);
125
+ const { setOverlay, clearOverlay } = useRootOverlay();
126
+
127
+ const preview = (): React.ReactNode => {
128
+ if (hovered === null) return null;
129
+ const discussion = discussions.find((candidate) => candidate.key === hovered.key);
130
+
131
+ if (!discussion) return null;
132
+ const quote = spanQuote(discussion.span);
133
+ const lastComment = discussion.annotations.at(-1)!;
134
+
135
+ return (
136
+ <box
137
+ style={{
138
+ position: "absolute",
139
+ top: Math.max(0, hovered.screenY - 1),
140
+ left: Math.max(0, hovered.anchorX - 48),
141
+ width: 48,
142
+ flexDirection: "column",
143
+ border: true,
144
+ borderStyle: "single",
145
+ borderColor: tokens.border,
146
+ backgroundColor: tokens.elevated,
147
+ paddingLeft: 1,
148
+ paddingRight: 1,
149
+ }}
150
+ >
151
+ <text fg={tokens.textDim}>{`"${quote.slice(0, 42)}"`}</text>
152
+ <text fg={tokens.text}>
153
+ {`${lastComment.author === undefined ? "●" : "○"} ${lastComment.body}`.slice(0, 44)}
154
+ </text>
155
+ <text fg={tokens.textDim}>
156
+ {`${discussion.annotations.length} comment${
157
+ discussion.annotations.length === 1 ? "" : "s"
158
+ } · click to jump`}
159
+ </text>
160
+ </box>
161
+ );
162
+ };
163
+
164
+ // render the preview at the app root (above every pane rule); OpenTUI has no z-index, so an
165
+ // absolute box inside this pane would be sliced by the next pane's border
166
+ useEffect(() => {
167
+ setOverlay("discussion-preview", preview());
168
+
169
+ return () => clearOverlay("discussion-preview");
170
+ // eslint-disable-next-line react-hooks/exhaustive-deps
171
+ }, [hovered, discussions, tokens]);
172
+
173
+ return (
174
+ <>
175
+ <ScrollMarkers
176
+ discussions={discussions}
177
+ hovered={hovered?.key ?? null}
178
+ focused={focusedKey}
179
+ tokens={tokens}
180
+ onHover={setHovered}
181
+ onJump={onJump}
182
+ />
183
+ {scrollbox ? <OverlayScrollbar scrollbox={scrollbox} theme={theme} /> : null}
184
+ </>
185
+ );
186
+ }
@@ -0,0 +1,59 @@
1
+ import React from "react";
2
+ import { DARK } from "../theme";
3
+ import type { Story, StoryMeta } from "./story";
4
+ import { EditorGrid } from "./EditorGrid";
5
+ import { changesTab, fileTab, makeGroup, splitGroup, type EditorTab } from "./editor-grid";
6
+
7
+ export const meta: StoryMeta = { title: "Surfaces/EditorGrid" };
8
+
9
+ const noop = (): void => {};
10
+
11
+ function renderTab(tab: EditorTab): React.ReactNode {
12
+ return (
13
+ <box style={{ flexDirection: "column", paddingLeft: 1, paddingTop: 1 }}>
14
+ <text fg={DARK.textMuted}>{tab.kind === "changes" ? "the whole diff" : tab.path}</text>
15
+ </box>
16
+ );
17
+ }
18
+
19
+ const oneGroup = makeGroup([changesTab(), fileTab("App.tsx", "src/App.tsx", "diff")]);
20
+ const sourceGroup = makeGroup([fileTab("plugin.json", ".claude-plugin/plugin.json", "contents")]);
21
+ const splitTree = splitGroup(sourceGroup, sourceGroup.id, "right").tree;
22
+
23
+ export const OneGroup: Story = {
24
+ render: () => (
25
+ <EditorGrid
26
+ tree={oneGroup}
27
+ focusedGroupId={oneGroup.id}
28
+ onFocusGroup={noop}
29
+ onActivateTab={noop}
30
+ onCloseTab={noop}
31
+ onSplit={noop}
32
+ onZoom={noop}
33
+ zoomed={false}
34
+ renderTab={renderTab}
35
+ theme={DARK}
36
+ />
37
+ ),
38
+ expectedColors: [DARK.textMuted, DARK.border],
39
+ size: { width: 80, height: 16 },
40
+ };
41
+
42
+ export const SplitGroups: Story = {
43
+ render: () => (
44
+ <EditorGrid
45
+ tree={splitTree}
46
+ focusedGroupId={null}
47
+ onFocusGroup={noop}
48
+ onActivateTab={noop}
49
+ onCloseTab={noop}
50
+ onSplit={noop}
51
+ onZoom={noop}
52
+ zoomed={false}
53
+ renderTab={renderTab}
54
+ theme={DARK}
55
+ />
56
+ ),
57
+ expectedColors: [DARK.textMuted, DARK.border],
58
+ size: { width: 100, height: 16 },
59
+ };
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Regression: file tabs keep the width of their name and the strip scrolls horizontally
3
+ * behind the header controls when the row overflows, revealing the active tab - instead of
4
+ * squeezing every tab and truncating its label.
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 { EditorGrid, visibleTabWindow } from "./EditorGrid";
11
+ import { changesTab, fileTab, makeGroup, type EditorTab } from "./editor-grid";
12
+ import { NERD } from "./primitives/icons";
13
+ import { allowEventLoopUpdates } from "../test-support";
14
+
15
+ describe("visibleTabWindow", () => {
16
+ const tabs: EditorTab[] = ["aaaa", "bbbb", "cccc", "dddd", "eeee"].map((name) =>
17
+ fileTab(name, name, "diff"),
18
+ );
19
+
20
+ test("an unmeasured strip shows every tab", () => {
21
+ // Arrange / Act / Assert - width 0 means the header clips until it is measured
22
+ expect(visibleTabWindow(tabs, 0, 0, 0)).toEqual({ first: 0, end: tabs.length });
23
+ });
24
+
25
+ test("only the run that fits renders, and the active tab pulls the window to it", () => {
26
+ // Arrange - each tab is 9 cells (4 label + 5 chrome); a 30-cell strip fits about three
27
+ // Act - the last tab is active
28
+ const window = visibleTabWindow(tabs, 0, tabs.length - 1, 30);
29
+
30
+ // Assert - the window ends on the active tab and dropped earlier tabs
31
+ expect(window.end).toBe(tabs.length);
32
+ expect(window.first).toBeGreaterThan(0);
33
+ });
34
+
35
+ test("a tab wider than the strip still shows on its own", () => {
36
+ // Arrange
37
+ const wide = [fileTab("a-very-long-file-name.tsx", "x", "diff")];
38
+
39
+ // Act / Assert
40
+ expect(visibleTabWindow(wide, 0, 0, 10)).toEqual({ first: 0, end: 1 });
41
+ });
42
+
43
+ test("the comment badge counts toward a tab's width, so a commented run does not overflow", () => {
44
+ // Arrange - three 9-cell tabs fit a 27-cell strip exactly with no badge
45
+ const three = ["aaaa", "bbbb", "cccc"].map((name) => fileTab(name, name, "diff"));
46
+
47
+ // Act - without badges all three fit; a ` ● 2` badge on each widens them past the strip
48
+ const bare = visibleTabWindow(three, 0, 0, 27);
49
+ const badged = visibleTabWindow(three, 0, 0, 27, () => 2);
50
+
51
+ // Assert - the badge width narrows the window rather than clipping the last tab
52
+ expect(bare.end).toBe(3);
53
+ expect(badged.end).toBeLessThan(3);
54
+ });
55
+ });
56
+
57
+ const NAMES = [
58
+ "AppShell.tsx",
59
+ "DiffContentView.tsx",
60
+ "IconButton.tsx",
61
+ "EditorGrid.zoom.test.tsx",
62
+ "ThreadView.tsx",
63
+ ];
64
+
65
+ async function headerRow(activeIndex: number): Promise<string> {
66
+ const group = makeGroup([
67
+ changesTab(),
68
+ ...NAMES.map((name) => fileTab(name, `src/${name}`, "diff")),
69
+ ]);
70
+ const tree = { ...group, activeTabId: group.tabs[activeIndex]!.id };
71
+ const setup = await testRender(
72
+ <EditorGrid
73
+ tree={tree}
74
+ focusedGroupId={tree.id}
75
+ onFocusGroup={() => {}}
76
+ onActivateTab={() => {}}
77
+ onCloseTab={() => {}}
78
+ onSplit={() => {}}
79
+ onZoom={() => {}}
80
+ zoomed={false}
81
+ renderTab={() => <text>body</text>}
82
+ />,
83
+ { width: 70, height: 4 },
84
+ );
85
+
86
+ allowEventLoopUpdates();
87
+ await setup.waitForVisualIdle();
88
+ // the strip reveals the active tab on the frame after layout
89
+ await setup.waitForVisualIdle();
90
+ const row = setup.captureCharFrame().split("\n")[0]!;
91
+
92
+ setup.renderer.destroy();
93
+
94
+ return row;
95
+ }
96
+
97
+ describe("editor tab strip", () => {
98
+ test("tabs keep their full names and the controls stay pinned when the row overflows", async () => {
99
+ // Act
100
+ const row = await headerRow(0);
101
+
102
+ // Assert - no squeezed labels: every visible tab reads in full up to the clip edge
103
+ expect(row).toContain("Changes ");
104
+ expect(row).toContain("AppShell.tsx ");
105
+ expect(row).toContain("DiffContentView.tsx ");
106
+ // and the header controls sit at the right edge, past the clipped strip (search is out for now)
107
+ expect(row.trimEnd().endsWith(NERD.zoom)).toBe(true);
108
+ });
109
+
110
+ test("the strip scrolls to reveal the active tab", async () => {
111
+ // Act
112
+ const row = await headerRow(NAMES.length);
113
+
114
+ // Assert - the last tab is on screen, the first has scrolled out behind the left edge
115
+ expect(row).toContain("ThreadView.tsx ");
116
+ expect(row).not.toContain("Changes ");
117
+ });
118
+ });