@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cueloop/client",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.94",
|
|
4
4
|
"description": "The cueloop review UI: projection renderer, keyboard grammar, review rail",
|
|
5
5
|
"homepage": "https://github.com/mmurakaru/cueloop#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -27,18 +27,18 @@
|
|
|
27
27
|
"access": "public"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@cueloop/daemon": "0.1.0-alpha.
|
|
31
|
-
"@cueloop/extension-api": "0.1.0-alpha.
|
|
32
|
-
"@cueloop/integration-obsidian": "0.1.0-alpha.
|
|
33
|
-
"@cueloop/schema": "0.1.0-alpha.
|
|
34
|
-
"@opentui/core": "^0.5.
|
|
35
|
-
"@opentui/keymap": "0.5.
|
|
36
|
-
"@opentui/react": "^0.5.
|
|
37
|
-
"@opentui/ssh": "0.5.
|
|
30
|
+
"@cueloop/daemon": "0.1.0-alpha.94",
|
|
31
|
+
"@cueloop/extension-api": "0.1.0-alpha.94",
|
|
32
|
+
"@cueloop/integration-obsidian": "0.1.0-alpha.94",
|
|
33
|
+
"@cueloop/schema": "0.1.0-alpha.94",
|
|
34
|
+
"@opentui/core": "^0.5.12",
|
|
35
|
+
"@opentui/keymap": "0.5.12",
|
|
36
|
+
"@opentui/react": "^0.5.12",
|
|
37
|
+
"@opentui/ssh": "0.5.12",
|
|
38
38
|
"@pierre/diffs": "^1.4.1",
|
|
39
39
|
"@tanstack/virtual-core": "^3.17.11",
|
|
40
40
|
"diff": "^9",
|
|
41
|
-
"puppeteer-core": "^25.
|
|
41
|
+
"puppeteer-core": "^25.11.0",
|
|
42
42
|
"react": "^19.3.0",
|
|
43
43
|
"valibot": "^1.5.0",
|
|
44
44
|
"zustand": "^5.0.15"
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/** Keyboard scrolling in a changed-file tab uses caret navigation, even when the file was deleted. */
|
|
2
|
+
|
|
3
|
+
import { expect, test } from "bun:test";
|
|
4
|
+
import { mkdtempSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import React from "react";
|
|
8
|
+
import { ScrollBoxRenderable, type Renderable } from "@opentui/core";
|
|
9
|
+
import { DaemonServer } from "@cueloop/daemon";
|
|
10
|
+
import { App } from "./App";
|
|
11
|
+
import { annotationPaletteFor } from "./annotation-palette";
|
|
12
|
+
import { DARK } from "./theme";
|
|
13
|
+
import {
|
|
14
|
+
isolateUserConfig,
|
|
15
|
+
locateText,
|
|
16
|
+
press,
|
|
17
|
+
renderReadyApp,
|
|
18
|
+
settle,
|
|
19
|
+
waitForText,
|
|
20
|
+
} from "./test-support";
|
|
21
|
+
|
|
22
|
+
function findScrollbox(node: Renderable): ScrollBoxRenderable | null {
|
|
23
|
+
if (node.id === "diff-scroll" && node instanceof ScrollBoxRenderable) return node;
|
|
24
|
+
|
|
25
|
+
for (const child of node.getChildren()) {
|
|
26
|
+
const found = findScrollbox(child);
|
|
27
|
+
|
|
28
|
+
if (found) return found;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function hex(color: { toInts(): [number, number, number, number] }): string {
|
|
35
|
+
const [red, green, blue] = color.toInts();
|
|
36
|
+
|
|
37
|
+
return "#" + [red, green, blue].map((part) => part.toString(16).padStart(2, "0")).join("");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
test("deleted file caret scrolls one row and stays at the file boundaries", async () => {
|
|
41
|
+
const home = mkdtempSync(join(tmpdir(), "cueloop-scroll-home-"));
|
|
42
|
+
const repo = mkdtempSync(join(tmpdir(), "cueloop-scroll-repo-"));
|
|
43
|
+
const restoreConfig = isolateUserConfig(home);
|
|
44
|
+
const server = new DaemonServer({ home, idleExitMs: 0 });
|
|
45
|
+
let setup: Awaited<ReturnType<typeof renderReadyApp>> | undefined;
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const file = join(repo, "long.md");
|
|
49
|
+
const lines = Array.from({ length: 180 }, (_, index) =>
|
|
50
|
+
index === 113
|
|
51
|
+
? "[nodejs/node#36005](https://github.com/nodejs/node/issues/36005)."
|
|
52
|
+
: `row ${String(index).padStart(3, "0")} ${"a paragraph with wrapped text and a link [source](https://example.test/page) ".repeat((index % 3) + 1)}`,
|
|
53
|
+
);
|
|
54
|
+
const git = (args: string[]) => Bun.spawnSync(["git", "-C", repo, ...args]);
|
|
55
|
+
|
|
56
|
+
writeFileSync(file, lines.join("\n") + "\n");
|
|
57
|
+
git(["init", "-q"]);
|
|
58
|
+
git(["config", "user.email", "fixture@example.com"]);
|
|
59
|
+
git(["config", "user.name", "Fixture"]);
|
|
60
|
+
git(["add", "-A"]);
|
|
61
|
+
git(["commit", "-q", "-m", "seed"]);
|
|
62
|
+
unlinkSync(file);
|
|
63
|
+
server.start();
|
|
64
|
+
setup = await renderReadyApp(<App home={home} sessionId={undefined} cwd={repo} />, {
|
|
65
|
+
width: 180,
|
|
66
|
+
height: 48,
|
|
67
|
+
});
|
|
68
|
+
await waitForText(setup, "long.md");
|
|
69
|
+
const treeFile = locateText(setup, "long.md");
|
|
70
|
+
|
|
71
|
+
await setup.mockMouse.click(treeFile.column, treeFile.row);
|
|
72
|
+
await waitForText(setup, "row 00");
|
|
73
|
+
const box = findScrollbox(setup.renderer.root);
|
|
74
|
+
|
|
75
|
+
if (!box) throw new Error("file diff scrollbox missing");
|
|
76
|
+
for (let offset = 0; offset < 1000; offset += 20) {
|
|
77
|
+
box.scrollTo({ x: 0, y: offset });
|
|
78
|
+
await settle(setup);
|
|
79
|
+
if (setup.captureCharFrame().includes("row 130")) break;
|
|
80
|
+
}
|
|
81
|
+
const target = locateText(setup, "row 130");
|
|
82
|
+
|
|
83
|
+
await setup.mockMouse.click(target.column, target.row);
|
|
84
|
+
await press(setup, "escape");
|
|
85
|
+
|
|
86
|
+
const steps: number[] = [];
|
|
87
|
+
const caretCell = annotationPaletteFor(DARK).caretCell;
|
|
88
|
+
const frameTrace: Array<{ markers: number[]; caret: number }> = [];
|
|
89
|
+
const captureFrame = () => {
|
|
90
|
+
const lines = setup!.captureCharFrame().split("\n");
|
|
91
|
+
const markers = lines.flatMap((line, row) => (line.includes("▎") ? [row] : []));
|
|
92
|
+
const caret = setup!
|
|
93
|
+
.captureSpans()
|
|
94
|
+
.lines.findIndex((line) => line.spans.some((span) => hex(span.bg) === caretCell));
|
|
95
|
+
|
|
96
|
+
frameTrace.push({ markers, caret });
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
setup.renderer.on("frame", captureFrame);
|
|
100
|
+
|
|
101
|
+
for (let index = 0; index < 12; index++) {
|
|
102
|
+
const before = box.scrollTop;
|
|
103
|
+
await press(setup, "down");
|
|
104
|
+
steps.push(box.scrollTop - before);
|
|
105
|
+
const caretRow = setup
|
|
106
|
+
.captureSpans()
|
|
107
|
+
.lines.findIndex((line) => line.spans.some((span) => hex(span.bg) === caretCell));
|
|
108
|
+
const markerRows = setup
|
|
109
|
+
.captureCharFrame()
|
|
110
|
+
.split("\n")
|
|
111
|
+
.flatMap((line, row) => (line.includes("▎") ? [row] : []));
|
|
112
|
+
|
|
113
|
+
expect(caretRow).toBeGreaterThanOrEqual(0);
|
|
114
|
+
expect(markerRows).toEqual([caretRow]);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
expect(Math.max(...steps)).toBeLessThanOrEqual(1);
|
|
118
|
+
expect(Math.min(...steps)).toBeGreaterThanOrEqual(0);
|
|
119
|
+
let anchored = false;
|
|
120
|
+
|
|
121
|
+
for (let index = 0; index < 80; index++) {
|
|
122
|
+
await press(setup, "up");
|
|
123
|
+
const marker = setup
|
|
124
|
+
.captureCharFrame()
|
|
125
|
+
.split("\n")
|
|
126
|
+
.findIndex((line) => line.includes("▎"));
|
|
127
|
+
|
|
128
|
+
if (marker === box.viewport.screenY) anchored = true;
|
|
129
|
+
if (anchored && box.scrollTop > 0) expect(marker).toBe(box.viewport.screenY);
|
|
130
|
+
}
|
|
131
|
+
expect(anchored).toBe(true);
|
|
132
|
+
box.scrollTo({ x: 0, y: 100000 });
|
|
133
|
+
await settle(setup);
|
|
134
|
+
const last = locateText(setup, "row 179");
|
|
135
|
+
|
|
136
|
+
await setup.mockMouse.click(last.column, last.row);
|
|
137
|
+
await press(setup, "escape");
|
|
138
|
+
let lastMarker = -1;
|
|
139
|
+
|
|
140
|
+
for (let index = 0; index < 12; index++) {
|
|
141
|
+
const traceStart = frameTrace.length;
|
|
142
|
+
const before = box.scrollTop;
|
|
143
|
+
|
|
144
|
+
await press(setup, "down");
|
|
145
|
+
const states = frameTrace.slice(traceStart);
|
|
146
|
+
|
|
147
|
+
expect(box.scrollTop).toBe(before);
|
|
148
|
+
expect(states.length).toBeGreaterThan(0);
|
|
149
|
+
for (const state of states) {
|
|
150
|
+
expect(state.markers).toEqual([state.caret]);
|
|
151
|
+
expect(state.caret).toBeGreaterThanOrEqual(lastMarker);
|
|
152
|
+
lastMarker = state.caret;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
box.scrollTo({ x: 0, y: 0 });
|
|
156
|
+
await settle(setup);
|
|
157
|
+
const first = locateText(setup, "row 000");
|
|
158
|
+
|
|
159
|
+
await setup.mockMouse.click(first.column, first.row);
|
|
160
|
+
await press(setup, "escape");
|
|
161
|
+
let firstMarker = Number.POSITIVE_INFINITY;
|
|
162
|
+
|
|
163
|
+
for (let index = 0; index < 12; index++) {
|
|
164
|
+
const traceStart = frameTrace.length;
|
|
165
|
+
const before = box.scrollTop;
|
|
166
|
+
|
|
167
|
+
await press(setup, "up");
|
|
168
|
+
const states = frameTrace.slice(traceStart);
|
|
169
|
+
|
|
170
|
+
expect(box.scrollTop).toBe(before);
|
|
171
|
+
expect(states.length).toBeGreaterThan(0);
|
|
172
|
+
for (const state of states) {
|
|
173
|
+
expect(state.markers).toEqual([state.caret]);
|
|
174
|
+
expect(state.caret).toBeLessThanOrEqual(firstMarker);
|
|
175
|
+
firstMarker = state.caret;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
setup.renderer.off("frame", captureFrame);
|
|
179
|
+
} finally {
|
|
180
|
+
setup?.renderer.destroy();
|
|
181
|
+
server.stop();
|
|
182
|
+
restoreConfig();
|
|
183
|
+
rmSync(home, { recursive: true, force: true });
|
|
184
|
+
rmSync(repo, { recursive: true, force: true });
|
|
185
|
+
}
|
|
186
|
+
}, 30000);
|
package/src/App.diff.test.tsx
CHANGED
|
@@ -10,7 +10,6 @@ import type { Thread } from "@cueloop/schema";
|
|
|
10
10
|
import { App } from "./App";
|
|
11
11
|
import { pullRequestReviewLayout } from "./launch-layout";
|
|
12
12
|
import { DARK } from "./theme";
|
|
13
|
-
import { NERD } from "./components/primitives/icons";
|
|
14
13
|
import {
|
|
15
14
|
clickText,
|
|
16
15
|
dragText,
|
|
@@ -220,7 +219,7 @@ describe("diff review", () => {
|
|
|
220
219
|
await waitForText(setup, "● Existing note.");
|
|
221
220
|
});
|
|
222
221
|
|
|
223
|
-
test("option+c folds the caret's file to its band;
|
|
222
|
+
test("option+c folds the caret's file to its band; its title restores the body", async () => {
|
|
224
223
|
// Arrange - the caret opens on the file's first code line
|
|
225
224
|
const setup = await renderApp();
|
|
226
225
|
|
|
@@ -234,8 +233,8 @@ describe("diff review", () => {
|
|
|
234
233
|
expect(collapsed).toContain("src/store.ts");
|
|
235
234
|
expect(collapsed).toContain("+1");
|
|
236
235
|
|
|
237
|
-
// Act - the
|
|
238
|
-
await clickText(setup,
|
|
236
|
+
// Act - the title unfolds it again
|
|
237
|
+
await clickText(setup, "src/store.ts");
|
|
239
238
|
|
|
240
239
|
// Assert
|
|
241
240
|
await waitForText(setup, "new Map()");
|
package/src/App.test.tsx
CHANGED
|
@@ -442,6 +442,7 @@ describe("the thread view and the menu", () => {
|
|
|
442
442
|
const dialog = setup.captureCharFrame();
|
|
443
443
|
|
|
444
444
|
expect(dialog).toContain("nav mode");
|
|
445
|
+
expect(dialog).toContain("open selected file or toggle folder");
|
|
445
446
|
expect(dialog).toContain("place the caret");
|
|
446
447
|
expect(dialog).not.toContain("grow/shrink");
|
|
447
448
|
|
package/src/App.toast.test.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** The share toast is non-modal: while it is up, escape still cancels an open overlay. */
|
|
2
2
|
|
|
3
|
-
import { afterEach, beforeEach, describe, mock, test } from "bun:test";
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
4
4
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { join } from "node:path";
|
|
@@ -13,6 +13,7 @@ import type { ShareTransport } from "./thread-controller";
|
|
|
13
13
|
import {
|
|
14
14
|
clickText,
|
|
15
15
|
isolateUserConfig,
|
|
16
|
+
locateText,
|
|
16
17
|
press,
|
|
17
18
|
pressKey,
|
|
18
19
|
typeText,
|
|
@@ -63,6 +64,31 @@ afterEach(() => {
|
|
|
63
64
|
});
|
|
64
65
|
|
|
65
66
|
describe("share toast", () => {
|
|
67
|
+
test("clicking outside dismisses the toast while clicking inside keeps it open", async () => {
|
|
68
|
+
const setup = await testRender(
|
|
69
|
+
<App home={home} sessionId={session.id} shareTransport={shareTransport} />,
|
|
70
|
+
{ width: 120, height: 32 },
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
await waitForText(setup, "cueloop");
|
|
74
|
+
await pressKey(setup, "s", { ctrl: true });
|
|
75
|
+
await waitForText(setup, "+ new link");
|
|
76
|
+
await press(setup, "enter");
|
|
77
|
+
await press(setup, "enter");
|
|
78
|
+
await waitForText(setup, "link name");
|
|
79
|
+
await press(setup, "enter");
|
|
80
|
+
await waitForText(setup, "link copied");
|
|
81
|
+
await press(setup, "escape");
|
|
82
|
+
await waitForTextGone(setup, "share externally");
|
|
83
|
+
const body = locateText(setup, "ssh p_share01@cueloop.dev");
|
|
84
|
+
|
|
85
|
+
await setup.mockMouse.click(body.column, body.row);
|
|
86
|
+
expect(setup.captureCharFrame()).toContain("link copied");
|
|
87
|
+
await setup.mockMouse.click(1, 1);
|
|
88
|
+
await waitForTextGone(setup, "link copied");
|
|
89
|
+
setup.renderer.destroy();
|
|
90
|
+
});
|
|
91
|
+
|
|
66
92
|
test("escape cancels an open composer even while the toast is up", async () => {
|
|
67
93
|
// Arrange
|
|
68
94
|
const setup = await testRender(
|
package/src/App.tsx
CHANGED
|
@@ -112,6 +112,10 @@ const TOAST_DISMISS_MS = 2000;
|
|
|
112
112
|
export interface AppProps {
|
|
113
113
|
home?: string;
|
|
114
114
|
sessionId?: string;
|
|
115
|
+
/** Review data already captured by the command before the UI mounts. */
|
|
116
|
+
initialReview?: { session: Thread; diff: { patch: string; files: DiffFileContents[] } };
|
|
117
|
+
/** An owner connection already opened by the launching command. */
|
|
118
|
+
initialClient?: ThreadClient;
|
|
115
119
|
/** The launch directory whose git repo backs the no-session welcome tree; defaults to process.cwd(). */
|
|
116
120
|
cwd?: string;
|
|
117
121
|
/**
|
|
@@ -151,6 +155,18 @@ export interface AppProps {
|
|
|
151
155
|
extensionRegistry?: ClientExtensionRegistry;
|
|
152
156
|
}
|
|
153
157
|
|
|
158
|
+
function initialReviewControllerOptions(initialReview: AppProps["initialReview"]) {
|
|
159
|
+
const options = { initialSession: initialReview?.session, initialDiff: initialReview?.diff };
|
|
160
|
+
|
|
161
|
+
return options;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function initialReviewFiles(initialReview: AppProps["initialReview"], sessionId: string) {
|
|
165
|
+
const files = initialReview?.session.id === sessionId ? initialReview.diff.files : undefined;
|
|
166
|
+
|
|
167
|
+
return files;
|
|
168
|
+
}
|
|
169
|
+
|
|
154
170
|
function extensionContext(cwd: string | undefined, threadId: string | null): ExtensionUIContext {
|
|
155
171
|
return { workspace: cwd ?? process.cwd(), threadId };
|
|
156
172
|
}
|
|
@@ -326,12 +342,13 @@ function authorLabelResolver(
|
|
|
326
342
|
function ProjectPanelBody(props: {
|
|
327
343
|
mode: ProjectPanelMode;
|
|
328
344
|
loadChanges: () => Promise<readonly DiffFileContents[]>;
|
|
345
|
+
initialFiles?: readonly DiffFileContents[];
|
|
329
346
|
loadProjectFiles: () => Promise<string[]>;
|
|
330
347
|
diffSourceKey: string;
|
|
331
348
|
liveWorkingTree: boolean;
|
|
332
349
|
frozenFiles?: readonly DiffFileContents[];
|
|
333
|
-
onOpenChangedFile: (path: string) => void;
|
|
334
|
-
onOpenProjectFile: (path: string) => void;
|
|
350
|
+
onOpenChangedFile: (path: string, persistent?: boolean) => void;
|
|
351
|
+
onOpenProjectFile: (path: string, persistent?: boolean) => void;
|
|
335
352
|
commentCounts?: ReadonlyMap<string, number>;
|
|
336
353
|
focused?: boolean;
|
|
337
354
|
theme: Theme;
|
|
@@ -340,6 +357,7 @@ function ProjectPanelBody(props: {
|
|
|
340
357
|
loadChanges: props.loadChanges,
|
|
341
358
|
visible: props.mode === "changes" && props.liveWorkingTree,
|
|
342
359
|
diffSourceKey: props.diffSourceKey,
|
|
360
|
+
initialFiles: props.initialFiles,
|
|
343
361
|
});
|
|
344
362
|
|
|
345
363
|
if (props.mode === "changes") {
|
|
@@ -391,12 +409,20 @@ function usableScreenReached(
|
|
|
391
409
|
/** The keybinds dialog content: the thread grammar while the thread view owns the keys. */
|
|
392
410
|
function cheatsheetFor(keyBindings: KeyBindings, threadViewActive: boolean): CheatsheetSection[] {
|
|
393
411
|
const base = keyBindings.cheatsheet();
|
|
412
|
+
const fileTree: CheatsheetSection = {
|
|
413
|
+
title: "Changes / Project files",
|
|
414
|
+
entries: [
|
|
415
|
+
{ keys: "enter", label: "open selected file or toggle folder" },
|
|
416
|
+
{ keys: "tab", label: "cycle panes" },
|
|
417
|
+
],
|
|
418
|
+
};
|
|
394
419
|
|
|
395
420
|
if (!threadViewActive) {
|
|
396
|
-
return base;
|
|
421
|
+
return [fileTree, ...base];
|
|
397
422
|
}
|
|
398
423
|
|
|
399
424
|
return [
|
|
425
|
+
fileTree,
|
|
400
426
|
...THREAD_VIEW_CHEATSHEET,
|
|
401
427
|
{ title: "Nav mode · session", entries: sessionCommandEntries() },
|
|
402
428
|
{ title: "Nav mode · diff", entries: diffCommandEntries() },
|
|
@@ -523,6 +549,8 @@ export function reconciledFocus(navigable: FocusPane[], focusedPane: FocusPane):
|
|
|
523
549
|
export function App({
|
|
524
550
|
home,
|
|
525
551
|
sessionId,
|
|
552
|
+
initialReview,
|
|
553
|
+
initialClient,
|
|
526
554
|
cwd,
|
|
527
555
|
readOnly = false,
|
|
528
556
|
onExit,
|
|
@@ -543,6 +571,8 @@ export function App({
|
|
|
543
571
|
createReviewController({
|
|
544
572
|
home,
|
|
545
573
|
sessionId,
|
|
574
|
+
...initialReviewControllerOptions(initialReview),
|
|
575
|
+
initialClient,
|
|
546
576
|
cwd,
|
|
547
577
|
readOnly: observer,
|
|
548
578
|
onExit,
|
|
@@ -573,6 +603,11 @@ export function App({
|
|
|
573
603
|
);
|
|
574
604
|
const { session, inbox, status, toast, error, completion, editOrphanCount, walk } =
|
|
575
605
|
useSyncExternalStore(controller.subscribe, controller.getSnapshot, controller.getSnapshot);
|
|
606
|
+
const initialReviewForFirstMount = useRef(initialReview);
|
|
607
|
+
|
|
608
|
+
useEffect(() => {
|
|
609
|
+
initialReviewForFirstMount.current = undefined;
|
|
610
|
+
}, []);
|
|
576
611
|
|
|
577
612
|
// A shared plan you own polls for collaborator notes while it is open; the
|
|
578
613
|
// merge refreshes through the normal event path. Stops on leave.
|
|
@@ -630,7 +665,7 @@ export function App({
|
|
|
630
665
|
const [menuDialog, setMenuDialog] = useState<"keybinds" | "settings" | null>(null);
|
|
631
666
|
const [shareDialogOpen, setShareDialogOpen] = useState(false);
|
|
632
667
|
const [autoClose, setAutoClose] = useState<AutoClose>("off");
|
|
633
|
-
// unified or side-by-side diff; split
|
|
668
|
+
// unified or side-by-side diff; split lays out when the Changes pane is zoomed
|
|
634
669
|
const [diffView, setDiffView] = useState<DiffViewMode>("split");
|
|
635
670
|
const [defaultMessage, setDefaultMessage] = useState<MessageOutcome>("approved");
|
|
636
671
|
const [focusedAnnotationId, setFocusedAnnotationId] = useState<string | undefined>(undefined);
|
|
@@ -656,10 +691,6 @@ export function App({
|
|
|
656
691
|
const [skills, setSkills] = useState<SlashItem[]>([]);
|
|
657
692
|
const [reviewSkill, setReviewSkill] = useState("code-review");
|
|
658
693
|
const [reviewWorkspace, setReviewWorkspace] = useState<ReviewWorkspaceMode>("worktree");
|
|
659
|
-
const reviewSkillOptions = useMemo(
|
|
660
|
-
() => [...new Set([reviewSkill, "code-review", ...skills.map((skill) => skill.name)])],
|
|
661
|
-
[reviewSkill, skills],
|
|
662
|
-
);
|
|
663
694
|
const paletteNames = useMemo(
|
|
664
695
|
() => new Set(mergeSlashItems(slashItemsFrom(quickActions), skills).map((item) => item.name)),
|
|
665
696
|
[quickActions, skills],
|
|
@@ -767,8 +798,6 @@ export function App({
|
|
|
767
798
|
setMode({ type: "renameSelf", text: identity.name ?? "" });
|
|
768
799
|
},
|
|
769
800
|
reviewSkill,
|
|
770
|
-
reviewSkillOptions,
|
|
771
|
-
setReviewSkill,
|
|
772
801
|
reviewWorkspace,
|
|
773
802
|
setReviewWorkspace,
|
|
774
803
|
});
|
|
@@ -1116,6 +1145,7 @@ export function App({
|
|
|
1116
1145
|
onPin={togglePin}
|
|
1117
1146
|
onRename={(id, title) => setMode({ type: "renameThread", sessionId: id, text: title })}
|
|
1118
1147
|
quickActions={quickActions}
|
|
1148
|
+
diffView={diffView}
|
|
1119
1149
|
onWelcomeComposingChange={setWelcomeComposing}
|
|
1120
1150
|
layout={layout}
|
|
1121
1151
|
/>
|
|
@@ -1367,6 +1397,7 @@ export function App({
|
|
|
1367
1397
|
commentCounts={diffCommentCounts}
|
|
1368
1398
|
onFocusGroup={workbench.focusGroup}
|
|
1369
1399
|
onActivateTab={workbench.activate}
|
|
1400
|
+
onKeepTab={workbench.keepTab}
|
|
1370
1401
|
onCloseTab={workbench.close}
|
|
1371
1402
|
onSplit={workbench.split}
|
|
1372
1403
|
onZoom={workbench.toggleZoom}
|
|
@@ -1442,13 +1473,21 @@ export function App({
|
|
|
1442
1473
|
<ProjectPanelBody
|
|
1443
1474
|
mode={workbench.projectMode}
|
|
1444
1475
|
loadChanges={() => controller.repoChanges()}
|
|
1476
|
+
initialFiles={initialReviewFiles(
|
|
1477
|
+
initialReviewForFirstMount.current,
|
|
1478
|
+
activeSession.id,
|
|
1479
|
+
)}
|
|
1445
1480
|
loadProjectFiles={() => controller.repoFiles()}
|
|
1446
1481
|
diffSourceKey={activeSession.id}
|
|
1447
1482
|
{...projectDiffFiles(activeSession)}
|
|
1448
1483
|
// the Changes navigator always opens a changed file as a diff - a diff review shows its
|
|
1449
1484
|
// captured snapshot, every other thread the live working-tree diff
|
|
1450
|
-
onOpenChangedFile={(path) =>
|
|
1451
|
-
|
|
1485
|
+
onOpenChangedFile={(path, persistent) =>
|
|
1486
|
+
workbench.openFile(path, "diff", persistent)
|
|
1487
|
+
}
|
|
1488
|
+
onOpenProjectFile={(path, persistent) =>
|
|
1489
|
+
workbench.openFile(path, "contents", persistent)
|
|
1490
|
+
}
|
|
1452
1491
|
commentCounts={diffCommentCounts}
|
|
1453
1492
|
focused={focusedPane === "project"}
|
|
1454
1493
|
theme={theme}
|
|
@@ -1466,6 +1505,7 @@ export function App({
|
|
|
1466
1505
|
theme={theme}
|
|
1467
1506
|
mode={mode}
|
|
1468
1507
|
toast={toast}
|
|
1508
|
+
onDismissToast={() => controller.dismissToast()}
|
|
1469
1509
|
setMode={setMode}
|
|
1470
1510
|
dispatch={dispatch}
|
|
1471
1511
|
/>
|