@cueloop/client 0.1.0-alpha.5 → 0.1.0-alpha.50
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 +36 -31
- package/src/App.autoclose.test.tsx +137 -0
- package/src/App.diff.test.tsx +158 -22
- package/src/App.export.test.tsx +44 -37
- package/src/App.plan-review.test.tsx +672 -0
- package/src/App.prototype.test.tsx +155 -0
- package/src/App.readonly.test.tsx +60 -36
- package/src/App.rendering.test.tsx +132 -0
- package/src/App.review-panel.test.tsx +136 -0
- package/src/App.submit-confirm.test.tsx +218 -0
- package/src/App.test.tsx +128 -69
- package/src/App.toast.test.tsx +75 -0
- package/src/App.tsx +0 -0
- package/src/App.walk.test.tsx +289 -0
- package/src/__snapshots__/view-plan.regression.test.ts.snap +100 -0
- package/src/attribution.test.ts +31 -0
- package/src/attribution.ts +38 -0
- package/src/clipboard.test.ts +23 -0
- package/src/clipboard.ts +30 -0
- package/src/components/AnnotationCard.compose-rows.test.ts +49 -0
- package/src/components/AnnotationCard.stories.tsx +94 -0
- package/src/components/AnnotationCard.tsx +241 -0
- package/src/components/Breadcrumb.stories.tsx +19 -0
- package/src/components/Breadcrumb.tsx +44 -0
- package/src/components/CodeBlock.stories.tsx +42 -0
- package/src/components/CodeBlock.tsx +83 -0
- package/src/components/CompletionOverlay.stories.tsx +23 -0
- package/src/components/CompletionOverlay.tsx +59 -0
- package/src/components/ConfirmCard.stories.tsx +35 -0
- package/src/components/ConfirmCard.tsx +127 -0
- package/src/components/ConfirmDialog.stories.tsx +21 -0
- package/src/components/ConfirmDialog.tsx +64 -0
- package/src/components/DiffSheet.scroll.test.tsx +123 -0
- package/src/components/DiffSheet.stories.tsx +78 -0
- package/src/components/DiffSheet.syntax.test.tsx +62 -0
- package/src/components/DiffSheet.tsx +355 -0
- package/src/components/InboxList.stories.tsx +31 -0
- package/src/components/InboxList.tsx +62 -0
- package/src/components/KeybindsDialog.stories.tsx +31 -0
- package/src/components/KeybindsDialog.tsx +58 -0
- package/src/components/MarkerPopover.stories.tsx +58 -0
- package/src/components/MarkerPopover.tsx +108 -0
- package/src/components/MenuBar.stories.tsx +37 -0
- package/src/components/MenuBar.tsx +74 -0
- package/src/components/PlanSheet.stories.tsx +113 -0
- package/src/components/PlanSheet.tsx +456 -0
- package/src/components/PromptDialog.stories.tsx +36 -0
- package/src/components/PromptDialog.tsx +78 -0
- package/src/components/PrototypeSheet.stories.tsx +20 -0
- package/src/components/PrototypeSheet.tsx +358 -0
- package/src/components/ReviewPanel.stories.tsx +86 -0
- package/src/components/ReviewPanel.tsx +101 -0
- package/src/components/ReviewRail.stories.tsx +218 -0
- package/src/components/ReviewRail.tsx +317 -0
- package/src/components/SettingsDialog.stories.tsx +111 -0
- package/src/components/SettingsDialog.tsx +141 -0
- package/src/components/SettingsRows.stories.tsx +18 -0
- package/src/components/SettingsRows.tsx +108 -0
- package/src/components/Toast.stories.tsx +12 -0
- package/src/components/Toast.tsx +44 -0
- package/src/components/WalkWizard.stories.tsx +100 -0
- package/src/components/WalkWizard.tsx +152 -0
- package/src/components/__snapshots__/stories.test.tsx.snap +1857 -0
- package/src/components/agent-launcher.stories.tsx +26 -0
- package/src/components/agent-launcher.test.tsx +49 -0
- package/src/components/agent-launcher.tsx +147 -0
- package/src/components/diff-sheet-layout.test.ts +127 -0
- package/src/components/diff-sheet-layout.ts +167 -0
- package/src/components/primitives/Button.stories.tsx +32 -0
- package/src/components/primitives/Button.tsx +51 -0
- package/src/components/primitives/Card.stories.tsx +25 -0
- package/src/components/primitives/Card.tsx +59 -0
- package/src/components/primitives/Dialog.stories.tsx +24 -0
- package/src/components/primitives/Dialog.tsx +64 -0
- package/src/components/primitives/StatusBar.stories.tsx +11 -0
- package/src/components/primitives/StatusBar.tsx +22 -0
- package/src/components/primitives/Tabs.stories.tsx +29 -0
- package/src/components/primitives/Tabs.tsx +98 -0
- package/src/components/primitives/Toolbar.stories.tsx +17 -0
- package/src/components/primitives/Toolbar.tsx +14 -0
- package/src/components/primitives/frame.test.ts +12 -0
- package/src/components/primitives/frame.ts +14 -0
- package/src/components/quick-actions-editor.stories.tsx +54 -0
- package/src/components/quick-actions-editor.tsx +136 -0
- package/src/components/stories-app.tsx +123 -0
- package/src/components/stories.test.tsx +62 -0
- package/src/components/story-fixtures.ts +98 -0
- package/src/components/story.ts +69 -0
- package/src/components/syntax-highlight.ts +92 -0
- package/src/components/terminal-pane.test.tsx +40 -0
- package/src/components/terminal-pane.ts +181 -0
- package/src/components/theme-context.stories.tsx +46 -0
- package/src/components/theme-context.tsx +36 -0
- package/src/components/truncate-text.ts +6 -0
- package/src/config.test.ts +430 -31
- package/src/config.ts +304 -26
- package/src/diff-hunk-curate.test.ts +315 -0
- package/src/diff-hunk-curate.ts +233 -0
- package/src/diff-intraline.test.ts +164 -0
- package/src/diff-intraline.ts +132 -0
- package/src/diff-syntax.test.ts +96 -0
- package/src/diff-syntax.ts +154 -0
- package/src/editor.test.ts +143 -0
- package/src/editor.ts +123 -9
- package/src/ghostty-terminal.test.ts +82 -0
- package/src/ghostty-terminal.ts +168 -0
- package/src/herdr.test.ts +41 -0
- package/src/herdr.ts +27 -0
- package/src/index.ts +10 -0
- package/src/integrations.test.ts +41 -0
- package/src/integrations.ts +30 -0
- package/src/intent-dispatch.test.ts +493 -0
- package/src/intent-dispatch.ts +428 -0
- package/src/key-bindings.test.ts +152 -0
- package/src/key-bindings.ts +430 -0
- package/src/keymap.test.ts +622 -0
- package/src/keymap.ts +339 -0
- package/src/kitty-image.test.ts +20 -0
- package/src/kitty-image.ts +75 -0
- package/src/prototype-browser.test.ts +55 -0
- package/src/prototype-browser.ts +243 -0
- package/src/pty.test.ts +78 -0
- package/src/pty.ts +222 -0
- package/src/review-panel.test.ts +113 -0
- package/src/review-panel.ts +83 -0
- package/src/run.ts +9 -5
- package/src/serve.test.ts +24 -9
- package/src/serve.ts +7 -7
- package/src/session-controller.curate.test.ts +253 -0
- package/src/session-controller.share.test.ts +309 -0
- package/src/session-controller.ts +914 -0
- package/src/share.ts +113 -0
- package/src/test-support.test.ts +55 -0
- package/src/test-support.ts +187 -0
- package/src/theme-presets.ts +168 -0
- package/src/theme.ts +38 -17
- package/src/version.ts +5 -0
- package/src/view-diff.test.ts +19 -8
- package/src/view-diff.ts +34 -23
- package/src/view-plan.regression.test.ts +60 -0
- package/src/view-plan.test.ts +399 -0
- package/src/view-plan.ts +495 -0
- package/src/walk.test.ts +121 -0
- package/src/walk.ts +70 -0
- package/src/view.test.ts +0 -157
- package/src/view.ts +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { DARK } from "../theme";
|
|
3
|
+
import type { Story, StoryMeta } from "./story";
|
|
4
|
+
import { ConfirmDialog } from "./ConfirmDialog";
|
|
5
|
+
|
|
6
|
+
export const meta: StoryMeta = { title: "ConfirmDialog" };
|
|
7
|
+
|
|
8
|
+
const callbacks = { onConfirm: () => {}, onCancel: () => {} };
|
|
9
|
+
|
|
10
|
+
export const DeletePlan: Story = {
|
|
11
|
+
render: () => (
|
|
12
|
+
<ConfirmDialog
|
|
13
|
+
isOpen
|
|
14
|
+
title=" Delete plan "
|
|
15
|
+
message={'Delete "Auth rollout"? This removes the plan and its review.'}
|
|
16
|
+
{...callbacks}
|
|
17
|
+
/>
|
|
18
|
+
),
|
|
19
|
+
expectedColors: [DARK.accent],
|
|
20
|
+
size: { width: 64, height: 16 },
|
|
21
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A centered yes/no confirmation on the shared Dialog overlay. Presentational:
|
|
3
|
+
* the key grammar (⏎ confirm, esc cancel) is driven by the reducer; the buttons
|
|
4
|
+
* carry the same intents for the mouse. Used for destructive actions like the
|
|
5
|
+
* inbox delete.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React from "react";
|
|
9
|
+
import { useTerminalDimensions } from "@opentui/react";
|
|
10
|
+
import type { Theme } from "../theme";
|
|
11
|
+
import { useComponentTheme } from "./theme-context";
|
|
12
|
+
import { Dialog } from "./primitives/Dialog";
|
|
13
|
+
import { Button } from "./primitives/Button";
|
|
14
|
+
import { Toolbar } from "./primitives/Toolbar";
|
|
15
|
+
|
|
16
|
+
export interface ConfirmDialogProps {
|
|
17
|
+
isOpen: boolean;
|
|
18
|
+
title?: string;
|
|
19
|
+
message: string;
|
|
20
|
+
confirmLabel?: string;
|
|
21
|
+
cancelLabel?: string;
|
|
22
|
+
onConfirm: () => void;
|
|
23
|
+
onCancel: () => void;
|
|
24
|
+
theme?: Theme;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function ConfirmDialog({
|
|
28
|
+
isOpen,
|
|
29
|
+
title,
|
|
30
|
+
message,
|
|
31
|
+
confirmLabel = "Delete",
|
|
32
|
+
cancelLabel = "Cancel",
|
|
33
|
+
onConfirm,
|
|
34
|
+
onCancel,
|
|
35
|
+
theme,
|
|
36
|
+
}: ConfirmDialogProps): React.ReactNode {
|
|
37
|
+
const tokens = useComponentTheme(theme);
|
|
38
|
+
const { width: terminalWidth } = useTerminalDimensions();
|
|
39
|
+
if (!isOpen) return null;
|
|
40
|
+
return (
|
|
41
|
+
<Dialog isOpen title={title} width={Math.min(54, terminalWidth - 6)} height={7} theme={theme}>
|
|
42
|
+
<box
|
|
43
|
+
style={{
|
|
44
|
+
flexDirection: "column",
|
|
45
|
+
flexGrow: 1,
|
|
46
|
+
paddingLeft: 1,
|
|
47
|
+
paddingRight: 1,
|
|
48
|
+
paddingTop: 1,
|
|
49
|
+
}}
|
|
50
|
+
>
|
|
51
|
+
<text fg={tokens.text}>{message}</text>
|
|
52
|
+
<box style={{ flexGrow: 1 }} />
|
|
53
|
+
<Toolbar>
|
|
54
|
+
<Button variant="solid" onPress={onConfirm} theme={theme}>
|
|
55
|
+
{` ${confirmLabel} `}
|
|
56
|
+
</Button>
|
|
57
|
+
<Button onPress={onCancel} theme={theme}>
|
|
58
|
+
{` ${cancelLabel} esc `}
|
|
59
|
+
</Button>
|
|
60
|
+
</Toolbar>
|
|
61
|
+
</box>
|
|
62
|
+
</Dialog>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/** Keyboard scroll is smooth: cursor-follow never drifts past wrapped cards. */
|
|
2
|
+
|
|
3
|
+
import { expect, test } from "bun:test";
|
|
4
|
+
import React, { useState } from "react";
|
|
5
|
+
import { testRender } from "@opentui/react/test-utils";
|
|
6
|
+
import { useKeyboard } from "@opentui/react";
|
|
7
|
+
import type { Renderable, ScrollBoxRenderable } from "@opentui/core";
|
|
8
|
+
import type { Annotation } from "@cueloop/schema";
|
|
9
|
+
import { DiffSheet } from "./DiffSheet";
|
|
10
|
+
import { diffRows, type DiffRow } from "../view-diff";
|
|
11
|
+
import { DARK } from "../theme";
|
|
12
|
+
import { press } from "../test-support";
|
|
13
|
+
|
|
14
|
+
/** Find a renderable by id anywhere in the tree (getRenderable is not recursive). */
|
|
15
|
+
function findById(node: Renderable, id: string): Renderable | undefined {
|
|
16
|
+
if (node.id === id) return node;
|
|
17
|
+
for (const child of node.getChildren()) {
|
|
18
|
+
const found = findById(child, id);
|
|
19
|
+
if (found) return found;
|
|
20
|
+
}
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function hex(color: { toInts(): [number, number, number, number] }): string {
|
|
25
|
+
const [red, green, blue] = color.toInts();
|
|
26
|
+
return "#" + [red, green, blue].map((part) => part.toString(16).padStart(2, "0")).join("");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** A single-hunk patch of `count` added lines, taller than any test viewport. */
|
|
30
|
+
function tallPatch(count: number): string {
|
|
31
|
+
const adds = Array.from(
|
|
32
|
+
{ length: count },
|
|
33
|
+
(_, index) => `+line ${String(index).padStart(2, "0")}`,
|
|
34
|
+
);
|
|
35
|
+
return [
|
|
36
|
+
"diff --git a/f.ts b/f.ts",
|
|
37
|
+
"--- a/f.ts",
|
|
38
|
+
"+++ b/f.ts",
|
|
39
|
+
`@@ -0,0 +1,${count} @@`,
|
|
40
|
+
...adds,
|
|
41
|
+
"",
|
|
42
|
+
].join("\n");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** DiffSheet with j-driven cursor, mirroring the App's down-navigation. */
|
|
46
|
+
function ScrollHarness({
|
|
47
|
+
rows,
|
|
48
|
+
annotations,
|
|
49
|
+
}: {
|
|
50
|
+
rows: DiffRow[];
|
|
51
|
+
annotations: Annotation[];
|
|
52
|
+
}): React.ReactNode {
|
|
53
|
+
const [cursor, setCursor] = useState(0);
|
|
54
|
+
useKeyboard((key) => {
|
|
55
|
+
if (key.name === "j") setCursor((current) => Math.min(rows.length - 1, current + 1));
|
|
56
|
+
});
|
|
57
|
+
return <DiffSheet rows={rows} cursor={cursor} annotations={annotations} />;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
test("scrolling down past wrapped annotation cards keeps the cursor pinned and the scroll monotonic", async () => {
|
|
61
|
+
// Arrange - a tall diff whose annotation bodies would wrap at this width; the
|
|
62
|
+
// wrapped card is exactly the layout the cursor-follow model used to mis-measure
|
|
63
|
+
const rows = diffRows(tallPatch(40));
|
|
64
|
+
const longBody =
|
|
65
|
+
"this is a deliberately long annotation body that would wrap across the narrow test viewport";
|
|
66
|
+
const annotations: Annotation[] = [8, 16, 24].map((rowIndex) => ({
|
|
67
|
+
id: `a${rowIndex}`,
|
|
68
|
+
kind: "comment",
|
|
69
|
+
body: longBody,
|
|
70
|
+
anchor: { quote: rows[rowIndex]!.text, prefix: "", suffix: "" },
|
|
71
|
+
createdAt: "2026-01-01T00:00:00Z",
|
|
72
|
+
}));
|
|
73
|
+
const setup = await testRender(<ScrollHarness rows={rows} annotations={annotations} />, {
|
|
74
|
+
width: 60,
|
|
75
|
+
height: 12,
|
|
76
|
+
});
|
|
77
|
+
await setup.waitForVisualIdle();
|
|
78
|
+
const scrollbox = findById(setup.renderer.root, "diff-scroll") as ScrollBoxRenderable;
|
|
79
|
+
|
|
80
|
+
// the cursor row is the one painted with the cursor background
|
|
81
|
+
const cursorScreenRow = (): number => {
|
|
82
|
+
const lines = setup.captureSpans().lines;
|
|
83
|
+
for (let index = 0; index < lines.length; index++) {
|
|
84
|
+
if (lines[index]!.spans.some((span) => hex(span.bg) === DARK.cursorBackground)) return index;
|
|
85
|
+
}
|
|
86
|
+
return -1;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// Act - walk the cursor down through the whole diff, sampling the scroll each step
|
|
90
|
+
const scrollTops: number[] = [];
|
|
91
|
+
const screenRows: number[] = [];
|
|
92
|
+
for (let step = 0; step < rows.length; step++) {
|
|
93
|
+
scrollTops.push(scrollbox.scrollTop);
|
|
94
|
+
screenRows.push(cursorScreenRow());
|
|
95
|
+
await press(setup, "j");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Assert - scrollTop only ever grows going down (never rebounds/jitters)
|
|
99
|
+
for (let step = 1; step < scrollTops.length; step++) {
|
|
100
|
+
expect(scrollTops[step]!).toBeGreaterThanOrEqual(scrollTops[step - 1]!);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Assert - the cursor never scrolls off screen (the pre-fix bug walked it off
|
|
104
|
+
// the viewport as it passed each wrapped card)
|
|
105
|
+
expect(screenRows.every((row) => row >= 0)).toBe(true);
|
|
106
|
+
|
|
107
|
+
// Assert - through the steady scroll region (not the initial fill, not the
|
|
108
|
+
// final clamp at the content end) the cursor holds ONE stable row near the
|
|
109
|
+
// bottom; pre-fix it drifted down a row per wrapped card
|
|
110
|
+
const maxScroll = scrollbox.scrollHeight - scrollbox.height;
|
|
111
|
+
const steadyRows = screenRows.filter(
|
|
112
|
+
(_, step) => scrollTops[step]! > 0 && scrollTops[step]! < maxScroll,
|
|
113
|
+
);
|
|
114
|
+
expect(steadyRows.length).toBeGreaterThan(0);
|
|
115
|
+
expect(new Set(steadyRows).size).toBe(1);
|
|
116
|
+
const pinnedRow = steadyRows[0]!;
|
|
117
|
+
const viewportBottom = scrollbox.height;
|
|
118
|
+
// the cursor sits a couple of rows inside the bottom edge, per the follow rule
|
|
119
|
+
expect(viewportBottom - pinnedRow).toBeGreaterThanOrEqual(2);
|
|
120
|
+
expect(viewportBottom - pinnedRow).toBeLessThanOrEqual(3);
|
|
121
|
+
|
|
122
|
+
setup.renderer.destroy();
|
|
123
|
+
}, 25000);
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { DARK } from "../theme";
|
|
3
|
+
import type { Story, StoryMeta } from "./story";
|
|
4
|
+
import { DiffSheet } from "./DiffSheet";
|
|
5
|
+
import { FIXTURE_PATCH, fixtureDiffRows } from "./story-fixtures";
|
|
6
|
+
|
|
7
|
+
export const meta: StoryMeta = { title: "DiffSheet" };
|
|
8
|
+
|
|
9
|
+
export const SignedRows: Story = {
|
|
10
|
+
render: () => <DiffSheet rows={fixtureDiffRows()} cursor={0} annotations={[]} />,
|
|
11
|
+
expectedColors: [DARK.insertedForeground, DARK.deletedForeground],
|
|
12
|
+
size: { width: 90, height: 16 },
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// A modified line keeps the diff color on the changed words and dims the
|
|
16
|
+
// unchanged prefix to the dim token, so the intra-line change stands out.
|
|
17
|
+
export const IntralineWordDiff: Story = {
|
|
18
|
+
render: () => <DiffSheet rows={fixtureDiffRows()} cursor={0} annotations={[]} />,
|
|
19
|
+
expectedColors: [DARK.insertedForeground, DARK.deletedForeground, DARK.textDim],
|
|
20
|
+
size: { width: 90, height: 16 },
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const CursorOnAddedLine: Story = {
|
|
24
|
+
render: () => <DiffSheet rows={fixtureDiffRows()} cursor={4} annotations={[]} />,
|
|
25
|
+
expectedColors: [DARK.cursorBackground],
|
|
26
|
+
size: { width: 90, height: 16 },
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const ComposingOnLine: Story = {
|
|
30
|
+
render: () => (
|
|
31
|
+
<DiffSheet
|
|
32
|
+
rows={fixtureDiffRows()}
|
|
33
|
+
cursor={4}
|
|
34
|
+
annotations={[]}
|
|
35
|
+
compose={{
|
|
36
|
+
kind: "comment",
|
|
37
|
+
rowIndex: 4,
|
|
38
|
+
quote: " private items = new Map();",
|
|
39
|
+
draft: {
|
|
40
|
+
text: "Map needs an eviction story.",
|
|
41
|
+
onInput: () => {},
|
|
42
|
+
onSave: () => {},
|
|
43
|
+
onCancel: () => {},
|
|
44
|
+
},
|
|
45
|
+
}}
|
|
46
|
+
/>
|
|
47
|
+
),
|
|
48
|
+
expectedColors: [DARK.accent],
|
|
49
|
+
size: { width: 90, height: 16 },
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const AnnotatedLine: Story = {
|
|
53
|
+
render: () => (
|
|
54
|
+
<DiffSheet
|
|
55
|
+
rows={fixtureDiffRows()}
|
|
56
|
+
cursor={0}
|
|
57
|
+
annotations={[
|
|
58
|
+
{
|
|
59
|
+
id: "a_story_diff",
|
|
60
|
+
kind: "comment",
|
|
61
|
+
anchor: {
|
|
62
|
+
// diff anchors quote the row text verbatim, trailing newline included
|
|
63
|
+
quote:
|
|
64
|
+
FIXTURE_PATCH.split("\n")
|
|
65
|
+
.find((line) => line.startsWith("+ "))!
|
|
66
|
+
.slice(1) + "\n",
|
|
67
|
+
prefix: "",
|
|
68
|
+
suffix: "",
|
|
69
|
+
},
|
|
70
|
+
body: "Map needs an eviction story.",
|
|
71
|
+
createdAt: "2026-01-01T00:00:00Z",
|
|
72
|
+
},
|
|
73
|
+
]}
|
|
74
|
+
/>
|
|
75
|
+
),
|
|
76
|
+
expectedColors: [DARK.markCommentBackground],
|
|
77
|
+
size: { width: 90, height: 16 },
|
|
78
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { afterEach, beforeEach, expect, test } from "bun:test";
|
|
2
|
+
import { testRender } from "@opentui/react/test-utils";
|
|
3
|
+
import type { RGBA } from "@opentui/core";
|
|
4
|
+
import React from "react";
|
|
5
|
+
import { DiffSheet } from "./DiffSheet";
|
|
6
|
+
import { diffRows } from "../view-diff";
|
|
7
|
+
import { DARK } from "../theme";
|
|
8
|
+
import { allowEventLoopUpdates } from "../test-support";
|
|
9
|
+
|
|
10
|
+
const PATCH = `diff --git a/src/x.ts b/src/x.ts
|
|
11
|
+
--- a/src/x.ts
|
|
12
|
+
+++ b/src/x.ts
|
|
13
|
+
@@ -1,3 +1,3 @@
|
|
14
|
+
export class Store {
|
|
15
|
+
- const limit = 100;
|
|
16
|
+
+ const limit = 250;
|
|
17
|
+
}
|
|
18
|
+
`;
|
|
19
|
+
|
|
20
|
+
function hex(color: RGBA): string {
|
|
21
|
+
const [red, green, blue] = color.toInts();
|
|
22
|
+
return "#" + [red, green, blue].map((part) => part.toString(16).padStart(2, "0")).join("");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
beforeEach(() => allowEventLoopUpdates());
|
|
26
|
+
afterEach(() => allowEventLoopUpdates());
|
|
27
|
+
|
|
28
|
+
test("renders tree-sitter colors, with the changed word in the diff color on top", async () => {
|
|
29
|
+
// Arrange
|
|
30
|
+
const setup = await testRender(<DiffSheet rows={diffRows(PATCH)} cursor={0} annotations={[]} />, {
|
|
31
|
+
width: 60,
|
|
32
|
+
height: 12,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Act
|
|
36
|
+
// syntax highlighting resolves off the render path; pump until a keyword paints
|
|
37
|
+
let keywordAccent = false;
|
|
38
|
+
for (let attempt = 0; attempt < 60 && !keywordAccent; attempt++) {
|
|
39
|
+
await setup.renderOnce();
|
|
40
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
41
|
+
for (const line of setup.captureSpans().lines) {
|
|
42
|
+
for (const span of line.spans) {
|
|
43
|
+
if (span.text.includes("export") && hex(span.fg) === DARK.accent) keywordAccent = true;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Assert
|
|
49
|
+
expect(keywordAccent).toBe(true);
|
|
50
|
+
// the intra-line changed number keeps the diff color over its syntax color
|
|
51
|
+
const changedColors = new Set<string>();
|
|
52
|
+
for (const line of setup.captureSpans().lines) {
|
|
53
|
+
for (const span of line.spans) {
|
|
54
|
+
if (span.text.includes("250")) changedColors.add(hex(span.fg));
|
|
55
|
+
if (span.text.includes("100")) changedColors.add(hex(span.fg));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
expect(changedColors.has(DARK.insertedForeground)).toBe(true);
|
|
59
|
+
expect(changedColors.has(DARK.deletedForeground)).toBe(true);
|
|
60
|
+
|
|
61
|
+
setup.renderer.destroy();
|
|
62
|
+
}, 25000);
|