@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.
- package/native/build-pty.sh +51 -0
- package/native/build.sh +82 -0
- package/native/darwin-arm64/libcuelooppty.dylib +0 -0
- package/native/darwin-arm64/libcueloopvt.dylib +0 -0
- package/native/src/pty.zig +193 -0
- package/native/src/shim.zig +114 -0
- package/package.json +42 -34
- package/src/App.appearance.test.tsx +53 -0
- package/src/App.autoclose.test.tsx +77 -51
- package/src/App.diff.test.tsx +259 -45
- package/src/App.export.test.tsx +58 -41
- package/src/App.focus.test.ts +89 -0
- package/src/App.identity.test.tsx +78 -0
- package/src/App.plan-render.test.tsx +209 -0
- package/src/App.plan-review.test.tsx +330 -161
- package/src/App.prototype-markdown.test.tsx +128 -0
- package/src/App.prototype.test.tsx +176 -0
- package/src/App.readonly.test.tsx +129 -67
- package/src/App.regression.test.tsx +94 -0
- package/src/App.rendering.test.tsx +61 -35
- package/src/App.share-dialog.test.tsx +172 -0
- package/src/App.submit-confirm.test.tsx +264 -0
- package/src/App.test.tsx +402 -126
- package/src/App.toast.test.tsx +98 -0
- package/src/App.tsx +1406 -1060
- package/src/App.visual.test.tsx +57 -0
- package/src/App.walk.test.tsx +299 -0
- package/src/App.workbench.test.tsx +787 -0
- package/src/__snapshots__/view-plan.regression.test.ts.snap +100 -0
- package/src/annotation-palette.ts +62 -0
- package/src/ansi-screen.test.ts +28 -0
- package/src/ansi-screen.ts +72 -0
- package/src/app-key-state.ts +61 -0
- package/src/app-screens.tsx +577 -0
- package/src/app-view-model.ts +168 -0
- package/src/attribution.test.ts +31 -0
- package/src/attribution.ts +42 -0
- package/src/client-extension-registry.test.tsx +61 -0
- package/src/client-extension-registry.ts +159 -0
- package/src/clipboard.test.ts +23 -0
- package/src/clipboard.ts +33 -0
- package/src/components/AnnotatableFileView.stories.tsx +42 -0
- package/src/components/AnnotatableFileView.test.tsx +102 -0
- package/src/components/AnnotatableFileView.tsx +134 -0
- package/src/components/AnnotationCards.stories.tsx +86 -0
- package/src/components/AnnotationCards.tsx +341 -0
- package/src/components/AnsiScreen.stories.tsx +12 -0
- package/src/components/AnsiScreen.tsx +28 -0
- package/src/components/AppHeader.stories.tsx +50 -0
- package/src/components/AppHeader.tsx +83 -0
- package/src/components/AppShell.brand.test.tsx +58 -0
- package/src/components/AppShell.extensions.test.tsx +191 -0
- package/src/components/AppShell.stories.tsx +89 -0
- package/src/components/AppShell.tsx +362 -0
- package/src/components/AppShell.zoom.test.tsx +135 -0
- package/src/components/BareWorkbenchFileView.stories.tsx +27 -0
- package/src/components/BareWorkbenchFileView.tsx +64 -0
- package/src/components/Breadcrumb.stories.tsx +19 -0
- package/src/components/Breadcrumb.tsx +45 -0
- package/src/components/ChangesColumn.stories.tsx +44 -0
- package/src/components/ChangesColumn.tsx +199 -0
- package/src/components/ChangesFileTree.test.tsx +61 -0
- package/src/components/CodeBlock.stories.tsx +42 -0
- package/src/components/CodeBlock.tsx +84 -0
- package/src/components/CompletionOverlay.stories.tsx +37 -0
- package/src/components/CompletionOverlay.tsx +77 -0
- package/src/components/ConfirmCard.stories.tsx +37 -0
- package/src/components/ConfirmCard.tsx +150 -0
- package/src/components/ConfirmDialog.stories.tsx +21 -0
- package/src/components/ConfirmDialog.tsx +72 -0
- package/src/components/DiffContentView.scroll.test.tsx +149 -0
- package/src/components/DiffContentView.split.test.tsx +140 -0
- package/src/components/DiffContentView.stories.tsx +80 -0
- package/src/components/DiffContentView.syntax.test.tsx +82 -0
- package/src/components/DiffContentView.tsx +906 -0
- package/src/components/DiscussionMarkerRail.stories.tsx +54 -0
- package/src/components/DiscussionMarkerRail.tsx +186 -0
- package/src/components/EditorGrid.stories.tsx +59 -0
- package/src/components/EditorGrid.tabs.test.tsx +118 -0
- package/src/components/EditorGrid.tsx +426 -0
- package/src/components/EditorGrid.zoom.test.tsx +68 -0
- package/src/components/ExtensionZone.stories.tsx +28 -0
- package/src/components/ExtensionZone.tsx +145 -0
- package/src/components/FileContentsView.stories.tsx +16 -0
- package/src/components/FileContentsView.test.tsx +29 -0
- package/src/components/FileContentsView.tsx +98 -0
- package/src/components/GridTabContent.stories.tsx +59 -0
- package/src/components/GridTabContent.tsx +182 -0
- package/src/components/HandleChipsInput.stories.tsx +24 -0
- package/src/components/HandleChipsInput.test.tsx +35 -0
- package/src/components/HandleChipsInput.tsx +145 -0
- package/src/components/KeybindsDialog.stories.tsx +31 -0
- package/src/components/KeybindsDialog.tsx +103 -0
- package/src/components/MarkdownGridBlock.stories.tsx +33 -0
- package/src/components/MarkdownGridBlock.tsx +72 -0
- package/src/components/MarkdownThreadEditor.stories.tsx +15 -0
- package/src/components/MarkdownThreadEditor.test.tsx +98 -0
- package/src/components/MarkdownThreadEditor.tsx +162 -0
- package/src/components/MenuBar.stories.tsx +28 -0
- package/src/components/MenuBar.tsx +44 -0
- package/src/components/NavModeHint.stories.tsx +40 -0
- package/src/components/NavModeHint.tsx +31 -0
- package/src/components/OverlayScrollbar.stories.tsx +33 -0
- package/src/components/OverlayScrollbar.test.tsx +139 -0
- package/src/components/OverlayScrollbar.tsx +129 -0
- package/src/components/PaneGrid.stories.tsx +84 -0
- package/src/components/PaneGrid.tsx +128 -0
- package/src/components/PanelColumn.stories.tsx +25 -0
- package/src/components/PanelColumn.tsx +98 -0
- package/src/components/ProjectTreeView.stories.tsx +25 -0
- package/src/components/ProjectTreeView.test.tsx +75 -0
- package/src/components/ProjectTreeView.tsx +125 -0
- package/src/components/PromptDialog.stories.tsx +36 -0
- package/src/components/PromptDialog.test.tsx +61 -0
- package/src/components/PromptDialog.tsx +99 -0
- package/src/components/PrototypeContentView.stories.tsx +19 -0
- package/src/components/PrototypeContentView.tsx +446 -0
- package/src/components/RootOverlay.stories.tsx +45 -0
- package/src/components/RootOverlay.tsx +48 -0
- package/src/components/ScrollArea.stories.tsx +20 -0
- package/src/components/ScrollArea.tsx +44 -0
- package/src/components/SettingsDialog.stories.tsx +117 -0
- package/src/components/SettingsDialog.tsx +186 -0
- package/src/components/SettingsRows.stories.tsx +18 -0
- package/src/components/SettingsRows.tsx +112 -0
- package/src/components/ShareDialog.stories.tsx +121 -0
- package/src/components/ShareDialog.tsx +552 -0
- package/src/components/ShellHeader.stories.tsx +50 -0
- package/src/components/ShellHeader.tsx +137 -0
- package/src/components/SlashComposer.stories.tsx +37 -0
- package/src/components/SlashComposer.tsx +87 -0
- package/src/components/ThreadFooter.stories.tsx +30 -0
- package/src/components/ThreadFooter.test.tsx +47 -0
- package/src/components/ThreadFooter.tsx +76 -0
- package/src/components/ThreadTree.stories.tsx +46 -0
- package/src/components/ThreadTree.tsx +255 -0
- package/src/components/ThreadView.selection.test.tsx +951 -0
- package/src/components/ThreadView.slash.test.tsx +146 -0
- package/src/components/ThreadView.stories.tsx +26 -0
- package/src/components/ThreadView.tsx +525 -0
- package/src/components/Toast.stories.tsx +19 -0
- package/src/components/Toast.tsx +52 -0
- package/src/components/Tooltip.stories.tsx +26 -0
- package/src/components/Tooltip.tsx +72 -0
- package/src/components/TreePane.stories.tsx +106 -0
- package/src/components/TreePane.tsx +157 -0
- package/src/components/WalkWizard.stories.tsx +100 -0
- package/src/components/WalkWizard.tsx +150 -0
- package/src/components/WelcomePlayground.stories.tsx +12 -0
- package/src/components/WelcomePlayground.test.tsx +49 -0
- package/src/components/WelcomePlayground.tsx +139 -0
- package/src/components/WorkspacePanels.stories.tsx +36 -0
- package/src/components/WorkspacePanels.tsx +116 -0
- package/src/components/__snapshots__/stories.test.tsx.snap +2264 -0
- package/src/components/diff-content-view-layout.test.ts +198 -0
- package/src/components/diff-content-view-layout.ts +84 -0
- package/src/components/editor-grid.test.ts +86 -0
- package/src/components/editor-grid.ts +138 -0
- package/src/components/file-tree.test.ts +60 -0
- package/src/components/file-tree.ts +112 -0
- package/src/components/menu-control.stories.tsx +35 -0
- package/src/components/menu-control.tsx +43 -0
- package/src/components/pages/Workspace.stories.tsx +181 -0
- package/src/components/primitives/Button.stories.tsx +32 -0
- package/src/components/primitives/Button.tsx +59 -0
- package/src/components/primitives/Card.stories.tsx +25 -0
- package/src/components/primitives/Card.tsx +63 -0
- package/src/components/primitives/Dialog.stories.tsx +24 -0
- package/src/components/primitives/Dialog.test.tsx +37 -0
- package/src/components/primitives/Dialog.tsx +71 -0
- package/src/components/primitives/DialogActions.stories.tsx +20 -0
- package/src/components/primitives/DialogActions.tsx +39 -0
- package/src/components/primitives/IconButton.stories.tsx +36 -0
- package/src/components/primitives/IconButton.tsx +62 -0
- package/src/components/primitives/StatusBar.stories.tsx +11 -0
- package/src/components/primitives/StatusBar.tsx +23 -0
- package/src/components/primitives/Switch.stories.tsx +17 -0
- package/src/components/primitives/Switch.tsx +32 -0
- package/src/components/primitives/Tabs.stories.tsx +29 -0
- package/src/components/primitives/Tabs.tsx +102 -0
- package/src/components/primitives/Toolbar.stories.tsx +17 -0
- package/src/components/primitives/Toolbar.tsx +14 -0
- package/src/components/primitives/Tree.stories.tsx +124 -0
- package/src/components/primitives/Tree.tsx +100 -0
- package/src/components/primitives/frame.test.ts +12 -0
- package/src/components/primitives/frame.ts +17 -0
- package/src/components/primitives/icons.ts +61 -0
- package/src/components/primitives/tree-model.test.ts +99 -0
- package/src/components/primitives/tree-model.ts +137 -0
- package/src/components/quick-actions-editor.stories.tsx +58 -0
- package/src/components/quick-actions-editor.test.tsx +117 -0
- package/src/components/quick-actions-editor.tsx +226 -0
- package/src/components/share-dialog-store.ts +58 -0
- package/src/components/stories-app.tsx +193 -0
- package/src/components/stories.test.tsx +74 -0
- package/src/components/story-fixtures.ts +102 -0
- package/src/components/story-tree.test.ts +108 -0
- package/src/components/story.ts +159 -0
- package/src/components/syntax-highlight.ts +133 -0
- package/src/components/terminal-pane.test.tsx +40 -0
- package/src/components/terminal-pane.ts +199 -0
- package/src/components/theme-context.stories.tsx +46 -0
- package/src/components/theme-context.tsx +37 -0
- package/src/components/thread-tree.test.ts +134 -0
- package/src/components/thread-tree.ts +109 -0
- package/src/components/truncate-text.ts +7 -0
- package/src/components/truncate-title.test.ts +31 -0
- package/src/components/truncate-title.ts +15 -0
- package/src/config-identity.test.ts +78 -0
- package/src/config.test.ts +579 -34
- package/src/config.ts +533 -57
- package/src/diff-fold.test.ts +78 -0
- package/src/diff-fold.ts +126 -0
- package/src/diff-hunk-curate.test.ts +315 -0
- package/src/diff-hunk-curate.ts +153 -0
- package/src/diff-intraline.test.ts +238 -0
- package/src/diff-intraline.ts +236 -0
- package/src/diff-syntax.test.ts +112 -0
- package/src/diff-syntax.ts +179 -0
- package/src/discussions.ts +79 -0
- package/src/editor.test.ts +149 -0
- package/src/editor.ts +139 -9
- package/src/ghostty-terminal.test.ts +85 -0
- package/src/ghostty-terminal.ts +199 -0
- package/src/github-identity.test.ts +58 -0
- package/src/github-identity.ts +41 -0
- package/src/herdr.test.ts +9 -34
- package/src/herdr.ts +25 -21
- package/src/index.ts +29 -0
- package/src/integrations.test.ts +41 -0
- package/src/integrations.ts +32 -0
- package/src/intent-dispatch.test.ts +724 -0
- package/src/intent-dispatch.ts +690 -0
- package/src/key-bindings.test.ts +156 -0
- package/src/key-bindings.ts +459 -0
- package/src/keymap.test.ts +456 -115
- package/src/keymap.ts +315 -81
- package/src/kitty-image.test.ts +66 -0
- package/src/kitty-image.ts +138 -0
- package/src/launch-layout.test.ts +78 -0
- package/src/launch-layout.ts +62 -0
- package/src/mark-runs.ts +121 -0
- package/src/markdown-editor-syntax-style.ts +50 -0
- package/src/markdown-grid.test.ts +101 -0
- package/src/markdown-grid.ts +203 -0
- package/src/markdown-highlight.test.ts +61 -0
- package/src/markdown-highlight.ts +122 -0
- package/src/pasted-image.test.ts +67 -0
- package/src/pasted-image.ts +60 -0
- package/src/perf/otlp-trace.test.ts +116 -0
- package/src/perf/otlp-trace.ts +130 -0
- package/src/perf/perf-report.ts +17 -0
- package/src/perf/perf-timings.test.ts +18 -0
- package/src/perf/perf-timings.ts +38 -0
- package/src/prototype-browser.test.ts +81 -0
- package/src/prototype-browser.ts +329 -0
- package/src/prototype-pixels.tsx +35 -0
- package/src/pty.test.ts +81 -0
- package/src/pty.ts +245 -0
- package/src/ready-signal.test.ts +85 -0
- package/src/ready-signal.ts +52 -0
- package/src/run.ts +85 -12
- package/src/serve.test.ts +24 -12
- package/src/serve.ts +52 -10
- package/src/share.ts +237 -0
- package/src/skills.test.ts +74 -0
- package/src/skills.ts +75 -0
- package/src/slash-palette.test.ts +103 -0
- package/src/slash-palette.ts +129 -0
- package/src/split-diff.test.ts +110 -0
- package/src/split-diff.ts +149 -0
- package/src/test-support.test.ts +264 -0
- package/src/test-support.ts +369 -0
- package/src/theme-presets.test.ts +51 -0
- package/src/theme-presets.ts +219 -0
- package/src/theme.ts +88 -19
- package/src/thread-chords.test.ts +79 -0
- package/src/thread-chords.ts +163 -0
- package/src/thread-code-syntax.ts +45 -0
- package/src/thread-controller.curate.test.ts +476 -0
- package/src/thread-controller.derivation.test.ts +62 -0
- package/src/thread-controller.live-diff.test.ts +322 -0
- package/src/thread-controller.optimistic.test.ts +69 -0
- package/src/thread-controller.share.test.ts +561 -0
- package/src/thread-controller.switch-cache.test.ts +176 -0
- package/src/thread-controller.tree.test.ts +261 -0
- package/src/thread-controller.ts +2050 -0
- package/src/thread-selection.test.ts +210 -0
- package/src/thread-selection.ts +243 -0
- package/src/tree-view.test.ts +128 -0
- package/src/tree-view.ts +153 -0
- package/src/use-annotation-surface.tsx +975 -0
- package/src/use-changes-workbench.ts +190 -0
- package/src/use-frame-measure.ts +41 -0
- package/src/use-remember-layout.ts +31 -0
- package/src/use-repo-changes.ts +87 -0
- package/src/use-settings-dialog.tsx +364 -0
- package/src/use-terminal-virtualizer.ts +259 -0
- package/src/use-thread-body-editing.tsx +58 -0
- package/src/version.ts +5 -0
- package/src/view-diff.test.ts +279 -9
- package/src/view-diff.ts +460 -24
- package/src/view-plan.regression.test.ts +60 -0
- package/src/view-plan.test.ts +545 -0
- package/src/view-plan.ts +1019 -0
- package/src/visual/contrast.test.ts +35 -0
- package/src/visual/contrast.ts +33 -0
- package/src/walk.test.ts +121 -0
- package/src/walk.ts +74 -0
- package/src/workbench-snapshot.test.ts +99 -0
- package/src/workbench-snapshot.ts +59 -0
- package/src/session-controller.ts +0 -398
- package/src/syntax.test.ts +0 -41
- package/src/syntax.ts +0 -88
- package/src/view.test.ts +0 -157
- 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
|
+
});
|