@cueloop/client 0.1.0-alpha.92 → 0.1.0-alpha.94
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 +10 -10
- 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 +52 -12
- package/src/App.workbench.test.tsx +290 -2
- package/src/app-screens.tsx +34 -8
- 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/run.ts +6 -0
- package/src/thread-controller.curate.test.ts +27 -0
- package/src/thread-controller.live-diff.test.ts +30 -0
- package/src/thread-controller.ts +29 -7
- 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-repo-changes.ts +5 -1
- package/src/use-settings-dialog.tsx +1 -13
- package/src/use-shared-keyboard.ts +55 -0
- package/src/use-terminal-virtualizer.ts +8 -2
|
@@ -112,7 +112,127 @@ const rightToggleColumn = (setup: Setup): number => toggleColumn(setup, NERD.sid
|
|
|
112
112
|
// the collapsed rail shows the outline (off) variant, mirroring the left toggle's filled/outline states
|
|
113
113
|
const railToggleColumn = (setup: Setup): number => toggleColumn(setup, NERD.sidebarRightOff);
|
|
114
114
|
|
|
115
|
+
function changedFileVisibleInRightTree(frame: string, file: string): boolean {
|
|
116
|
+
return frame.split("\n").some((line) => {
|
|
117
|
+
const divider = line.lastIndexOf("│");
|
|
118
|
+
|
|
119
|
+
return divider >= 0 && line.slice(divider + 1).includes(file);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
115
123
|
describe("the four-pane workbench", () => {
|
|
124
|
+
test("a bootstrapped workbench paints its changed-file list in the first usable frame", async () => {
|
|
125
|
+
const workbench = server.core.sessionCreate({
|
|
126
|
+
workspace: { repoRoot: repo, branch: "main" },
|
|
127
|
+
artifact: { type: "diff", content: "", meta: { title: "Workbench", workbench: true } },
|
|
128
|
+
});
|
|
129
|
+
const initialReview = {
|
|
130
|
+
session: workbench,
|
|
131
|
+
diff: {
|
|
132
|
+
patch: PATCH,
|
|
133
|
+
files: [
|
|
134
|
+
{
|
|
135
|
+
path: "src/store.ts",
|
|
136
|
+
oldContents: "export class Store { private items = []; }\n",
|
|
137
|
+
newContents: "export class Store { private items = new Map(); }\n",
|
|
138
|
+
status: "modified" as const,
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
const setup = await renderReadyApp(
|
|
144
|
+
<App
|
|
145
|
+
home={home}
|
|
146
|
+
sessionId={workbench.id}
|
|
147
|
+
initialReview={initialReview}
|
|
148
|
+
layout={reviewLayout()}
|
|
149
|
+
/>,
|
|
150
|
+
{ width: 160, height: 20 },
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
expect(changedFileVisibleInRightTree(setup.captureCharFrame(), "store.ts")).toBe(true);
|
|
154
|
+
setup.renderer.destroy();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("reopening Changes does not restore files from the launch snapshot", async () => {
|
|
158
|
+
const workbench = server.core.sessionCreate({
|
|
159
|
+
workspace: { repoRoot: repo, branch: "main" },
|
|
160
|
+
artifact: { type: "diff", content: "", meta: { title: "Workbench", workbench: true } },
|
|
161
|
+
});
|
|
162
|
+
const initialReview = {
|
|
163
|
+
session: workbench,
|
|
164
|
+
diff: {
|
|
165
|
+
patch: PATCH,
|
|
166
|
+
files: [
|
|
167
|
+
{
|
|
168
|
+
path: "src/store.ts",
|
|
169
|
+
oldContents: "before\n",
|
|
170
|
+
newContents: "after\n",
|
|
171
|
+
status: "modified" as const,
|
|
172
|
+
},
|
|
173
|
+
],
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
const setup = await renderReadyApp(
|
|
177
|
+
<App
|
|
178
|
+
home={home}
|
|
179
|
+
sessionId={workbench.id}
|
|
180
|
+
initialReview={initialReview}
|
|
181
|
+
layout={reviewLayout()}
|
|
182
|
+
/>,
|
|
183
|
+
{ width: 160, height: 20 },
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
await waitForState(
|
|
187
|
+
setup,
|
|
188
|
+
() => !changedFileVisibleInRightTree(setup.captureCharFrame(), "store.ts"),
|
|
189
|
+
);
|
|
190
|
+
await setup.mockMouse.click(rightToggleColumn(setup), HEADER_ROW);
|
|
191
|
+
await waitForState(setup, () => diffToggleColumn(setup) === -1);
|
|
192
|
+
await setup.mockMouse.click(railToggleColumn(setup), HEADER_ROW);
|
|
193
|
+
|
|
194
|
+
expect(changedFileVisibleInRightTree(setup.captureCharFrame(), "store.ts")).toBe(false);
|
|
195
|
+
setup.renderer.destroy();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("rapid wheel scrolling a large Changes diff keeps the app responsive", async () => {
|
|
199
|
+
const filePatches = Array.from({ length: 40 }, (_, fileIndex) => [
|
|
200
|
+
`diff --git a/file-${fileIndex}.ts b/file-${fileIndex}.ts`,
|
|
201
|
+
`--- a/file-${fileIndex}.ts`,
|
|
202
|
+
`+++ b/file-${fileIndex}.ts`,
|
|
203
|
+
"@@ -0,0 +1,10 @@",
|
|
204
|
+
...Array.from(
|
|
205
|
+
{ length: 10 },
|
|
206
|
+
(_, lineIndex) => `+line ${lineIndex} ${"dense source text ".repeat(40)}`,
|
|
207
|
+
),
|
|
208
|
+
]).flat();
|
|
209
|
+
const large = server.core.sessionCreate({
|
|
210
|
+
workspace: { repoRoot: repo, branch: "main" },
|
|
211
|
+
artifact: {
|
|
212
|
+
type: "diff",
|
|
213
|
+
content: [...filePatches, ""].join("\n"),
|
|
214
|
+
meta: { title: "large changes" },
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
const setup = await renderReadyApp(<App home={home} sessionId={large.id} />, {
|
|
218
|
+
width: 160,
|
|
219
|
+
height: 20,
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
await waitForText(setup, "file-0.ts");
|
|
223
|
+
for (const [index, direction] of Array.from({ length: 500 }, () => "down" as const).entries()) {
|
|
224
|
+
// eslint-disable-next-line no-await-in-loop
|
|
225
|
+
await setup.mockMouse.scroll(75, 10, direction);
|
|
226
|
+
if (index % 25 === 0) {
|
|
227
|
+
// eslint-disable-next-line no-await-in-loop
|
|
228
|
+
await setup.waitForVisualIdle();
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
await setup.waitForVisualIdle();
|
|
232
|
+
expect(setup.captureCharFrame()).toContain("changes");
|
|
233
|
+
setup.renderer.destroy();
|
|
234
|
+
}, 60000);
|
|
235
|
+
|
|
116
236
|
test("pair starts focused on the Project tree beside the zoomed Changes editor", async () => {
|
|
117
237
|
const setup = await renderReadyApp(
|
|
118
238
|
<App home={home} sessionId={session.id} layout={pairLayout()} />,
|
|
@@ -198,6 +318,8 @@ describe("the four-pane workbench", () => {
|
|
|
198
318
|
expect(frame).toContain("cueloop");
|
|
199
319
|
expect(frame).toContain("Changes");
|
|
200
320
|
expect(frame).toContain("store.ts");
|
|
321
|
+
expect(frame).toContain("src/store.ts");
|
|
322
|
+
expect(frame).toContain(NERD.copy);
|
|
201
323
|
// the diff (±) toggle exists in the Project header
|
|
202
324
|
expect(diffToggleColumn(setup)).toBeGreaterThan(0);
|
|
203
325
|
});
|
|
@@ -233,6 +355,43 @@ describe("the four-pane workbench", () => {
|
|
|
233
355
|
expect(frame.split("\n")[HEADER_ROW]!).toContain("README.md");
|
|
234
356
|
});
|
|
235
357
|
|
|
358
|
+
test("project clicks reuse one preview and a double-click keeps the file open once", async () => {
|
|
359
|
+
const setup = await renderApp();
|
|
360
|
+
|
|
361
|
+
await setup.mockMouse.click(treeToggleColumn(setup), HEADER_ROW);
|
|
362
|
+
await waitForText(setup, "README.md");
|
|
363
|
+
const readme = locateText(setup, "README.md");
|
|
364
|
+
await setup.mockMouse.click(readme.column, readme.row);
|
|
365
|
+
await waitForState(setup, () =>
|
|
366
|
+
setup.captureCharFrame().split("\n")[HEADER_ROW]!.includes("README.md"),
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
const folder = locateText(setup, "src");
|
|
370
|
+
await setup.mockMouse.click(folder.column, folder.row);
|
|
371
|
+
await waitForText(setup, "util.ts");
|
|
372
|
+
const util = locateText(setup, "util.ts");
|
|
373
|
+
await setup.mockMouse.click(util.column, util.row);
|
|
374
|
+
await waitForState(setup, () => {
|
|
375
|
+
const header = setup.captureCharFrame().split("\n")[HEADER_ROW]!;
|
|
376
|
+
|
|
377
|
+
return header.includes("util.ts") && !header.includes("README.md");
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
await setup.mockMouse.doubleClick(util.column, util.row);
|
|
381
|
+
await setup.mockMouse.click(readme.column, readme.row);
|
|
382
|
+
await waitForState(setup, () => {
|
|
383
|
+
const header = setup.captureCharFrame().split("\n")[HEADER_ROW]!;
|
|
384
|
+
|
|
385
|
+
return header.includes("util.ts") && header.includes("README.md");
|
|
386
|
+
});
|
|
387
|
+
await setup.mockMouse.click(util.column, util.row);
|
|
388
|
+
const header = setup.captureCharFrame().split("\n")[HEADER_ROW]!;
|
|
389
|
+
|
|
390
|
+
expect(header.match(/util\.ts/g)).toHaveLength(1);
|
|
391
|
+
expect(header.match(/README\.md/g)).toHaveLength(1);
|
|
392
|
+
setup.renderer.destroy();
|
|
393
|
+
});
|
|
394
|
+
|
|
236
395
|
test("the split control offers four directions and arrow-right makes two groups", async () => {
|
|
237
396
|
const setup = await renderApp();
|
|
238
397
|
|
|
@@ -255,8 +414,15 @@ describe("the four-pane workbench", () => {
|
|
|
255
414
|
|
|
256
415
|
// the arrow keys drive the popover: right splits the group into two
|
|
257
416
|
await pressKey(setup, "ARROW_RIGHT");
|
|
258
|
-
await waitForState(setup, () =>
|
|
259
|
-
|
|
417
|
+
await waitForState(setup, () =>
|
|
418
|
+
/│ Changes\s+│.*│ README\.md/.test(setup.captureCharFrame().split("\n")[HEADER_ROW]!),
|
|
419
|
+
);
|
|
420
|
+
expect(
|
|
421
|
+
setup
|
|
422
|
+
.captureCharFrame()
|
|
423
|
+
.split("\n")
|
|
424
|
+
[HEADER_ROW]!.match(/README\.md/g),
|
|
425
|
+
).toHaveLength(1);
|
|
260
426
|
});
|
|
261
427
|
|
|
262
428
|
test("dismissing the Changes tab collapses the Changes pane but keeps the Project sidebar", async () => {
|
|
@@ -461,6 +627,28 @@ describe("the bare-launch welcome shell", () => {
|
|
|
461
627
|
expect(setup.captureCharFrame()).toContain("README.md");
|
|
462
628
|
});
|
|
463
629
|
|
|
630
|
+
test("Review shows only the skill selected in TOML", async () => {
|
|
631
|
+
writeFileSync(process.env.CUELOOP_CONFIG!, '[review]\nskill = "custom-review"\n');
|
|
632
|
+
const setup = await renderWelcome();
|
|
633
|
+
|
|
634
|
+
await setup.mockMouse.click(1, HEADER_ROW);
|
|
635
|
+
await waitForText(setup, "settings");
|
|
636
|
+
const settings = locateText(setup, "settings");
|
|
637
|
+
|
|
638
|
+
await setup.mockMouse.click(settings.column, settings.row);
|
|
639
|
+
await waitForText(setup, "General");
|
|
640
|
+
const review = locateText(setup, "Review");
|
|
641
|
+
|
|
642
|
+
await setup.mockMouse.click(review.column, review.row);
|
|
643
|
+
await waitForText(setup, "custom-review");
|
|
644
|
+
expect(setup.captureCharFrame()).not.toContain("research");
|
|
645
|
+
const skill = locateText(setup, "custom-review");
|
|
646
|
+
|
|
647
|
+
await setup.mockMouse.click(skill.column, skill.row);
|
|
648
|
+
expect(setup.captureCharFrame()).toContain("custom-review");
|
|
649
|
+
setup.renderer.destroy();
|
|
650
|
+
});
|
|
651
|
+
|
|
464
652
|
test("the first comment in the bare shell creates and persists a per-repo workbench thread", async () => {
|
|
465
653
|
const setup = await renderWelcome();
|
|
466
654
|
|
|
@@ -501,6 +689,63 @@ describe("the bare-launch welcome shell", () => {
|
|
|
501
689
|
expect(setup.captureCharFrame()).toContain("README.md");
|
|
502
690
|
});
|
|
503
691
|
|
|
692
|
+
test("a changed file tab keeps copy and expand without a collapse action", async () => {
|
|
693
|
+
const setup = await renderWelcome();
|
|
694
|
+
|
|
695
|
+
await waitForText(setup, "README.md");
|
|
696
|
+
const readme = locateText(setup, "README.md");
|
|
697
|
+
|
|
698
|
+
await setup.mockMouse.click(readme.column, readme.row);
|
|
699
|
+
await waitForText(setup, "edited in the working tree");
|
|
700
|
+
const frame = setup.captureCharFrame();
|
|
701
|
+
|
|
702
|
+
expect(frame).toContain(NERD.copy);
|
|
703
|
+
expect(frame).not.toContain("v README.md");
|
|
704
|
+
expect(frame).toContain("expand");
|
|
705
|
+
setup.renderer.destroy();
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
test("a bare changed-file tab uses Split only when zoomed in", async () => {
|
|
709
|
+
const setup = await renderWelcome();
|
|
710
|
+
|
|
711
|
+
await waitForText(setup, "README.md");
|
|
712
|
+
const readme = locateText(setup, "README.md");
|
|
713
|
+
|
|
714
|
+
await setup.mockMouse.click(readme.column, readme.row);
|
|
715
|
+
await waitForText(setup, "edited in the working tree");
|
|
716
|
+
const pairedLine = (line: string): boolean =>
|
|
717
|
+
line.includes("Workbench Fixture") && line.includes("+ # Fixture");
|
|
718
|
+
|
|
719
|
+
expect(setup.captureCharFrame().split("\n").some(pairedLine)).toBe(false);
|
|
720
|
+
const zoom = locateText(setup, NERD.zoom);
|
|
721
|
+
|
|
722
|
+
await setup.mockMouse.click(zoom.column, zoom.row);
|
|
723
|
+
await waitForState(setup, () => setup.captureCharFrame().split("\n").some(pairedLine));
|
|
724
|
+
expect(setup.captureCharFrame().split("\n").some(pairedLine)).toBe(true);
|
|
725
|
+
setup.renderer.destroy();
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
test("reopening Changes after dismissing Welcome keeps file actions", async () => {
|
|
729
|
+
const setup = await renderWelcome();
|
|
730
|
+
const welcome = locateText(setup, "Welcome");
|
|
731
|
+
|
|
732
|
+
await setup.mockMouse.click(welcome.column + 8, welcome.row);
|
|
733
|
+
await setup.mockMouse.click(diffToggleColumn(setup), HEADER_ROW);
|
|
734
|
+
await waitForText(setup, "edited in the working tree");
|
|
735
|
+
const frame = setup.captureCharFrame();
|
|
736
|
+
|
|
737
|
+
expect(frame).toContain("README.md");
|
|
738
|
+
expect(frame).toContain(NERD.copy);
|
|
739
|
+
const title = locateText(setup, "README.md");
|
|
740
|
+
|
|
741
|
+
await setup.mockMouse.click(title.column, title.row);
|
|
742
|
+
await waitForTextGone(setup, "edited in the working tree");
|
|
743
|
+
expect(setup.captureCharFrame()).toContain("README.md");
|
|
744
|
+
await setup.mockMouse.click(title.column, title.row);
|
|
745
|
+
await waitForText(setup, "edited in the working tree");
|
|
746
|
+
setup.renderer.destroy();
|
|
747
|
+
});
|
|
748
|
+
|
|
504
749
|
test("the changes tree clears after the launch repo becomes clean", async () => {
|
|
505
750
|
const setup = await renderWelcome();
|
|
506
751
|
|
|
@@ -766,6 +1011,49 @@ describe("the bare-launch welcome shell", () => {
|
|
|
766
1011
|
await waitForState(setup, () => diffToggleColumn(setup) === -1);
|
|
767
1012
|
});
|
|
768
1013
|
|
|
1014
|
+
test("the changes sidebar label opens one Changes tab beside Welcome and reactivates it", async () => {
|
|
1015
|
+
const setup = await renderWelcome();
|
|
1016
|
+
|
|
1017
|
+
await setup.mockMouse.click(diffToggleColumn(setup), HEADER_ROW);
|
|
1018
|
+
await waitForText(setup, "edited in the working tree");
|
|
1019
|
+
const firstHeader = setup.captureCharFrame().split("\n")[HEADER_ROW]!;
|
|
1020
|
+
|
|
1021
|
+
expect(firstHeader).toContain("Welcome");
|
|
1022
|
+
expect(firstHeader).toContain("Changes");
|
|
1023
|
+
|
|
1024
|
+
const welcome = locateText(setup, "Welcome");
|
|
1025
|
+
|
|
1026
|
+
await setup.mockMouse.click(welcome.column, welcome.row);
|
|
1027
|
+
await waitForText(setup, "Getting started");
|
|
1028
|
+
await setup.mockMouse.click(diffToggleColumn(setup), HEADER_ROW);
|
|
1029
|
+
await waitForText(setup, "edited in the working tree");
|
|
1030
|
+
expect(
|
|
1031
|
+
setup
|
|
1032
|
+
.captureCharFrame()
|
|
1033
|
+
.split("\n")
|
|
1034
|
+
[HEADER_ROW]!.match(/Changes/g),
|
|
1035
|
+
).toHaveLength(1);
|
|
1036
|
+
setup.renderer.destroy();
|
|
1037
|
+
});
|
|
1038
|
+
|
|
1039
|
+
test("the changes sidebar label activates Changes beside an open file tab", async () => {
|
|
1040
|
+
const setup = await renderWelcome();
|
|
1041
|
+
|
|
1042
|
+
await waitForText(setup, "README.md");
|
|
1043
|
+
const file = locateText(setup, "README.md");
|
|
1044
|
+
|
|
1045
|
+
await setup.mockMouse.click(file.column, file.row);
|
|
1046
|
+
await waitForText(setup, "README.md");
|
|
1047
|
+
await setup.mockMouse.click(diffToggleColumn(setup), HEADER_ROW);
|
|
1048
|
+
await waitForText(setup, "edited in the working tree");
|
|
1049
|
+
const header = setup.captureCharFrame().split("\n")[HEADER_ROW]!;
|
|
1050
|
+
|
|
1051
|
+
expect(header).toContain("Welcome");
|
|
1052
|
+
expect(header).toContain("README.md");
|
|
1053
|
+
expect(header).toContain("Changes");
|
|
1054
|
+
setup.renderer.destroy();
|
|
1055
|
+
});
|
|
1056
|
+
|
|
769
1057
|
test("a closed region draws its reopen divider in the header only, not full-height", async () => {
|
|
770
1058
|
const setup = await renderWelcome();
|
|
771
1059
|
|
package/src/app-screens.tsx
CHANGED
|
@@ -3,7 +3,7 @@ import React, { useCallback, useMemo, type Dispatch, type SetStateAction } from
|
|
|
3
3
|
import type { Thread, MessageOutcome } from "@cueloop/schema";
|
|
4
4
|
import { returnPaneFor } from "@cueloop/schema";
|
|
5
5
|
import type { Theme } from "./theme";
|
|
6
|
-
import type { QuickAction } from "./config";
|
|
6
|
+
import type { DiffViewMode, QuickAction } from "./config";
|
|
7
7
|
import type { LaunchLayout } from "./launch-layout";
|
|
8
8
|
import { useRememberLayout } from "./use-remember-layout";
|
|
9
9
|
import type { Mode, TreeAsk } from "./intent-dispatch";
|
|
@@ -30,6 +30,7 @@ import { ProjectTreeView } from "./components/ProjectTreeView";
|
|
|
30
30
|
import { ChangesFileTree } from "./components/ChangesColumn";
|
|
31
31
|
import { BareWorkbenchFileView, draftThread } from "./components/BareWorkbenchFileView";
|
|
32
32
|
import { GridTabContent, type DiffSurfaceProps } from "./components/GridTabContent";
|
|
33
|
+
import type { DiffFoldControls } from "./components/DiffContentView";
|
|
33
34
|
import { useChangesWorkbench } from "./use-changes-workbench";
|
|
34
35
|
import { useRepoChanges } from "./use-repo-changes";
|
|
35
36
|
import { ConfirmDialog } from "./components/ConfirmDialog";
|
|
@@ -112,9 +113,9 @@ function WelcomeProjectPanel({
|
|
|
112
113
|
mode: ProjectPanelMode;
|
|
113
114
|
controller: ReviewController;
|
|
114
115
|
/** Changes tree click -> open the file's working-tree diff. */
|
|
115
|
-
onOpenChangedFile: (path: string) => void;
|
|
116
|
+
onOpenChangedFile: (path: string, persistent?: boolean) => void;
|
|
116
117
|
/** Project tree click -> open the file's read-only contents. */
|
|
117
|
-
onOpenProjectFile: (path: string) => void;
|
|
118
|
+
onOpenProjectFile: (path: string, persistent?: boolean) => void;
|
|
118
119
|
focused?: boolean;
|
|
119
120
|
theme: Theme;
|
|
120
121
|
}): React.ReactNode {
|
|
@@ -177,6 +178,7 @@ export function NoThreadShell(props: {
|
|
|
177
178
|
onRename: (id: string, title: string) => void;
|
|
178
179
|
/** Drives the "/" quick actions and skills in the welcome playground's composer. */
|
|
179
180
|
quickActions: QuickAction[];
|
|
181
|
+
diffView: DiffViewMode;
|
|
180
182
|
/** Reports the welcome composer's open state, so the shell suspends its inbox keys while typing. */
|
|
181
183
|
onWelcomeComposingChange: (composing: boolean) => void;
|
|
182
184
|
/** The pane composition this vanilla launch restores; changes here are remembered for the next one. */
|
|
@@ -201,6 +203,7 @@ export function NoThreadShell(props: {
|
|
|
201
203
|
onPin,
|
|
202
204
|
onRename,
|
|
203
205
|
quickActions,
|
|
206
|
+
diffView,
|
|
204
207
|
onWelcomeComposingChange,
|
|
205
208
|
layout,
|
|
206
209
|
} = props;
|
|
@@ -222,13 +225,14 @@ export function NoThreadShell(props: {
|
|
|
222
225
|
workbench.zoomed,
|
|
223
226
|
);
|
|
224
227
|
// the Changes tree opens a file's working-tree diff; the Project tree opens read-only contents
|
|
225
|
-
const openChangedFile = (path: string): void => {
|
|
228
|
+
const openChangedFile = (path: string, persistent?: boolean): void => {
|
|
226
229
|
// no thread means no live-diff refresh loop, so re-capture on open or a long-lived shell goes
|
|
227
230
|
// stale; the catch keeps a fire-and-forget refresh from throwing when the shell tears down
|
|
228
231
|
void controller.repoChanges().catch(() => undefined);
|
|
229
|
-
workbench.openFile(path, "diff");
|
|
232
|
+
workbench.openFile(path, "diff", persistent);
|
|
230
233
|
};
|
|
231
|
-
const openProjectFile = (path: string): void =>
|
|
234
|
+
const openProjectFile = (path: string, persistent?: boolean): void =>
|
|
235
|
+
workbench.openFile(path, "contents", persistent);
|
|
232
236
|
// a bare launch has no thread yet, so the diff renders against a draft session; the first note
|
|
233
237
|
// promotes it to the per-repo workbench thread (commentOnWorkbenchDiff)
|
|
234
238
|
const draft = useMemo(() => draftThread(), []);
|
|
@@ -256,6 +260,15 @@ export function NoThreadShell(props: {
|
|
|
256
260
|
onUpdateAnnotation: () => {},
|
|
257
261
|
onExit: () => {},
|
|
258
262
|
};
|
|
263
|
+
const diffFold: DiffFoldControls = {
|
|
264
|
+
isCollapsed: (file) => controller.isFileCollapsed(file),
|
|
265
|
+
isExpanded: (file) => controller.isFileExpanded(file),
|
|
266
|
+
canExpand: (file) => controller.canExpandFile(file),
|
|
267
|
+
onToggleCollapse: (file) =>
|
|
268
|
+
controller.setFileCollapsed(file, !controller.isFileCollapsed(file)),
|
|
269
|
+
onToggleExpand: (file) => controller.setFileExpanded(file, !controller.isFileExpanded(file)),
|
|
270
|
+
onCopyPath: (file) => controller.copyFilePath(file),
|
|
271
|
+
};
|
|
259
272
|
|
|
260
273
|
return (
|
|
261
274
|
<ThemeProvider theme={theme}>
|
|
@@ -313,6 +326,7 @@ export function NoThreadShell(props: {
|
|
|
313
326
|
focusedGroupId={workbench.activeGroup}
|
|
314
327
|
onFocusGroup={workbench.focusGroup}
|
|
315
328
|
onActivateTab={workbench.activate}
|
|
329
|
+
onKeepTab={workbench.keepTab}
|
|
316
330
|
onCloseTab={workbench.close}
|
|
317
331
|
onSplit={workbench.split}
|
|
318
332
|
onZoom={workbench.toggleZoom}
|
|
@@ -340,6 +354,9 @@ export function NoThreadShell(props: {
|
|
|
340
354
|
rows={controller.rows()}
|
|
341
355
|
surface={bareSurface}
|
|
342
356
|
rejectedRows={EMPTY_ROWS}
|
|
357
|
+
fold={diffFold}
|
|
358
|
+
fileStats={controller.fileStats()}
|
|
359
|
+
split={diffView === "split" && workbench.zoomed}
|
|
343
360
|
dimmed={false}
|
|
344
361
|
readFile={(path) => controller.repoReadFile(path)}
|
|
345
362
|
onAddFileComment={(path, anchor, body) =>
|
|
@@ -396,7 +413,12 @@ export function NoThreadShell(props: {
|
|
|
396
413
|
/>
|
|
397
414
|
) : null}
|
|
398
415
|
{props.toast ? (
|
|
399
|
-
<Toast
|
|
416
|
+
<Toast
|
|
417
|
+
title={props.toast.title}
|
|
418
|
+
body={props.toast.body}
|
|
419
|
+
onDismiss={() => controller.dismissToast()}
|
|
420
|
+
theme={theme}
|
|
421
|
+
/>
|
|
400
422
|
) : null}
|
|
401
423
|
</AppShell>
|
|
402
424
|
</MenuControlProvider>
|
|
@@ -460,6 +482,7 @@ export function TrailingOverlays(props: {
|
|
|
460
482
|
theme: Theme;
|
|
461
483
|
mode: Mode;
|
|
462
484
|
toast: ToastState | null;
|
|
485
|
+
onDismissToast: () => void;
|
|
463
486
|
setMode: Dispatch<SetStateAction<Mode>>;
|
|
464
487
|
dispatch: (intent: Intent) => void;
|
|
465
488
|
}): React.ReactNode {
|
|
@@ -473,6 +496,7 @@ export function TrailingOverlays(props: {
|
|
|
473
496
|
theme,
|
|
474
497
|
mode,
|
|
475
498
|
toast,
|
|
499
|
+
onDismissToast,
|
|
476
500
|
setMode,
|
|
477
501
|
dispatch,
|
|
478
502
|
} = props;
|
|
@@ -571,7 +595,9 @@ export function TrailingOverlays(props: {
|
|
|
571
595
|
theme={theme}
|
|
572
596
|
/>
|
|
573
597
|
) : null}
|
|
574
|
-
{toast ?
|
|
598
|
+
{toast ? (
|
|
599
|
+
<Toast title={toast.title} body={toast.body} onDismiss={onDismissToast} theme={theme} />
|
|
600
|
+
) : null}
|
|
575
601
|
</>
|
|
576
602
|
);
|
|
577
603
|
}
|
|
@@ -11,7 +11,7 @@ import { testRender } from "@opentui/react/test-utils";
|
|
|
11
11
|
import React from "react";
|
|
12
12
|
import { AppShell } from "./AppShell";
|
|
13
13
|
import { EditorGrid } from "./EditorGrid";
|
|
14
|
-
import { changesTab, makeGroup } from "./editor-grid";
|
|
14
|
+
import { changesTab, makeGroup, splitGroup } from "./editor-grid";
|
|
15
15
|
import { NERD } from "./primitives/icons";
|
|
16
16
|
import { allowEventLoopUpdates } from "../test-support";
|
|
17
17
|
|
|
@@ -21,7 +21,11 @@ async function zoomIconColumn(
|
|
|
21
21
|
width: number,
|
|
22
22
|
zoomed: boolean,
|
|
23
23
|
sidebarOpen: boolean,
|
|
24
|
+
splitEditor = false,
|
|
24
25
|
): Promise<number> {
|
|
26
|
+
const editor = splitEditor
|
|
27
|
+
? splitGroup(grid, grid.id, "right")
|
|
28
|
+
: { tree: grid, focusGroupId: grid.id };
|
|
25
29
|
const setup = await testRender(
|
|
26
30
|
<AppShell
|
|
27
31
|
sidebarOpen={sidebarOpen}
|
|
@@ -40,8 +44,8 @@ async function zoomIconColumn(
|
|
|
40
44
|
projectPanel={<text>project</text>}
|
|
41
45
|
changesPanel={
|
|
42
46
|
<EditorGrid
|
|
43
|
-
tree={
|
|
44
|
-
focusedGroupId={
|
|
47
|
+
tree={editor.tree}
|
|
48
|
+
focusedGroupId={editor.focusGroupId}
|
|
45
49
|
onFocusGroup={() => {}}
|
|
46
50
|
onActivateTab={() => {}}
|
|
47
51
|
onCloseTab={() => {}}
|
|
@@ -132,4 +136,13 @@ describe("zoom icon in the shell", () => {
|
|
|
132
136
|
// before it, the same inset the right-sidebar icon keeps from its own edge
|
|
133
137
|
for (const column of setupColumns) expect(128 - column).toBe(2);
|
|
134
138
|
});
|
|
139
|
+
|
|
140
|
+
test("a split editor keeps the zoom icon clear of the Project border", async () => {
|
|
141
|
+
const columns = await Promise.all([
|
|
142
|
+
zoomIconColumn(160, false, false, true),
|
|
143
|
+
zoomIconColumn(160, true, false, true),
|
|
144
|
+
]);
|
|
145
|
+
|
|
146
|
+
for (const column of columns) expect(128 - column).toBe(2);
|
|
147
|
+
});
|
|
135
148
|
});
|
|
@@ -86,7 +86,7 @@ export interface ChangesColumnProps {
|
|
|
86
86
|
files?: readonly DiffFileContents[];
|
|
87
87
|
/** The path whose diff the sheet is scrolled to, highlighted in the tree. */
|
|
88
88
|
selectedPath?: string;
|
|
89
|
-
onSelectFile: (path: string) => void;
|
|
89
|
+
onSelectFile: (path: string, persistent?: boolean) => void;
|
|
90
90
|
/** Comments per file path, shown as a dot-and-count badge on each entry. */
|
|
91
91
|
commentCounts?: ReadonlyMap<string, number>;
|
|
92
92
|
focused?: boolean;
|
|
@@ -141,18 +141,23 @@ export function ChangesFileTree({
|
|
|
141
141
|
if (key.name === "k" || key.name === "up") return setCursor(Math.max(cursorIndex - 1, 0));
|
|
142
142
|
const row = rows[cursorIndex];
|
|
143
143
|
if (!row) return;
|
|
144
|
-
if (key.name === "
|
|
145
|
-
return row.isFolder ? toggle(row.id) : onSelectFile(row.id);
|
|
144
|
+
if (key.name === "return" || key.name === "enter")
|
|
145
|
+
return row.isFolder ? toggle(row.id) : onSelectFile(row.id, true);
|
|
146
146
|
if (key.name === "l" && row.isFolder && collapsedIds.has(row.id)) return toggle(row.id);
|
|
147
147
|
if (key.name === "h" && row.isFolder && !collapsedIds.has(row.id)) return toggle(row.id);
|
|
148
148
|
});
|
|
149
149
|
|
|
150
150
|
return (
|
|
151
|
-
<ScrollArea
|
|
151
|
+
<ScrollArea
|
|
152
|
+
id="tree-scroll"
|
|
153
|
+
revealId={focused ? rows[cursorIndex]?.id : undefined}
|
|
154
|
+
gestureWheel
|
|
155
|
+
>
|
|
152
156
|
<Tree
|
|
153
157
|
nodes={nodes}
|
|
154
158
|
expandedIds={expandedIds}
|
|
155
159
|
selectedId={focused ? rows[cursorIndex]?.id : selectedPath}
|
|
160
|
+
singleLine
|
|
156
161
|
flattenEmptyDirectories
|
|
157
162
|
showStatus
|
|
158
163
|
onSelect={(id) => {
|
|
@@ -161,6 +166,7 @@ export function ChangesFileTree({
|
|
|
161
166
|
if (index >= 0) setCursor(index);
|
|
162
167
|
onSelectFile(id);
|
|
163
168
|
}}
|
|
169
|
+
onDoubleSelect={(id) => onSelectFile(id, true)}
|
|
164
170
|
onToggle={toggle}
|
|
165
171
|
theme={theme}
|
|
166
172
|
/>
|