@cueloop/client 0.1.0-alpha.91 → 0.1.0-alpha.93
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/package.json +5 -5
- package/src/App.deleted-scroll.test.tsx +186 -0
- package/src/App.diff.test.tsx +3 -4
- package/src/App.test.tsx +1 -0
- package/src/App.toast.test.tsx +27 -1
- package/src/App.tsx +21 -12
- package/src/App.workbench.test.tsx +208 -2
- package/src/app-screens.tsx +34 -8
- package/src/components/AnnotatableFileView.test.tsx +48 -1
- package/src/components/AppShell.zoom.test.tsx +16 -3
- package/src/components/ChangesColumn.tsx +10 -4
- package/src/components/ChangesFileTree.test.tsx +122 -4
- package/src/components/DiffContentView.scroll.test.tsx +285 -1
- package/src/components/DiffContentView.tsx +214 -56
- package/src/components/EditorGrid.split.test.tsx +118 -0
- package/src/components/EditorGrid.stories.tsx +27 -0
- package/src/components/EditorGrid.tsx +170 -50
- package/src/components/EditorGrid.zoom.test.tsx +65 -1
- package/src/components/GridTabContent.actions.test.tsx +151 -0
- package/src/components/GridTabContent.stories.tsx +2 -2
- package/src/components/GridTabContent.tsx +5 -1
- package/src/components/ProjectTreeView.test.tsx +166 -8
- package/src/components/ProjectTreeView.tsx +53 -27
- package/src/components/ScrollArea.tsx +21 -2
- package/src/components/Toast.tsx +3 -1
- package/src/components/WelcomePlayground.test.tsx +28 -1
- package/src/components/__snapshots__/stories.test.tsx.snap +41 -21
- package/src/components/editor-grid.test.ts +87 -0
- package/src/components/editor-grid.ts +142 -6
- package/src/components/primitives/Tree.tsx +51 -15
- package/src/diff-syntax.test.ts +15 -2
- package/src/diff-syntax.ts +13 -5
- package/src/thread-controller.curate.test.ts +27 -0
- package/src/thread-controller.ts +4 -6
- package/src/use-annotation-surface.tsx +86 -19
- package/src/use-changes-workbench.ts +30 -18
- package/src/use-frame-measure.ts +40 -7
- package/src/use-settings-dialog.tsx +1 -13
- package/src/use-shared-keyboard.ts +55 -0
- package/src/use-terminal-virtualizer.ts +8 -2
|
@@ -1,14 +1,26 @@
|
|
|
1
|
-
/** The Project tree navigates by keyboard when focused: j/k move the cursor,
|
|
1
|
+
/** The Project tree navigates by keyboard when focused: j/k move the cursor, Enter folds a folder or
|
|
2
2
|
* opens a file. */
|
|
3
3
|
|
|
4
4
|
import { test, expect } from "bun:test";
|
|
5
5
|
import React from "react";
|
|
6
6
|
import { testRender } from "@opentui/react/test-utils";
|
|
7
|
+
import { ScrollBoxRenderable, type Renderable } from "@opentui/core";
|
|
7
8
|
import { ProjectTreeView } from "./ProjectTreeView";
|
|
8
|
-
import { locateText, settle, waitForText } from "../test-support";
|
|
9
|
+
import { locateText, settle, waitForText, waitForTextGone } from "../test-support";
|
|
9
10
|
import { DARK } from "../theme";
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
function findById(node: Renderable, id: string): Renderable | undefined {
|
|
13
|
+
if (node.id === id) return node;
|
|
14
|
+
for (const child of node.getChildren()) {
|
|
15
|
+
const found = findById(child, id);
|
|
16
|
+
|
|
17
|
+
if (found) return found;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
test("Enter expands a folder, then j moves down and Enter opens the file", async () => {
|
|
12
24
|
const opened: string[] = [];
|
|
13
25
|
const setup = await testRender(
|
|
14
26
|
<ProjectTreeView
|
|
@@ -22,14 +34,14 @@ test("tab expands a folder, then j moves down and tab opens the file", async ()
|
|
|
22
34
|
await settle(setup);
|
|
23
35
|
await waitForText(setup, "src");
|
|
24
36
|
|
|
25
|
-
// cursor starts on the "src" folder;
|
|
26
|
-
setup.mockInput.pressKey("
|
|
37
|
+
// cursor starts on the "src" folder; Enter expands it
|
|
38
|
+
setup.mockInput.pressKey("RETURN");
|
|
27
39
|
await waitForText(setup, "a.ts");
|
|
28
40
|
|
|
29
|
-
// j moves onto the file,
|
|
41
|
+
// j moves onto the file, Enter opens it
|
|
30
42
|
setup.mockInput.pressKey("j");
|
|
31
43
|
await settle(setup);
|
|
32
|
-
setup.mockInput.pressKey("
|
|
44
|
+
setup.mockInput.pressKey("RETURN");
|
|
33
45
|
await settle(setup);
|
|
34
46
|
|
|
35
47
|
expect(opened).toEqual(["src/a.ts"]);
|
|
@@ -37,6 +49,91 @@ test("tab expands a folder, then j moves down and tab opens the file", async ()
|
|
|
37
49
|
setup.renderer.destroy();
|
|
38
50
|
});
|
|
39
51
|
|
|
52
|
+
test("keyboard navigation reveals the selected project row", async () => {
|
|
53
|
+
const paths = Array.from(
|
|
54
|
+
{ length: 35 },
|
|
55
|
+
(_, index) => `file-${String(index).padStart(2, "0")}.ts`,
|
|
56
|
+
);
|
|
57
|
+
const setup = await testRender(
|
|
58
|
+
<ProjectTreeView loadFiles={async () => paths} onSelectFile={() => {}} focused theme={DARK} />,
|
|
59
|
+
{ width: 28, height: 8 },
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
await waitForText(setup, "file-00.ts");
|
|
63
|
+
const scrollbox = findById(setup.renderer.root, "tree-scroll");
|
|
64
|
+
|
|
65
|
+
if (!(scrollbox instanceof ScrollBoxRenderable)) throw new Error("tree scrollbox missing");
|
|
66
|
+
const scrollSteps: number[] = [];
|
|
67
|
+
|
|
68
|
+
for (const [index, key] of Array.from({ length: 15 }, () => "j").entries()) {
|
|
69
|
+
const before = scrollbox.scrollTop;
|
|
70
|
+
|
|
71
|
+
setup.mockInput.pressKey(key);
|
|
72
|
+
// eslint-disable-next-line no-await-in-loop
|
|
73
|
+
await settle(setup);
|
|
74
|
+
scrollSteps.push(scrollbox.scrollTop - before);
|
|
75
|
+
const row = scrollbox.content.findDescendantById(
|
|
76
|
+
`tree-row-file-${String(index + 1).padStart(2, "0")}.ts`,
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
expect(row?.y).toBeGreaterThanOrEqual(scrollbox.viewport.y);
|
|
80
|
+
expect(row?.y).toBeLessThan(scrollbox.viewport.y + scrollbox.viewport.height);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
expect(scrollbox.scrollTop).toBeGreaterThan(0);
|
|
84
|
+
expect(Math.max(...scrollSteps)).toBe(1);
|
|
85
|
+
expect(setup.captureCharFrame()).toContain("file-15.ts");
|
|
86
|
+
setup.renderer.destroy();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("project wheel scrolling accelerates during a gesture and resets after a pause", async () => {
|
|
90
|
+
const paths = Array.from(
|
|
91
|
+
{ length: 35 },
|
|
92
|
+
(_, index) => `file-${String(index).padStart(2, "0")}.ts`,
|
|
93
|
+
);
|
|
94
|
+
const setup = await testRender(
|
|
95
|
+
<ProjectTreeView loadFiles={async () => paths} onSelectFile={() => {}} theme={DARK} />,
|
|
96
|
+
{ width: 28, height: 8 },
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
await waitForText(setup, "file-00.ts");
|
|
100
|
+
const scrollbox = findById(setup.renderer.root, "tree-scroll");
|
|
101
|
+
|
|
102
|
+
if (!(scrollbox instanceof ScrollBoxRenderable)) throw new Error("tree scrollbox missing");
|
|
103
|
+
const acceleration = scrollbox.scrollAcceleration;
|
|
104
|
+
const first = acceleration.tick(1000);
|
|
105
|
+
const burst = Array.from({ length: 10 }, (_, index) => acceleration.tick(1016 + index * 16));
|
|
106
|
+
const tail = acceleration.tick(1400);
|
|
107
|
+
|
|
108
|
+
expect(first).toBe(1);
|
|
109
|
+
expect(Math.max(...burst)).toBeGreaterThan(first);
|
|
110
|
+
expect(tail).toBe(1);
|
|
111
|
+
acceleration.reset();
|
|
112
|
+
await setup.mockMouse.scroll(10, 4, "down");
|
|
113
|
+
await settle(setup);
|
|
114
|
+
expect(scrollbox.scrollTop).toBeGreaterThan(0);
|
|
115
|
+
setup.renderer.destroy();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("long project filenames stay on one row without an ellipsis", async () => {
|
|
119
|
+
const path = "src/ChangesFileTree.test.tsx";
|
|
120
|
+
const setup = await testRender(
|
|
121
|
+
<ProjectTreeView loadFiles={async () => [path]} onSelectFile={() => {}} focused theme={DARK} />,
|
|
122
|
+
{ width: 22, height: 8 },
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
await waitForText(setup, "src");
|
|
126
|
+
setup.mockInput.pressKey("RETURN");
|
|
127
|
+
await settle(setup);
|
|
128
|
+
const frame = setup.captureCharFrame();
|
|
129
|
+
const row = findById(setup.renderer.root, `tree-row-${path}`);
|
|
130
|
+
|
|
131
|
+
expect(row?.height).toBe(1);
|
|
132
|
+
expect(frame).not.toContain("…");
|
|
133
|
+
expect(frame).toContain("ChangesFile");
|
|
134
|
+
setup.renderer.destroy();
|
|
135
|
+
});
|
|
136
|
+
|
|
40
137
|
test("without focus the keyboard does nothing", async () => {
|
|
41
138
|
const opened: string[] = [];
|
|
42
139
|
const setup = await testRender(
|
|
@@ -50,7 +147,7 @@ test("without focus the keyboard does nothing", async () => {
|
|
|
50
147
|
await settle(setup);
|
|
51
148
|
await waitForText(setup, "src");
|
|
52
149
|
|
|
53
|
-
setup.mockInput.pressKey("
|
|
150
|
+
setup.mockInput.pressKey("RETURN");
|
|
54
151
|
await settle(setup);
|
|
55
152
|
expect(setup.captureCharFrame()).not.toContain("a.ts");
|
|
56
153
|
expect(opened).toEqual([]);
|
|
@@ -73,3 +170,64 @@ test("the empty project hint stays centered in the panel", async () => {
|
|
|
73
170
|
expect(position.row).toBeLessThanOrEqual(6);
|
|
74
171
|
setup.renderer.destroy();
|
|
75
172
|
});
|
|
173
|
+
|
|
174
|
+
test("the visible tree removes deleted files without remounting", async () => {
|
|
175
|
+
let files = ["README.md", "gone.ts"];
|
|
176
|
+
const setup = await testRender(
|
|
177
|
+
<ProjectTreeView loadFiles={async () => files} onSelectFile={() => {}} theme={DARK} />,
|
|
178
|
+
{ width: 40, height: 12 },
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
await waitForText(setup, "gone.ts");
|
|
182
|
+
files = ["README.md"];
|
|
183
|
+
await waitForTextGone(setup, "gone.ts");
|
|
184
|
+
expect(setup.captureCharFrame()).toContain("README.md");
|
|
185
|
+
setup.renderer.destroy();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("refresh keeps the selected file when an earlier file disappears", async () => {
|
|
189
|
+
let files = ["a.ts", "b.ts", "c.ts"];
|
|
190
|
+
const opened: string[] = [];
|
|
191
|
+
const setup = await testRender(
|
|
192
|
+
<ProjectTreeView
|
|
193
|
+
loadFiles={async () => files}
|
|
194
|
+
onSelectFile={(path) => opened.push(path)}
|
|
195
|
+
focused
|
|
196
|
+
theme={DARK}
|
|
197
|
+
/>,
|
|
198
|
+
{ width: 40, height: 12 },
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
await waitForText(setup, "b.ts");
|
|
202
|
+
setup.mockInput.pressKey("j");
|
|
203
|
+
await settle(setup);
|
|
204
|
+
files = ["b.ts", "c.ts"];
|
|
205
|
+
await waitForTextGone(setup, "a.ts");
|
|
206
|
+
setup.mockInput.pressKey("RETURN");
|
|
207
|
+
await settle(setup);
|
|
208
|
+
|
|
209
|
+
expect(opened).toEqual(["b.ts"]);
|
|
210
|
+
setup.renderer.destroy();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("a double-click requests a persistent file tab", async () => {
|
|
214
|
+
const opened: { path: string; persistent: boolean }[] = [];
|
|
215
|
+
const setup = await testRender(
|
|
216
|
+
<ProjectTreeView
|
|
217
|
+
loadFiles={async () => ["README.md"]}
|
|
218
|
+
onSelectFile={(path, persistent) => opened.push({ path, persistent: persistent === true })}
|
|
219
|
+
theme={DARK}
|
|
220
|
+
/>,
|
|
221
|
+
{ width: 40, height: 12 },
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
await waitForText(setup, "README.md");
|
|
225
|
+
const file = locateText(setup, "README.md");
|
|
226
|
+
await setup.mockMouse.doubleClick(file.column, file.row);
|
|
227
|
+
expect(opened).toEqual([
|
|
228
|
+
{ path: "README.md", persistent: false },
|
|
229
|
+
{ path: "README.md", persistent: false },
|
|
230
|
+
{ path: "README.md", persistent: true },
|
|
231
|
+
]);
|
|
232
|
+
setup.renderer.destroy();
|
|
233
|
+
});
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// on the session id so switching sessions remounts it and never shows the previous repository's paths.
|
|
4
4
|
|
|
5
5
|
import { ScrollArea } from "./ScrollArea";
|
|
6
|
-
import React, { useEffect,
|
|
6
|
+
import React, { useEffect, useEffectEvent, useMemo, useState } from "react";
|
|
7
7
|
import { useKeyboard } from "@opentui/react";
|
|
8
8
|
import type { Theme } from "../theme";
|
|
9
9
|
import { useComponentTheme } from "./theme-context";
|
|
@@ -11,10 +11,12 @@ import { Tree } from "./primitives/Tree";
|
|
|
11
11
|
import { flattenTree } from "./primitives/tree-model";
|
|
12
12
|
import { buildPathTree } from "./file-tree";
|
|
13
13
|
|
|
14
|
+
const PROJECT_FILES_REFRESH_MS = 2000;
|
|
15
|
+
|
|
14
16
|
export interface ProjectTreeViewProps {
|
|
15
17
|
loadFiles: () => Promise<string[]>;
|
|
16
|
-
onSelectFile: (path: string) => void;
|
|
17
|
-
/** The pane owns the keyboard: j/k and arrows move the cursor
|
|
18
|
+
onSelectFile: (path: string, persistent?: boolean) => void;
|
|
19
|
+
/** The pane owns the keyboard: j/k and arrows move the cursor; Enter opens or folds. */
|
|
18
20
|
focused?: boolean;
|
|
19
21
|
theme?: Theme;
|
|
20
22
|
}
|
|
@@ -28,14 +30,14 @@ export function ProjectTreeView({
|
|
|
28
30
|
const tokens = useComponentTheme(theme);
|
|
29
31
|
const [paths, setPaths] = useState<readonly string[] | null>(null);
|
|
30
32
|
const [expandedIds, setExpandedIds] = useState<ReadonlySet<string>>(new Set());
|
|
31
|
-
const [
|
|
33
|
+
const [cursorId, setCursorId] = useState<string | null>(null);
|
|
32
34
|
const nodes = useMemo(() => (paths ? buildPathTree(paths) : []), [paths]);
|
|
33
35
|
const rows = useMemo(() => flattenTree(nodes, { expandedIds }), [nodes, expandedIds]);
|
|
34
|
-
const cursorIndex = Math.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
const cursorIndex = Math.max(
|
|
37
|
+
0,
|
|
38
|
+
rows.findIndex((row) => row.id === cursorId),
|
|
39
|
+
);
|
|
40
|
+
const loadLatestFiles = useEffectEvent(loadFiles);
|
|
39
41
|
|
|
40
42
|
const toggle = (id: string): void =>
|
|
41
43
|
setExpandedIds((current) => {
|
|
@@ -50,29 +52,49 @@ export function ProjectTreeView({
|
|
|
50
52
|
useKeyboard((key) => {
|
|
51
53
|
if (!focused || rows.length === 0) return;
|
|
52
54
|
if (key.name === "j" || key.name === "down")
|
|
53
|
-
return
|
|
54
|
-
if (key.name === "k" || key.name === "up")
|
|
55
|
+
return setCursorId(rows[Math.min(cursorIndex + 1, rows.length - 1)]!.id);
|
|
56
|
+
if (key.name === "k" || key.name === "up")
|
|
57
|
+
return setCursorId(rows[Math.max(cursorIndex - 1, 0)]!.id);
|
|
55
58
|
const row = rows[cursorIndex];
|
|
56
59
|
if (!row) return;
|
|
57
|
-
if (key.name === "
|
|
58
|
-
return row.isFolder ? toggle(row.id) : onSelectFile(row.id);
|
|
60
|
+
if (key.name === "return" || key.name === "enter")
|
|
61
|
+
return row.isFolder ? toggle(row.id) : onSelectFile(row.id, true);
|
|
59
62
|
if (key.name === "l" && row.isFolder && !expandedIds.has(row.id)) return toggle(row.id);
|
|
60
63
|
if (key.name === "h" && row.isFolder && expandedIds.has(row.id)) return toggle(row.id);
|
|
61
64
|
});
|
|
62
65
|
|
|
63
66
|
useEffect(() => {
|
|
64
|
-
let
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
67
|
+
let cancelled = false;
|
|
68
|
+
let requestInFlight = false;
|
|
69
|
+
|
|
70
|
+
const refreshFiles = async (): Promise<void> => {
|
|
71
|
+
if (requestInFlight) return;
|
|
72
|
+
requestInFlight = true;
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const files = await loadLatestFiles();
|
|
76
|
+
|
|
77
|
+
if (!cancelled) {
|
|
78
|
+
setPaths((previous) =>
|
|
79
|
+
previous?.length === files.length &&
|
|
80
|
+
previous.every((path, index) => path === files[index])
|
|
81
|
+
? previous
|
|
82
|
+
: files,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
} catch {
|
|
86
|
+
if (!cancelled) setPaths((previous) => previous ?? []);
|
|
87
|
+
} finally {
|
|
88
|
+
requestInFlight = false;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
void refreshFiles();
|
|
93
|
+
const timer = setInterval(() => void refreshFiles(), PROJECT_FILES_REFRESH_MS);
|
|
73
94
|
|
|
74
95
|
return () => {
|
|
75
|
-
|
|
96
|
+
cancelled = true;
|
|
97
|
+
clearInterval(timer);
|
|
76
98
|
};
|
|
77
99
|
}, []);
|
|
78
100
|
|
|
@@ -106,17 +128,21 @@ export function ProjectTreeView({
|
|
|
106
128
|
}
|
|
107
129
|
|
|
108
130
|
return (
|
|
109
|
-
<ScrollArea
|
|
131
|
+
<ScrollArea
|
|
132
|
+
id="tree-scroll"
|
|
133
|
+
revealId={focused ? rows[cursorIndex]?.id : undefined}
|
|
134
|
+
gestureWheel
|
|
135
|
+
>
|
|
110
136
|
<Tree
|
|
111
137
|
nodes={nodes}
|
|
112
138
|
expandedIds={expandedIds}
|
|
113
139
|
selectedId={focused ? rows[cursorIndex]?.id : undefined}
|
|
140
|
+
singleLine
|
|
114
141
|
onSelect={(id) => {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if (index >= 0) setCursor(index);
|
|
142
|
+
setCursorId(id);
|
|
118
143
|
onSelectFile(id);
|
|
119
144
|
}}
|
|
145
|
+
onDoubleSelect={(id) => onSelectFile(id, true)}
|
|
120
146
|
onToggle={toggle}
|
|
121
147
|
theme={theme}
|
|
122
148
|
/>
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* Pass scrollRef to keep scrollTo / scrollChildIntoView on the underlying ScrollBox.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import React, { useRef, type ReactNode, type RefObject } from "react";
|
|
8
|
-
import type
|
|
7
|
+
import React, { useLayoutEffect, useMemo, useRef, type ReactNode, type RefObject } from "react";
|
|
8
|
+
import { MacOSScrollAccel, type ScrollBoxRenderable } from "@opentui/core";
|
|
9
9
|
import type { Theme } from "../theme";
|
|
10
10
|
import { OverlayScrollbar } from "./OverlayScrollbar";
|
|
11
11
|
|
|
@@ -14,6 +14,11 @@ export interface ScrollAreaProps {
|
|
|
14
14
|
focused?: boolean;
|
|
15
15
|
theme?: Theme;
|
|
16
16
|
scrollRef?: RefObject<ScrollBoxRenderable | null>;
|
|
17
|
+
id?: string;
|
|
18
|
+
/** Reveal this mounted row when keyboard selection changes. */
|
|
19
|
+
revealId?: string;
|
|
20
|
+
/** Use gesture acceleration for a scrollable file tree. */
|
|
21
|
+
gestureWheel?: boolean;
|
|
17
22
|
}
|
|
18
23
|
|
|
19
24
|
export function ScrollArea({
|
|
@@ -21,19 +26,33 @@ export function ScrollArea({
|
|
|
21
26
|
focused = false,
|
|
22
27
|
theme,
|
|
23
28
|
scrollRef,
|
|
29
|
+
id,
|
|
30
|
+
revealId,
|
|
31
|
+
gestureWheel = false,
|
|
24
32
|
}: ScrollAreaProps): React.ReactNode {
|
|
25
33
|
const innerRef = useRef<ScrollBoxRenderable | null>(null);
|
|
34
|
+
const wheelAcceleration = useMemo(
|
|
35
|
+
() => (gestureWheel ? new MacOSScrollAccel({ A: 0.4, tau: 4, maxMultiplier: 3 }) : undefined),
|
|
36
|
+
[gestureWheel],
|
|
37
|
+
);
|
|
26
38
|
const attach = (node: ScrollBoxRenderable | null): void => {
|
|
27
39
|
innerRef.current = node;
|
|
28
40
|
if (scrollRef) scrollRef.current = node;
|
|
29
41
|
};
|
|
30
42
|
|
|
43
|
+
useLayoutEffect(() => {
|
|
44
|
+
if (revealId === undefined) return;
|
|
45
|
+
innerRef.current?.scrollChildIntoView(`tree-row-${revealId}`);
|
|
46
|
+
}, [revealId]);
|
|
47
|
+
|
|
31
48
|
return (
|
|
32
49
|
<box style={{ flexGrow: 1, flexDirection: "row" }}>
|
|
33
50
|
<scrollbox
|
|
51
|
+
id={id}
|
|
34
52
|
ref={attach}
|
|
35
53
|
style={{ flexGrow: 1 }}
|
|
36
54
|
focused={focused}
|
|
55
|
+
scrollAcceleration={wheelAcceleration}
|
|
37
56
|
verticalScrollbarOptions={{ visible: false }}
|
|
38
57
|
>
|
|
39
58
|
{children}
|
package/src/components/Toast.tsx
CHANGED
|
@@ -7,13 +7,14 @@ import { Dialog } from "./primitives/Dialog";
|
|
|
7
7
|
export interface ToastProps {
|
|
8
8
|
title?: string;
|
|
9
9
|
body: string;
|
|
10
|
+
onDismiss?: () => void;
|
|
10
11
|
theme?: Theme;
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
/** A toast never spans the screen: a long message wraps inside this, like the settings dialog. */
|
|
14
15
|
const TOAST_MAX_WIDTH = 64;
|
|
15
16
|
|
|
16
|
-
export function Toast({ title, body, theme }: ToastProps): React.ReactNode {
|
|
17
|
+
export function Toast({ title, body, onDismiss, theme }: ToastProps): React.ReactNode {
|
|
17
18
|
const tokens = useComponentTheme(theme);
|
|
18
19
|
const { width: terminalWidth, height: terminalHeight } = useTerminalDimensions();
|
|
19
20
|
const width = Math.min(
|
|
@@ -34,6 +35,7 @@ export function Toast({ title, body, theme }: ToastProps): React.ReactNode {
|
|
|
34
35
|
width={width}
|
|
35
36
|
height={height}
|
|
36
37
|
background={tokens.elevated}
|
|
38
|
+
onDismiss={onDismiss}
|
|
37
39
|
theme={theme}
|
|
38
40
|
>
|
|
39
41
|
<box
|
|
@@ -10,7 +10,7 @@ import React from "react";
|
|
|
10
10
|
import { WelcomePlayground } from "./WelcomePlayground";
|
|
11
11
|
import { DARK } from "../theme";
|
|
12
12
|
import { CLIENT_VERSION } from "../version";
|
|
13
|
-
import { dragText, typeText, pressKey, waitForText } from "../test-support";
|
|
13
|
+
import { dragText, locateText, press, typeText, pressKey, waitForText } from "../test-support";
|
|
14
14
|
|
|
15
15
|
describe("WelcomePlayground", () => {
|
|
16
16
|
test("the copy prompts the select-and-type and the slash gestures", async () => {
|
|
@@ -46,4 +46,31 @@ describe("WelcomePlayground", () => {
|
|
|
46
46
|
|
|
47
47
|
setup.renderer.destroy();
|
|
48
48
|
});
|
|
49
|
+
|
|
50
|
+
test("the caret marker remains visible when Up crosses an empty line", async () => {
|
|
51
|
+
const setup = await testRender(<WelcomePlayground quickActions={[]} theme={DARK} />, {
|
|
52
|
+
width: 100,
|
|
53
|
+
height: 28,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
await waitForText(setup, "Start");
|
|
57
|
+
const start = locateText(setup, "Start");
|
|
58
|
+
|
|
59
|
+
await setup.mockMouse.click(start.column, start.row);
|
|
60
|
+
await press(setup, "escape");
|
|
61
|
+
|
|
62
|
+
const markerRows = () =>
|
|
63
|
+
setup
|
|
64
|
+
.captureCharFrame()
|
|
65
|
+
.split("\n")
|
|
66
|
+
.flatMap((line, row) => (line.includes("▎") ? [row] : []));
|
|
67
|
+
|
|
68
|
+
expect(markerRows()).toEqual([start.row]);
|
|
69
|
+
await press(setup, "up");
|
|
70
|
+
expect(markerRows()).toEqual([start.row - 1]);
|
|
71
|
+
await press(setup, "up");
|
|
72
|
+
expect(markerRows()).toEqual([start.row - 2]);
|
|
73
|
+
|
|
74
|
+
setup.renderer.destroy();
|
|
75
|
+
});
|
|
49
76
|
});
|
|
@@ -181,12 +181,12 @@ exports[`stories catalog Chrome/Breadcrumb/SessionHeader renders, snapshots, and
|
|
|
181
181
|
exports[`stories catalog Surfaces/ChangesColumn/Changed renders, snapshots, and carries its colors: Surfaces/ChangesColumn/Changed 1`] = `
|
|
182
182
|
"│ packages
|
|
183
183
|
│ client/src
|
|
184
|
-
│ App.tsx
|
|
184
|
+
│ App.tsx M
|
|
185
185
|
│ components
|
|
186
|
-
│ ChangesColumn.tsx
|
|
186
|
+
│ ChangesColumn.tsx A
|
|
187
187
|
│ daemon/src
|
|
188
|
-
│ thread-review.ts
|
|
189
|
-
│ README.md
|
|
188
|
+
│ thread-review.ts D
|
|
189
|
+
│ README.md M
|
|
190
190
|
│
|
|
191
191
|
│
|
|
192
192
|
│
|
|
@@ -555,7 +555,7 @@ exports[`stories catalog Chrome/DiscussionMarkerRail/Dots renders, snapshots, an
|
|
|
555
555
|
`;
|
|
556
556
|
|
|
557
557
|
exports[`stories catalog Surfaces/EditorGrid/OneGroup renders, snapshots, and carries its colors: Surfaces/EditorGrid/OneGroup 1`] = `
|
|
558
|
-
" Changes │ App.tsx │
|
|
558
|
+
" Changes │ App.tsx │ split ⛶
|
|
559
559
|
‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
|
|
560
560
|
|
|
561
561
|
the whole diff
|
|
@@ -575,22 +575,42 @@ exports[`stories catalog Surfaces/EditorGrid/OneGroup renders, snapshots, and ca
|
|
|
575
575
|
`;
|
|
576
576
|
|
|
577
577
|
exports[`stories catalog Surfaces/EditorGrid/SplitGroups renders, snapshots, and carries its colors: Surfaces/EditorGrid/SplitGroups 1`] = `
|
|
578
|
-
"
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
578
|
+
" Changes │ split │ plugin.json │ split ⛶
|
|
579
|
+
‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾│‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
|
|
580
|
+
│
|
|
581
|
+
the whole diff │ .claude-plugin/plugin.json
|
|
582
|
+
│
|
|
583
|
+
│
|
|
584
|
+
│
|
|
585
|
+
│
|
|
586
|
+
│
|
|
587
|
+
│
|
|
588
|
+
│
|
|
589
|
+
│
|
|
590
|
+
│
|
|
591
|
+
│
|
|
592
|
+
│
|
|
593
|
+
│
|
|
594
|
+
"
|
|
595
|
+
`;
|
|
596
|
+
|
|
597
|
+
exports[`stories catalog Surfaces/EditorGrid/WholeCellSizing renders, snapshots, and carries its colors: Surfaces/EditorGrid/WholeCellSizing 1`] = `
|
|
598
|
+
"Whole-cell split sizing
|
|
599
|
+
100 cells = 49 left + 1 divider + 50 right
|
|
600
|
+
Fix the leading width; let the last group take the remainder.
|
|
601
|
+
Changes │ split │ plugin.json │ split ⛶
|
|
602
|
+
‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾│‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
|
|
603
|
+
│
|
|
604
|
+
the whole diff │ .claude-plugin/plugin.json
|
|
605
|
+
│
|
|
606
|
+
│
|
|
607
|
+
│
|
|
608
|
+
│
|
|
609
|
+
│
|
|
610
|
+
│
|
|
611
|
+
│
|
|
612
|
+
│
|
|
613
|
+
│
|
|
594
614
|
"
|
|
595
615
|
`;
|
|
596
616
|
|