@xuhaojun/githunk 0.2.0 → 0.3.1
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/README.md +23 -2
- package/dist/githunk.js +1425 -400
- package/package.json +6 -6
package/dist/githunk.js
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { chmodSync, cpSync, mkdtempSync, renameSync, rmSync } from "node:fs";
|
|
6
|
+
import { writeFile } from "node:fs/promises";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
import { join as join6 } from "node:path";
|
|
9
|
+
|
|
3
10
|
// src/cli/args.ts
|
|
4
11
|
import { Command, CommanderError } from "commander";
|
|
5
12
|
// package.json
|
|
6
13
|
var package_default = {
|
|
7
14
|
name: "@xuhaojun/githunk",
|
|
8
|
-
version: "0.
|
|
15
|
+
version: "0.3.1",
|
|
9
16
|
description: "A review-first Git TUI combining lazygit's everyday Git workflow with focused hunk review.",
|
|
10
17
|
type: "module",
|
|
11
18
|
bin: {
|
|
@@ -80,8 +87,9 @@ var cliVersion = typeof package_default.version === "string" && package_default.
|
|
|
80
87
|
function parseCliArgs(argv) {
|
|
81
88
|
let stdout = "";
|
|
82
89
|
let stderr = "";
|
|
90
|
+
let update;
|
|
83
91
|
const program = new Command;
|
|
84
|
-
program.name("githunk").description("A review-first Git TUI combining lazygit's everyday Git workflow with focused hunk review.").version(cliVersion, "-V, --version", "output the version number").option("-p, --path <dir>", "path to the Git repository to open").argument("[path]", "path to the Git repository to open").exitOverride().configureOutput({
|
|
92
|
+
program.name("githunk").description("A review-first Git TUI combining lazygit's everyday Git workflow with focused hunk review.").version(cliVersion, "-V, --version", "output the version number").option("-p, --path <dir>", "path to the Git repository to open").argument("[path]", "path to the Git repository to open").exitOverride().action(() => {}).configureOutput({
|
|
85
93
|
writeOut: (text) => {
|
|
86
94
|
stdout += text;
|
|
87
95
|
},
|
|
@@ -89,6 +97,12 @@ function parseCliArgs(argv) {
|
|
|
89
97
|
stderr += text;
|
|
90
98
|
}
|
|
91
99
|
});
|
|
100
|
+
program.command("update").description("update githunk to the newest (or a given) release").argument("[version]", "version to install; the newest release when omitted").option("--check", "report the installed and available versions without installing").action((version, options2) => {
|
|
101
|
+
update = {
|
|
102
|
+
...version === undefined ? {} : { version },
|
|
103
|
+
check: options2.check ?? false
|
|
104
|
+
};
|
|
105
|
+
});
|
|
92
106
|
try {
|
|
93
107
|
program.parse([...argv], { from: "user" });
|
|
94
108
|
} catch (error) {
|
|
@@ -102,12 +116,91 @@ function parseCliArgs(argv) {
|
|
|
102
116
|
}
|
|
103
117
|
throw error;
|
|
104
118
|
}
|
|
119
|
+
if (update !== undefined)
|
|
120
|
+
return { kind: "update", ...update };
|
|
105
121
|
const options = program.opts();
|
|
106
122
|
const positional = program.args[0];
|
|
107
123
|
const startDirectory = options.path ?? positional;
|
|
108
124
|
return startDirectory === undefined ? { kind: "start" } : { kind: "start", startDirectory };
|
|
109
125
|
}
|
|
110
126
|
|
|
127
|
+
// src/cli/update.ts
|
|
128
|
+
import { createHash } from "node:crypto";
|
|
129
|
+
import { basename, join } from "node:path";
|
|
130
|
+
function assetNameFor(platform, arch) {
|
|
131
|
+
const osToken = platform === "linux" ? "linux" : platform === "darwin" ? "darwin" : platform === "win32" ? "windows" : null;
|
|
132
|
+
const archToken = arch === "x64" ? "x64" : arch === "arm64" ? "arm64" : null;
|
|
133
|
+
if (osToken === null || archToken === null)
|
|
134
|
+
return null;
|
|
135
|
+
if (osToken === "windows" && archToken !== "x64")
|
|
136
|
+
return null;
|
|
137
|
+
return `githunk-${osToken}-${archToken}.tar.gz`;
|
|
138
|
+
}
|
|
139
|
+
function normalizeVersion(version) {
|
|
140
|
+
return version.trim().replace(/^v/, "");
|
|
141
|
+
}
|
|
142
|
+
function compareVersions(left, right) {
|
|
143
|
+
const parts = (version) => {
|
|
144
|
+
const [major = "0", minor = "0", patch = "0"] = normalizeVersion(version).split(".");
|
|
145
|
+
return [Number(major) || 0, Number(minor) || 0, Number(patch) || 0];
|
|
146
|
+
};
|
|
147
|
+
const [aMajor, aMinor, aPatch] = parts(left);
|
|
148
|
+
const [bMajor, bMinor, bPatch] = parts(right);
|
|
149
|
+
return aMajor - bMajor || aMinor - bMinor || aPatch - bPatch;
|
|
150
|
+
}
|
|
151
|
+
function isSelfManagedBinary(executablePath) {
|
|
152
|
+
const base = basename(executablePath);
|
|
153
|
+
return base === "githunk" || base === "githunk.exe";
|
|
154
|
+
}
|
|
155
|
+
function verifyChecksum(tarball, checksums, asset) {
|
|
156
|
+
const actual = createHash("sha256").update(tarball).digest("hex");
|
|
157
|
+
const line = checksums.split(`
|
|
158
|
+
`).map((entry) => entry.trim().split(/\s+/)).find((fields) => fields[fields.length - 1] === asset);
|
|
159
|
+
const expected = line?.[0];
|
|
160
|
+
if (expected === undefined || expected === "" || expected.toLowerCase() !== actual) {
|
|
161
|
+
throw new Error(`checksum mismatch for ${asset}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
async function applyUpdate(target, asset, env) {
|
|
165
|
+
await env.withTempDir(async (dir) => {
|
|
166
|
+
const { tarball, checksums } = await env.fetchAsset(`v${target}`, asset);
|
|
167
|
+
verifyChecksum(tarball, checksums, asset);
|
|
168
|
+
const archivePath = join(dir, asset);
|
|
169
|
+
await env.writeFile(archivePath, tarball);
|
|
170
|
+
await env.extractTarball(archivePath, dir);
|
|
171
|
+
await env.writeBinary(env.stagedBinary(dir), env.executablePath);
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
async function runUpdate(request, env) {
|
|
175
|
+
try {
|
|
176
|
+
if (!isSelfManagedBinary(env.executablePath)) {
|
|
177
|
+
return {
|
|
178
|
+
exitCode: 1,
|
|
179
|
+
message: "githunk was installed via npm — update it with `npm update --global @xuhaojun/githunk`"
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
const asset = assetNameFor(env.platform, env.arch);
|
|
183
|
+
if (asset === null) {
|
|
184
|
+
return {
|
|
185
|
+
exitCode: 1,
|
|
186
|
+
message: `no prebuilt githunk binary ships for ${env.platform}-${env.arch} — install with \`npm install -g @xuhaojun/githunk\``
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
const current = normalizeVersion(env.installedVersion());
|
|
190
|
+
const target = normalizeVersion(request.version ?? normalizeVersion(await env.fetchReleaseTag()));
|
|
191
|
+
if (compareVersions(target, current) === 0) {
|
|
192
|
+
return { exitCode: 0, message: `githunk ${current} is already up to date` };
|
|
193
|
+
}
|
|
194
|
+
if (request.check) {
|
|
195
|
+
return { exitCode: 0, message: `update available: ${current} -> ${target}` };
|
|
196
|
+
}
|
|
197
|
+
await applyUpdate(target, asset, env);
|
|
198
|
+
return { exitCode: 0, message: `updated githunk ${current} -> ${target}` };
|
|
199
|
+
} catch (error) {
|
|
200
|
+
return { exitCode: 1, message: error instanceof Error ? error.message : String(error) };
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
111
204
|
// src/main.ts
|
|
112
205
|
import { resolve as resolve4 } from "node:path";
|
|
113
206
|
import { createCliRenderer } from "@opentui/core";
|
|
@@ -991,12 +1084,12 @@ function reviewStateFor(record, currentFingerprint) {
|
|
|
991
1084
|
}
|
|
992
1085
|
|
|
993
1086
|
// src/review/working-tree-fingerprint.ts
|
|
994
|
-
import { createHash } from "node:crypto";
|
|
1087
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
995
1088
|
function utf8(value) {
|
|
996
1089
|
return new TextEncoder().encode(value);
|
|
997
1090
|
}
|
|
998
1091
|
function sha256Tuple(parts) {
|
|
999
|
-
const hash =
|
|
1092
|
+
const hash = createHash2("sha256");
|
|
1000
1093
|
for (const part of parts) {
|
|
1001
1094
|
const bytes = utf8(part);
|
|
1002
1095
|
const length = Buffer.allocUnsafe(4);
|
|
@@ -1029,7 +1122,7 @@ function fingerprintWorkingTreeFile(target, filePatch) {
|
|
|
1029
1122
|
|
|
1030
1123
|
// src/storage/local-state-file.ts
|
|
1031
1124
|
import { mkdir, open, rename, stat, unlink, lstat, link, readFile } from "node:fs/promises";
|
|
1032
|
-
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
1125
|
+
import { dirname, isAbsolute, join as join2, resolve } from "node:path";
|
|
1033
1126
|
import { randomUUID } from "node:crypto";
|
|
1034
1127
|
async function assertNoSymlinkInPath(path, pathKind) {
|
|
1035
1128
|
const absolute = resolve(path);
|
|
@@ -1059,7 +1152,7 @@ class LocalStateFile {
|
|
|
1059
1152
|
this.pathKind = options.pathKind ?? "state";
|
|
1060
1153
|
}
|
|
1061
1154
|
get path() {
|
|
1062
|
-
return this.resolvedPath ??
|
|
1155
|
+
return this.resolvedPath ?? join2(this.runner.cwd, ".git", this.relativePath);
|
|
1063
1156
|
}
|
|
1064
1157
|
async resolvePath() {
|
|
1065
1158
|
if (this.resolvedPath !== undefined)
|
|
@@ -1067,7 +1160,7 @@ class LocalStateFile {
|
|
|
1067
1160
|
const output = (await this.runner.run(["rev-parse", "--git-path", this.relativePath], { readOnly: true })).stdout.trim();
|
|
1068
1161
|
if (output.length === 0)
|
|
1069
1162
|
throw new Error(`git returned an empty path for ${this.relativePath}`);
|
|
1070
|
-
this.resolvedPath = isAbsolute(output) ? output :
|
|
1163
|
+
this.resolvedPath = isAbsolute(output) ? output : join2(this.runner.cwd, output);
|
|
1071
1164
|
return this.resolvedPath;
|
|
1072
1165
|
}
|
|
1073
1166
|
async readText() {
|
|
@@ -1976,7 +2069,7 @@ function validateUpstream(upstream) {
|
|
|
1976
2069
|
throw new Error("invalid upstream choice");
|
|
1977
2070
|
}
|
|
1978
2071
|
}
|
|
1979
|
-
async function
|
|
2072
|
+
async function fetch2(runner, remote, options = {}) {
|
|
1980
2073
|
await runner.run(remote === undefined ? ["fetch"] : ["fetch", remote], options.background === true ? { dontLog: true } : { streamOutput: true });
|
|
1981
2074
|
}
|
|
1982
2075
|
async function pull(runner, options = {}) {
|
|
@@ -2185,7 +2278,7 @@ async function listReflog(runner, options = {}) {
|
|
|
2185
2278
|
|
|
2186
2279
|
// src/git/worktrees.ts
|
|
2187
2280
|
import { readFile as readFile2, stat as stat2 } from "node:fs/promises";
|
|
2188
|
-
import { dirname as dirname2, join as
|
|
2281
|
+
import { dirname as dirname2, join as join3 } from "node:path";
|
|
2189
2282
|
function finalizeEntry(entry) {
|
|
2190
2283
|
return {
|
|
2191
2284
|
path: entry.path,
|
|
@@ -2320,11 +2413,11 @@ async function readTrimmedFile(path) {
|
|
|
2320
2413
|
}
|
|
2321
2414
|
async function inProgressBranch(gitDir) {
|
|
2322
2415
|
for (const directory of ["rebase-merge", "rebase-apply"]) {
|
|
2323
|
-
const headName = await readTrimmedFile(
|
|
2416
|
+
const headName = await readTrimmedFile(join3(gitDir, directory, "head-name"));
|
|
2324
2417
|
if (headName !== undefined)
|
|
2325
2418
|
return headName.replace(/^refs\/heads\//, "");
|
|
2326
2419
|
}
|
|
2327
|
-
return await readTrimmedFile(
|
|
2420
|
+
return await readTrimmedFile(join3(gitDir, "BISECT_START"));
|
|
2328
2421
|
}
|
|
2329
2422
|
async function listWorktrees(runner) {
|
|
2330
2423
|
const repositoryPaths = await resolveRepositoryPaths(runner);
|
|
@@ -2391,7 +2484,7 @@ function worktreeRemovalRequiresForce(error) {
|
|
|
2391
2484
|
|
|
2392
2485
|
// src/git/submodules.ts
|
|
2393
2486
|
import { readFile as readFile3 } from "node:fs/promises";
|
|
2394
|
-
import { join as
|
|
2487
|
+
import { join as join4, resolve as resolve2 } from "node:path";
|
|
2395
2488
|
|
|
2396
2489
|
// src/domain/submodule.ts
|
|
2397
2490
|
function submoduleFullName(submodule) {
|
|
@@ -2446,7 +2539,7 @@ function parseGitModules(raw) {
|
|
|
2446
2539
|
}
|
|
2447
2540
|
async function readGitModules(directory) {
|
|
2448
2541
|
try {
|
|
2449
|
-
return await readFile3(
|
|
2542
|
+
return await readFile3(join4(directory, ".gitmodules"), "utf8");
|
|
2450
2543
|
} catch (error) {
|
|
2451
2544
|
if (error instanceof Error && "code" in error) {
|
|
2452
2545
|
const code = error.code;
|
|
@@ -2457,7 +2550,7 @@ async function readGitModules(directory) {
|
|
|
2457
2550
|
}
|
|
2458
2551
|
}
|
|
2459
2552
|
async function collectSubmodules(worktreePath, parentModule, visited) {
|
|
2460
|
-
const directory = parentModule === undefined ? worktreePath :
|
|
2553
|
+
const directory = parentModule === undefined ? worktreePath : join4(worktreePath, submoduleFullPath(parentModule));
|
|
2461
2554
|
const resolved = resolve2(directory);
|
|
2462
2555
|
if (visited.has(resolved))
|
|
2463
2556
|
return [];
|
|
@@ -2941,7 +3034,7 @@ class AppController {
|
|
|
2941
3034
|
return;
|
|
2942
3035
|
if (options.background !== true)
|
|
2943
3036
|
this.logAction(LOG_ACTIONS.fetch);
|
|
2944
|
-
await this.runMutation(() => this.requireRunnerOperation((runner) =>
|
|
3037
|
+
await this.runMutation(() => this.requireRunnerOperation((runner) => fetch2(runner, remote, options)));
|
|
2945
3038
|
}
|
|
2946
3039
|
async pull(options = {}) {
|
|
2947
3040
|
if (!this.ensureWorkingTreeMutation())
|
|
@@ -5935,7 +6028,7 @@ function commitGraphRows(commits, getColor) {
|
|
|
5935
6028
|
}
|
|
5936
6029
|
|
|
5937
6030
|
// src/ui/author-style.ts
|
|
5938
|
-
import { createHash as
|
|
6031
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
5939
6032
|
var initialsCache = new Map;
|
|
5940
6033
|
var colorCache = new Map;
|
|
5941
6034
|
function randInt(bytes, max) {
|
|
@@ -5976,7 +6069,7 @@ function authorColor(authorName) {
|
|
|
5976
6069
|
const cached = colorCache.get(authorName);
|
|
5977
6070
|
if (cached !== undefined)
|
|
5978
6071
|
return cached;
|
|
5979
|
-
const hash = new Uint8Array(
|
|
6072
|
+
const hash = new Uint8Array(createHash3("md5").update(authorName).digest());
|
|
5980
6073
|
const color = hslToHex(randFloat(hash.slice(0, 4)) * 360, 0.6 + 0.4 * randFloat(hash.slice(4, 8)), 0.4 + 0.2 * randFloat(hash.slice(8, 12)));
|
|
5981
6074
|
colorCache.set(authorName, color);
|
|
5982
6075
|
return color;
|
|
@@ -15469,7 +15562,7 @@ async function loadRefsSnapshot(runner) {
|
|
|
15469
15562
|
}
|
|
15470
15563
|
|
|
15471
15564
|
// src/git/editor.ts
|
|
15472
|
-
import { join as
|
|
15565
|
+
import { join as join5, basename as basename2 } from "node:path";
|
|
15473
15566
|
function standardTerminalPreset(editor) {
|
|
15474
15567
|
return {
|
|
15475
15568
|
edit: `${editor} -- {{filename}}`,
|
|
@@ -15562,13 +15655,13 @@ function resolvePlaceholders(template, values) {
|
|
|
15562
15655
|
}
|
|
15563
15656
|
function guessEditorBase(env, gitEditor) {
|
|
15564
15657
|
if (env.GITHUNK_EDITOR !== undefined && env.GITHUNK_EDITOR.trim().length > 0)
|
|
15565
|
-
return
|
|
15658
|
+
return basename2(env.GITHUNK_EDITOR.split(" ")[0].trim());
|
|
15566
15659
|
if (gitEditor !== undefined && gitEditor.trim().length > 0)
|
|
15567
|
-
return
|
|
15660
|
+
return basename2(gitEditor.split(" ")[0].trim());
|
|
15568
15661
|
for (const key of ["GIT_EDITOR", "VISUAL", "EDITOR"]) {
|
|
15569
15662
|
const value = env[key];
|
|
15570
15663
|
if (value !== undefined && value.trim().length > 0)
|
|
15571
|
-
return
|
|
15664
|
+
return basename2(value.split(" ")[0].trim());
|
|
15572
15665
|
}
|
|
15573
15666
|
return "vi";
|
|
15574
15667
|
}
|
|
@@ -15607,7 +15700,7 @@ async function resolveEditCommand(files, options = {}) {
|
|
|
15607
15700
|
function absolutePath(repoRoot, relativePath) {
|
|
15608
15701
|
if (relativePath.startsWith("/"))
|
|
15609
15702
|
return relativePath;
|
|
15610
|
-
return
|
|
15703
|
+
return join5(repoRoot, relativePath);
|
|
15611
15704
|
}
|
|
15612
15705
|
|
|
15613
15706
|
// src/app/create-app.ts
|
|
@@ -15615,7 +15708,7 @@ import { isAbsolute as isAbsolute2, resolve as resolve3 } from "node:path";
|
|
|
15615
15708
|
|
|
15616
15709
|
// src/app/index-watcher.ts
|
|
15617
15710
|
import { watch, statSync } from "node:fs";
|
|
15618
|
-
import { basename as
|
|
15711
|
+
import { basename as basename3, dirname as dirname3 } from "node:path";
|
|
15619
15712
|
var DEFAULT_INDEX_EVENT_DEBOUNCE_MS = 50;
|
|
15620
15713
|
var BUSY_RETRY_MS = 50;
|
|
15621
15714
|
function fingerprint(path) {
|
|
@@ -15645,7 +15738,7 @@ class IndexWatcher {
|
|
|
15645
15738
|
stopped = true;
|
|
15646
15739
|
constructor(options) {
|
|
15647
15740
|
this.options = options;
|
|
15648
|
-
this.indexName =
|
|
15741
|
+
this.indexName = basename3(options.indexPath);
|
|
15649
15742
|
this.lockName = `${this.indexName}.lock`;
|
|
15650
15743
|
this.debounceMs = options.debounceMs ?? DEFAULT_INDEX_EVENT_DEBOUNCE_MS;
|
|
15651
15744
|
this.baseline = undefined;
|
|
@@ -15868,9 +15961,8 @@ function isWorkerAvailable() {
|
|
|
15868
15961
|
}
|
|
15869
15962
|
|
|
15870
15963
|
// src/ui/review-workspace/ReviewWorkspaceApp.tsx
|
|
15871
|
-
import {
|
|
15872
|
-
import {
|
|
15873
|
-
import { useCallback, useEffect as useEffect3, useLayoutEffect as useLayoutEffect2, useMemo as useMemo4, useRef as useRef2, useState as useState3, useSyncExternalStore } from "react";
|
|
15964
|
+
import { useKeyboard as useKeyboard2, useTerminalDimensions } from "@opentui/react";
|
|
15965
|
+
import { useCallback, useEffect as useEffect3, useLayoutEffect as useLayoutEffect3, useMemo as useMemo5, useRef as useRef3, useState as useState4, useSyncExternalStore } from "react";
|
|
15874
15966
|
|
|
15875
15967
|
// src/review/core/selectors.ts
|
|
15876
15968
|
function resolveViewedRecord(file, viewed) {
|
|
@@ -16031,16 +16123,23 @@ function reviewHeaderLines(state, width) {
|
|
|
16031
16123
|
totalDeletions = null;
|
|
16032
16124
|
const additionsText = totalAdditions === null ? "—" : `+${totalAdditions}`;
|
|
16033
16125
|
const deletionsText = totalDeletions === null ? "—" : `−${totalDeletions}`;
|
|
16034
|
-
const projectionLabel = "Aggregate";
|
|
16035
|
-
const
|
|
16036
|
-
const
|
|
16126
|
+
const projectionLabel = state.projection.kind === "since-last-review" ? "Since last review" : state.projection.kind === "commit" ? `Commit ${state.projection.oid.slice(0, 7)}` : "Aggregate";
|
|
16127
|
+
const baseBudget = Math.min(cellWidth(baseLabel), Math.max(1, Math.floor(w / 2)));
|
|
16128
|
+
const headPrefix = w > baseBudget + 3 ? `${truncateCell(headLabel, w - baseBudget - 3)} → ` : "";
|
|
16129
|
+
const baseText = truncateCell(baseLabel, Math.max(0, w - cellWidth(headPrefix)));
|
|
16130
|
+
const suffix = ` · ${commits} commits · ${files} files · ${additionsText} ${deletionsText} [${projectionLabel}]`;
|
|
16131
|
+
const suffixText = truncateCell(suffix, Math.max(0, w - cellWidth(headPrefix) - cellWidth(baseText)));
|
|
16037
16132
|
const reviewedLabel = `Reviewed ${progress.viewed}/${progress.total}`;
|
|
16038
16133
|
const changedPart = progress.changed > 0 ? ` · ${progress.changed} changed` : "";
|
|
16039
16134
|
const pendingPart = progress.pending > 0 ? ` · ${progress.pending} pending` : progress.pending === 0 ? " · 0 pending" : "";
|
|
16040
16135
|
const reviewingPart = progress.reviewing > 0 ? ` · ${progress.reviewing} reviewing` : "";
|
|
16041
16136
|
const line2Raw = `${reviewedLabel}${changedPart}${reviewingPart}${pendingPart}`;
|
|
16042
16137
|
const line2 = truncateCell(line2Raw, w);
|
|
16043
|
-
const line1Spans = [
|
|
16138
|
+
const line1Spans = [
|
|
16139
|
+
{ text: headPrefix, style: "strong" },
|
|
16140
|
+
{ text: baseText, style: "strong", action: "choose-base" },
|
|
16141
|
+
{ text: suffixText, style: "strong" }
|
|
16142
|
+
];
|
|
16044
16143
|
const line2Spans = [{ text: line2, style: "dim" }];
|
|
16045
16144
|
return [line1Spans, line2Spans];
|
|
16046
16145
|
}
|
|
@@ -16275,6 +16374,22 @@ var REVIEW_COMMANDS = [
|
|
|
16275
16374
|
available: always,
|
|
16276
16375
|
hint: "help"
|
|
16277
16376
|
},
|
|
16377
|
+
{
|
|
16378
|
+
id: "review.toggleSinceLastReview",
|
|
16379
|
+
title: "Review only what changed since the last review",
|
|
16380
|
+
keys: ["s"],
|
|
16381
|
+
focus: ["any"],
|
|
16382
|
+
available: (state) => state.projection.kind !== "aggregate" || state.lastSubmission !== null,
|
|
16383
|
+
hint: "since"
|
|
16384
|
+
},
|
|
16385
|
+
{
|
|
16386
|
+
id: "review.chooseBase",
|
|
16387
|
+
title: "Change base branch",
|
|
16388
|
+
keys: ["B"],
|
|
16389
|
+
focus: ["any"],
|
|
16390
|
+
available: always,
|
|
16391
|
+
hint: "base"
|
|
16392
|
+
},
|
|
16278
16393
|
{
|
|
16279
16394
|
id: "review.close",
|
|
16280
16395
|
title: "Close overlay or workspace",
|
|
@@ -16290,7 +16405,7 @@ for (const cmd of REVIEW_COMMANDS) {
|
|
|
16290
16405
|
if (!keyToCommand.has(k))
|
|
16291
16406
|
keyToCommand.set(k, cmd);
|
|
16292
16407
|
const lower = k.toLowerCase();
|
|
16293
|
-
if (lower !== k && !keyToCommand.has(lower))
|
|
16408
|
+
if (k.length > 1 && lower !== k && !keyToCommand.has(lower))
|
|
16294
16409
|
keyToCommand.set(lower, cmd);
|
|
16295
16410
|
}
|
|
16296
16411
|
}
|
|
@@ -16313,7 +16428,8 @@ function reviewHelp(focus, state) {
|
|
|
16313
16428
|
"review.focusDiff": 0,
|
|
16314
16429
|
"review.focusFiles": 1,
|
|
16315
16430
|
"review.toggleFocus": 2,
|
|
16316
|
-
"review.layoutCycle": 3
|
|
16431
|
+
"review.layoutCycle": 3,
|
|
16432
|
+
"review.chooseBase": 4
|
|
16317
16433
|
};
|
|
16318
16434
|
const ordered = [...available].sort((a, b) => (panelPriority[a.id] ?? 99) - (panelPriority[b.id] ?? 99));
|
|
16319
16435
|
const lines = ordered.map((c) => `${c.keys.map((key) => key === "tab" ? "Tab" : key).join("/")} ${c.title}`);
|
|
@@ -16322,7 +16438,7 @@ function reviewHelp(focus, state) {
|
|
|
16322
16438
|
}
|
|
16323
16439
|
|
|
16324
16440
|
// src/ui/review-workspace/review-sidebar.ts
|
|
16325
|
-
import { basename as
|
|
16441
|
+
import { basename as basename4, dirname as dirname4 } from "node:path/posix";
|
|
16326
16442
|
function normalizeDiffPath(p) {
|
|
16327
16443
|
return p?.replace(/[\r\n]+$/u, "");
|
|
16328
16444
|
}
|
|
@@ -16351,10 +16467,10 @@ function sidebarFileName(file) {
|
|
|
16351
16467
|
const path = formatTerminalPath(normalizeDiffPath(file.path) ?? file.path);
|
|
16352
16468
|
const previousPath = file.previousPath ? formatTerminalPath(normalizeDiffPath(file.previousPath) ?? file.previousPath) : undefined;
|
|
16353
16469
|
if (previousPath === undefined || previousPath === path) {
|
|
16354
|
-
return
|
|
16470
|
+
return basename4(path);
|
|
16355
16471
|
}
|
|
16356
|
-
const previousName =
|
|
16357
|
-
const nextName =
|
|
16472
|
+
const previousName = basename4(previousPath);
|
|
16473
|
+
const nextName = basename4(path);
|
|
16358
16474
|
return previousName === nextName ? nextName : `${previousName} -> ${nextName}`;
|
|
16359
16475
|
}
|
|
16360
16476
|
function formatSidebarStat(prefix, value, truncated = false) {
|
|
@@ -16677,7 +16793,7 @@ function hunkHeaderText(file, index) {
|
|
|
16677
16793
|
const hunk = file.metadata.hunks[index];
|
|
16678
16794
|
if (!hunk)
|
|
16679
16795
|
return "@@";
|
|
16680
|
-
return hunk.hunkSpecs ?? `@@ -${hunk.deletionStart},${hunk.deletionCount} +${hunk.additionStart},${hunk.additionCount} @@`;
|
|
16796
|
+
return hunk.hunkSpecs?.replace(/\r?\n$/u, "") ?? `@@ -${hunk.deletionStart},${hunk.deletionCount} +${hunk.additionStart},${hunk.additionCount} @@`;
|
|
16681
16797
|
}
|
|
16682
16798
|
function hunkGapBefore(file, hunkIndex) {
|
|
16683
16799
|
if (hunkIndex <= 0)
|
|
@@ -17284,8 +17400,114 @@ function ReviewDiffSection({
|
|
|
17284
17400
|
});
|
|
17285
17401
|
}
|
|
17286
17402
|
|
|
17403
|
+
// src/ui/review-workspace/components/ReviewStickyHeader.tsx
|
|
17404
|
+
import { StyledText as StyledText8, parseColor as parseColor3 } from "@opentui/core";
|
|
17405
|
+
import { jsx as jsx3 } from "@opentui/react/jsx-runtime";
|
|
17406
|
+
var STICKY_BACKGROUND = "#2b3138";
|
|
17407
|
+
var PATH_FOREGROUND = "#e0e2e4";
|
|
17408
|
+
var HUNK_FOREGROUND = "#7aa6da";
|
|
17409
|
+
var colorCache3 = new Map;
|
|
17410
|
+
function color2(value) {
|
|
17411
|
+
const cached = colorCache3.get(value);
|
|
17412
|
+
if (cached)
|
|
17413
|
+
return cached;
|
|
17414
|
+
const parsed = parseColor3(value);
|
|
17415
|
+
colorCache3.set(value, parsed);
|
|
17416
|
+
return parsed;
|
|
17417
|
+
}
|
|
17418
|
+
function chunk2(text, fg5) {
|
|
17419
|
+
return { __isChunk: true, text, fg: color2(fg5), bg: color2(STICKY_BACKGROUND) };
|
|
17420
|
+
}
|
|
17421
|
+
function fit(text, width) {
|
|
17422
|
+
if (width <= 0)
|
|
17423
|
+
return { text: "", used: 0 };
|
|
17424
|
+
let out = "";
|
|
17425
|
+
let used = 0;
|
|
17426
|
+
for (const character of text) {
|
|
17427
|
+
const characterWidth = cellWidth(character);
|
|
17428
|
+
if (used + characterWidth > width)
|
|
17429
|
+
break;
|
|
17430
|
+
out += character;
|
|
17431
|
+
used += characterWidth;
|
|
17432
|
+
}
|
|
17433
|
+
return { text: out, used };
|
|
17434
|
+
}
|
|
17435
|
+
function stickyHeaderChunks(sticky, width) {
|
|
17436
|
+
if (width <= 0)
|
|
17437
|
+
return [];
|
|
17438
|
+
const chunks = [];
|
|
17439
|
+
const hunkText2 = sticky.hunkText ?? "";
|
|
17440
|
+
const hunkWidth = hunkText2.length === 0 ? 0 : Math.min(width, cellWidth(hunkText2) + 2);
|
|
17441
|
+
const path = fit(sticky.filePath, Math.max(0, width - hunkWidth));
|
|
17442
|
+
if (path.text.length > 0)
|
|
17443
|
+
chunks.push(chunk2(path.text, PATH_FOREGROUND));
|
|
17444
|
+
let used = path.used;
|
|
17445
|
+
if (hunkText2.length > 0 && used < width) {
|
|
17446
|
+
const separator = fit(" ", width - used);
|
|
17447
|
+
if (separator.text.length > 0) {
|
|
17448
|
+
chunks.push(chunk2(separator.text, PATH_FOREGROUND));
|
|
17449
|
+
used += separator.used;
|
|
17450
|
+
}
|
|
17451
|
+
const hunk = fit(hunkText2, width - used);
|
|
17452
|
+
if (hunk.text.length > 0) {
|
|
17453
|
+
chunks.push(chunk2(hunk.text, HUNK_FOREGROUND));
|
|
17454
|
+
used += hunk.used;
|
|
17455
|
+
}
|
|
17456
|
+
}
|
|
17457
|
+
if (used < width)
|
|
17458
|
+
chunks.push(chunk2(" ".repeat(width - used), PATH_FOREGROUND));
|
|
17459
|
+
return chunks;
|
|
17460
|
+
}
|
|
17461
|
+
function ReviewStickyHeader({ sticky, width }) {
|
|
17462
|
+
return /* @__PURE__ */ jsx3("box", {
|
|
17463
|
+
id: "review-sticky-header",
|
|
17464
|
+
style: { width: "100%", height: 1, flexShrink: 0, backgroundColor: STICKY_BACKGROUND },
|
|
17465
|
+
children: /* @__PURE__ */ jsx3("text", {
|
|
17466
|
+
content: new StyledText8([...stickyHeaderChunks(sticky, Math.max(0, width))]),
|
|
17467
|
+
wrapMode: "none",
|
|
17468
|
+
truncate: true
|
|
17469
|
+
})
|
|
17470
|
+
});
|
|
17471
|
+
}
|
|
17472
|
+
|
|
17473
|
+
// src/ui/review-workspace/sticky-header.ts
|
|
17474
|
+
function headerTopOf(sectionOffsets, index) {
|
|
17475
|
+
return (sectionOffsets[index] ?? 0) + (index > 0 ? 1 : 0);
|
|
17476
|
+
}
|
|
17477
|
+
function sectionIndexAt(sectionOffsets, fileCount, top) {
|
|
17478
|
+
let index = 0;
|
|
17479
|
+
while (index < fileCount - 1 && headerTopOf(sectionOffsets, index + 1) < top)
|
|
17480
|
+
index += 1;
|
|
17481
|
+
return index;
|
|
17482
|
+
}
|
|
17483
|
+
function resolveStickyDiffHeader(request) {
|
|
17484
|
+
const { files, state, layout, sectionOffsets, expandedSourceByGap } = request;
|
|
17485
|
+
if (files.length === 0)
|
|
17486
|
+
return;
|
|
17487
|
+
const top = Math.max(0, Math.floor(request.scrollTop));
|
|
17488
|
+
const fileIndex = sectionIndexAt(sectionOffsets, files.length, top);
|
|
17489
|
+
const file = files[fileIndex];
|
|
17490
|
+
if (!file)
|
|
17491
|
+
return;
|
|
17492
|
+
const localRow = top - (sectionOffsets[fileIndex] ?? 0);
|
|
17493
|
+
const showDivider = fileIndex > 0;
|
|
17494
|
+
let hunkIndex = -1;
|
|
17495
|
+
for (let candidate = 0;candidate < file.metadata.hunks.length; candidate += 1) {
|
|
17496
|
+
const headerRow = hunkSectionRowOffset(file, layout, candidate, state, expandedSourceByGap, showDivider);
|
|
17497
|
+
if (headerRow > localRow)
|
|
17498
|
+
break;
|
|
17499
|
+
hunkIndex = candidate;
|
|
17500
|
+
}
|
|
17501
|
+
return {
|
|
17502
|
+
fileKey: file.id,
|
|
17503
|
+
filePath: file.path,
|
|
17504
|
+
hunkIndex,
|
|
17505
|
+
...hunkIndex < 0 ? {} : { hunkText: hunkHeaderText(file, hunkIndex) }
|
|
17506
|
+
};
|
|
17507
|
+
}
|
|
17508
|
+
|
|
17287
17509
|
// src/ui/review-workspace/components/ReviewDiffPane.tsx
|
|
17288
|
-
import { jsx as
|
|
17510
|
+
import { jsx as jsx4, jsxs as jsxs3 } from "@opentui/react/jsx-runtime";
|
|
17289
17511
|
function sectionWindow(files, state, layout, scrollTop, viewportHeight, overscan, expandedSourceByGap) {
|
|
17290
17512
|
const heights = [];
|
|
17291
17513
|
const offsets = [0];
|
|
@@ -17319,6 +17541,7 @@ function ReviewDiffPane({
|
|
|
17319
17541
|
selectedHunkIndex,
|
|
17320
17542
|
showLineNumbers = true,
|
|
17321
17543
|
wrapLines = false,
|
|
17544
|
+
showStickyHeader = true,
|
|
17322
17545
|
overscan = Math.max(10, height * 2),
|
|
17323
17546
|
highlightByFileKey,
|
|
17324
17547
|
expandedSourceByGap,
|
|
@@ -17337,12 +17560,14 @@ function ReviewDiffPane({
|
|
|
17337
17560
|
const ownedScrollRef = useRef(null);
|
|
17338
17561
|
const scrollRef = externalScrollRef ?? ownedScrollRef;
|
|
17339
17562
|
const [scrollTop, setScrollTop] = useState(0);
|
|
17563
|
+
const stickyRows = showStickyHeader && files.length > 0 ? 1 : 0;
|
|
17564
|
+
const viewportHeight = Math.max(1, height - stickyRows);
|
|
17340
17565
|
const previousFileRevealTokenRef = useRef(selectedFileRevealToken);
|
|
17341
17566
|
const previousSelectionRef = useRef(null);
|
|
17342
17567
|
const previousHunkRevealTokenRef = useRef(undefined);
|
|
17343
17568
|
const pendingSelectionRevealRequestRef = useRef(null);
|
|
17344
17569
|
const pendingSelectionRevealTimersRef = useRef([]);
|
|
17345
|
-
const window = useMemo2(() => sectionWindow(files, state, layout, scrollTop,
|
|
17570
|
+
const window = useMemo2(() => sectionWindow(files, state, layout, scrollTop, viewportHeight, overscan, expandedSourceByGap), [expandedSourceByGap, files, layout, overscan, scrollTop, state.expandedGaps, state.feedback, viewportHeight]);
|
|
17346
17571
|
useEffect(() => {
|
|
17347
17572
|
if (!onVisibleFileKeysChange)
|
|
17348
17573
|
return;
|
|
@@ -17381,13 +17606,13 @@ function ReviewDiffPane({
|
|
|
17381
17606
|
if (state.reveal.scrollToFeedback)
|
|
17382
17607
|
return;
|
|
17383
17608
|
const sectionTop = window.offsets[index] ?? 0;
|
|
17384
|
-
const target = Math.min(Math.max(0, sectionTop), Math.max(0, window.total -
|
|
17609
|
+
const target = Math.min(Math.max(0, sectionTop), Math.max(0, window.total - viewportHeight));
|
|
17385
17610
|
const scrollBox = scrollRef.current;
|
|
17386
17611
|
if (scrollBox)
|
|
17387
17612
|
scrollBox.scrollTop = target;
|
|
17388
17613
|
setScrollTop(target);
|
|
17389
17614
|
onViewportChange?.(target);
|
|
17390
|
-
}, [files,
|
|
17615
|
+
}, [files, onViewportChange, scrollRef, selectedFileKey, selectedFileRevealToken, state.reveal.scrollToFeedback, viewportHeight, window]);
|
|
17391
17616
|
useLayoutEffect(() => {
|
|
17392
17617
|
const currentSelection = { fileKey: selectedFileKey, hunkIndex: selectedHunkIndex };
|
|
17393
17618
|
const previousSelection = previousSelectionRef.current;
|
|
@@ -17447,11 +17672,11 @@ function ReviewDiffPane({
|
|
|
17447
17672
|
const scrollBox = scrollRef.current;
|
|
17448
17673
|
if (!scrollBox)
|
|
17449
17674
|
return;
|
|
17450
|
-
const
|
|
17675
|
+
const measuredHeight = Math.max(1, Math.floor(scrollBox.viewport.height || viewportHeight));
|
|
17451
17676
|
const currentTop = Math.max(0, Math.floor(scrollBox.scrollTop));
|
|
17452
|
-
const currentEnd = currentTop +
|
|
17677
|
+
const currentEnd = currentTop + measuredHeight;
|
|
17453
17678
|
if (target < currentTop || target + 1 > currentEnd) {
|
|
17454
|
-
const nextTop = Math.min(Math.max(0, target), Math.max(0, window.total -
|
|
17679
|
+
const nextTop = Math.min(Math.max(0, target), Math.max(0, window.total - measuredHeight));
|
|
17455
17680
|
scrollBox.scrollTop = nextTop;
|
|
17456
17681
|
setScrollTop(nextTop);
|
|
17457
17682
|
onViewportChange?.(nextTop);
|
|
@@ -17470,73 +17695,323 @@ function ReviewDiffPane({
|
|
|
17470
17695
|
}
|
|
17471
17696
|
}, delay));
|
|
17472
17697
|
return clearPendingTimers;
|
|
17473
|
-
}, [expandedSourceByGap, files,
|
|
17698
|
+
}, [expandedSourceByGap, files, layout, onViewportChange, selectedFileKey, selectedFileRevealToken, selectedHunkIndex, selectedHunkRevealToken, state, viewportHeight]);
|
|
17699
|
+
const sticky = useMemo2(() => stickyRows === 0 ? undefined : resolveStickyDiffHeader({
|
|
17700
|
+
files,
|
|
17701
|
+
state,
|
|
17702
|
+
layout,
|
|
17703
|
+
scrollTop,
|
|
17704
|
+
sectionOffsets: window.offsets,
|
|
17705
|
+
...expandedSourceByGap ? { expandedSourceByGap } : {}
|
|
17706
|
+
}), [expandedSourceByGap, files, layout, scrollTop, state, stickyRows, window.offsets]);
|
|
17474
17707
|
const leadingSpacer = window.offsets[window.first] ?? 0;
|
|
17475
17708
|
const trailingSpacer = window.total - (window.offsets[window.last + 1] ?? window.total);
|
|
17476
|
-
return /* @__PURE__ */
|
|
17477
|
-
id: "review-diff-
|
|
17478
|
-
|
|
17479
|
-
|
|
17480
|
-
|
|
17481
|
-
|
|
17482
|
-
|
|
17483
|
-
|
|
17484
|
-
|
|
17485
|
-
|
|
17486
|
-
|
|
17487
|
-
|
|
17488
|
-
|
|
17489
|
-
|
|
17490
|
-
|
|
17491
|
-
|
|
17492
|
-
|
|
17493
|
-
|
|
17494
|
-
|
|
17495
|
-
|
|
17496
|
-
|
|
17497
|
-
|
|
17709
|
+
return /* @__PURE__ */ jsxs3("box", {
|
|
17710
|
+
id: "review-diff-pane",
|
|
17711
|
+
style: { width: "100%", height: "100%", flexDirection: "column" },
|
|
17712
|
+
children: [
|
|
17713
|
+
sticky ? /* @__PURE__ */ jsx4(ReviewStickyHeader, {
|
|
17714
|
+
sticky,
|
|
17715
|
+
width
|
|
17716
|
+
}) : null,
|
|
17717
|
+
/* @__PURE__ */ jsx4("scrollbox", {
|
|
17718
|
+
id: "review-diff-scrollbox",
|
|
17719
|
+
ref: scrollRef,
|
|
17720
|
+
...focused === undefined ? {} : { focused },
|
|
17721
|
+
width: "100%",
|
|
17722
|
+
flexGrow: 1,
|
|
17723
|
+
minHeight: 0,
|
|
17724
|
+
scrollY: true,
|
|
17725
|
+
viewportCulling: true,
|
|
17726
|
+
verticalScrollbarOptions: { visible: false },
|
|
17727
|
+
onMouseScroll: (event) => {
|
|
17728
|
+
const direction = event.scroll?.direction;
|
|
17729
|
+
const delta = Math.max(1, Math.floor(event.scroll?.delta ?? 1));
|
|
17730
|
+
const target = event.currentTarget;
|
|
17731
|
+
const scrollBox = target && typeof target.scrollBy === "function" ? target : scrollRef.current;
|
|
17732
|
+
if (direction === "down")
|
|
17733
|
+
scrollBox?.scrollBy(delta);
|
|
17734
|
+
else if (direction === "up")
|
|
17735
|
+
scrollBox?.scrollBy(-delta);
|
|
17736
|
+
},
|
|
17737
|
+
children: /* @__PURE__ */ jsxs3("box", {
|
|
17738
|
+
id: "review-diff-content",
|
|
17739
|
+
style: { width: "100%", flexDirection: "column" },
|
|
17740
|
+
children: [
|
|
17741
|
+
leadingSpacer > 0 ? /* @__PURE__ */ jsx4("box", {
|
|
17742
|
+
style: { width: "100%", height: leadingSpacer }
|
|
17743
|
+
}, "review-leading-spacer") : null,
|
|
17744
|
+
window.first <= window.last ? files.slice(window.first, window.last + 1).map((file, offset) => {
|
|
17745
|
+
const fileIndex = window.first + offset;
|
|
17746
|
+
const sectionTop = window.offsets[fileIndex] ?? 0;
|
|
17747
|
+
const sectionHeight = window.heights[fileIndex] ?? 0;
|
|
17748
|
+
const rowStart = Math.max(0, Math.floor(scrollTop - sectionTop - overscan));
|
|
17749
|
+
const rowEnd = Math.min(sectionHeight, Math.ceil(scrollTop + viewportHeight + overscan - sectionTop));
|
|
17750
|
+
const highlight = highlightByFileKey?.get(file.id);
|
|
17751
|
+
const select = onSelectFile ? () => onSelectFile(file.id) : undefined;
|
|
17752
|
+
return /* @__PURE__ */ jsx4(ReviewDiffSection, {
|
|
17753
|
+
file,
|
|
17754
|
+
state,
|
|
17755
|
+
layout,
|
|
17756
|
+
width,
|
|
17757
|
+
selectedHunkIndex: file.id === selectedFileKey ? selectedHunkIndex : -1,
|
|
17758
|
+
showDivider: fileIndex > 0,
|
|
17759
|
+
showLineNumbers,
|
|
17760
|
+
wrapLines,
|
|
17761
|
+
rowStart,
|
|
17762
|
+
rowEnd: Math.max(rowStart, rowEnd),
|
|
17763
|
+
...highlight ? { highlight } : {},
|
|
17764
|
+
...expandedSourceByGap ? { expandedSourceByGap } : {},
|
|
17765
|
+
...select ? { onSelect: select } : {},
|
|
17766
|
+
...onSelectFeedback ? { onSelectFeedback } : {},
|
|
17767
|
+
...onSelectDiffAddress ? { onSelectDiffAddress } : {},
|
|
17768
|
+
...selectedFeedbackId !== undefined ? { selectedFeedbackId } : {},
|
|
17769
|
+
...onToggleGap ? { onToggleGap: (gapId) => onToggleGap(file.id, gapId) } : {}
|
|
17770
|
+
}, file.id);
|
|
17771
|
+
}) : null,
|
|
17772
|
+
trailingSpacer > 0 ? /* @__PURE__ */ jsx4("box", {
|
|
17773
|
+
style: { width: "100%", height: trailingSpacer }
|
|
17774
|
+
}, "review-trailing-spacer") : null
|
|
17775
|
+
]
|
|
17776
|
+
})
|
|
17777
|
+
})
|
|
17778
|
+
]
|
|
17779
|
+
});
|
|
17780
|
+
}
|
|
17781
|
+
|
|
17782
|
+
// src/ui/review-workspace/components/ReviewBasePicker.tsx
|
|
17783
|
+
import { useKeyboard } from "@opentui/react";
|
|
17784
|
+
import { useLayoutEffect as useLayoutEffect2, useMemo as useMemo3, useRef as useRef2, useState as useState2 } from "react";
|
|
17785
|
+
import { jsx as jsx5, jsxs as jsxs4 } from "@opentui/react/jsx-runtime";
|
|
17786
|
+
function consume(event) {
|
|
17787
|
+
event.preventDefault();
|
|
17788
|
+
event.stopPropagation();
|
|
17789
|
+
}
|
|
17790
|
+
function ReviewBasePicker({ selection, width, height, active: active2, warning, onChoose, onCancel, onRetry }) {
|
|
17791
|
+
const [query, setQuery] = useState2("");
|
|
17792
|
+
const [selectedIndex, setSelectedIndex] = useState2(0);
|
|
17793
|
+
const inputRef = useRef2(null);
|
|
17794
|
+
const scrollRef = useRef2(null);
|
|
17795
|
+
const busy = selection.loading || selection.selecting;
|
|
17796
|
+
const candidates = useMemo3(() => {
|
|
17797
|
+
const filter = query.trim().toLowerCase();
|
|
17798
|
+
return filter ? selection.candidates.filter((candidate) => candidate.label.toLowerCase().includes(filter) || candidate.ref.toLowerCase().includes(filter)) : selection.candidates;
|
|
17799
|
+
}, [query, selection.candidates]);
|
|
17800
|
+
const index = Math.min(selectedIndex, Math.max(0, candidates.length - 1));
|
|
17801
|
+
const selected = candidates[index];
|
|
17802
|
+
const dialogWidth = Math.max(1, Math.min(90, width - (width > 20 ? 4 : 0)));
|
|
17803
|
+
const dialogHeight = Math.max(1, Math.min(20, height - (height > 10 ? 2 : 0)));
|
|
17804
|
+
const border = dialogWidth >= 4 && dialogHeight >= 5;
|
|
17805
|
+
const showDetail = dialogHeight >= 9;
|
|
17806
|
+
const contentWidth = Math.max(1, dialogWidth - (border ? 2 : 0));
|
|
17807
|
+
const warningHeight = warning && dialogHeight >= 9 ? 1 : 0;
|
|
17808
|
+
const listHeight = Math.max(1, dialogHeight - (border ? 2 : 0) - 3 - (showDetail ? 1 : 0) - warningHeight);
|
|
17809
|
+
const reveal = (next) => {
|
|
17810
|
+
const scroll = scrollRef.current;
|
|
17811
|
+
if (!scroll)
|
|
17812
|
+
return;
|
|
17813
|
+
const viewportHeight = Math.max(1, Math.floor(scroll.viewport.height || listHeight));
|
|
17814
|
+
if (next < scroll.scrollTop)
|
|
17815
|
+
scroll.scrollTop = next;
|
|
17816
|
+
else if (next >= scroll.scrollTop + viewportHeight)
|
|
17817
|
+
scroll.scrollTop = next - viewportHeight + 1;
|
|
17818
|
+
};
|
|
17819
|
+
useLayoutEffect2(() => {
|
|
17820
|
+
reveal(index);
|
|
17821
|
+
}, [index, candidates, listHeight]);
|
|
17822
|
+
useLayoutEffect2(() => {
|
|
17823
|
+
inputRef.current?.blur();
|
|
17824
|
+
}, [active2, busy]);
|
|
17825
|
+
const move = (direction) => {
|
|
17826
|
+
if (busy)
|
|
17827
|
+
return;
|
|
17828
|
+
const next = Math.max(0, Math.min(candidates.length - 1, index + (direction === "down" ? 1 : -1)));
|
|
17829
|
+
setSelectedIndex(next);
|
|
17830
|
+
reveal(next);
|
|
17831
|
+
};
|
|
17832
|
+
const submit = () => {
|
|
17833
|
+
if (busy)
|
|
17834
|
+
return;
|
|
17835
|
+
if (selected)
|
|
17836
|
+
onChoose(selected.ref);
|
|
17837
|
+
else if (selection.error)
|
|
17838
|
+
onRetry();
|
|
17839
|
+
};
|
|
17840
|
+
const handlePickerKey = (event) => {
|
|
17841
|
+
const name = event.name.toLowerCase();
|
|
17842
|
+
if (busy) {
|
|
17843
|
+
consume(event);
|
|
17844
|
+
return true;
|
|
17845
|
+
}
|
|
17846
|
+
if (name === "escape") {
|
|
17847
|
+
consume(event);
|
|
17848
|
+
onCancel();
|
|
17849
|
+
return true;
|
|
17850
|
+
}
|
|
17851
|
+
if (name === "up" || name === "down") {
|
|
17852
|
+
move(name);
|
|
17853
|
+
consume(event);
|
|
17854
|
+
return true;
|
|
17855
|
+
}
|
|
17856
|
+
if (name === "return" || name === "enter") {
|
|
17857
|
+
consume(event);
|
|
17858
|
+
submit();
|
|
17859
|
+
return true;
|
|
17860
|
+
}
|
|
17861
|
+
if (event.ctrl && name === "r") {
|
|
17862
|
+
consume(event);
|
|
17863
|
+
onRetry();
|
|
17864
|
+
return true;
|
|
17865
|
+
}
|
|
17866
|
+
if (name === "tab") {
|
|
17867
|
+
consume(event);
|
|
17868
|
+
return true;
|
|
17869
|
+
}
|
|
17870
|
+
if (name === "backspace") {
|
|
17871
|
+
consume(event);
|
|
17872
|
+
setQuery((previous) => previous.length > 0 ? previous.slice(0, -1) : previous);
|
|
17873
|
+
setSelectedIndex(0);
|
|
17874
|
+
if (scrollRef.current)
|
|
17875
|
+
scrollRef.current.scrollTop = 0;
|
|
17876
|
+
return true;
|
|
17877
|
+
}
|
|
17878
|
+
if (!event.ctrl && !event.meta) {
|
|
17879
|
+
const char = name === "space" ? " " : name;
|
|
17880
|
+
if ([...char].length === 1) {
|
|
17881
|
+
const code = char.codePointAt(0) ?? 0;
|
|
17882
|
+
if (code >= 32 && code !== 127) {
|
|
17883
|
+
consume(event);
|
|
17884
|
+
setQuery((previous) => `${previous}${char}`);
|
|
17885
|
+
setSelectedIndex(0);
|
|
17886
|
+
if (scrollRef.current)
|
|
17887
|
+
scrollRef.current.scrollTop = 0;
|
|
17888
|
+
return true;
|
|
17889
|
+
}
|
|
17890
|
+
}
|
|
17891
|
+
}
|
|
17892
|
+
return false;
|
|
17893
|
+
};
|
|
17894
|
+
useKeyboard((event) => {
|
|
17895
|
+
if (!active2)
|
|
17896
|
+
return;
|
|
17897
|
+
handlePickerKey(event);
|
|
17898
|
+
});
|
|
17899
|
+
const status = selection.loading ? "Loading branches…" : selection.selecting ? "Loading review…" : selection.error ? `Error: ${selection.error}` : selection.candidates.length === 0 ? "No branches available. Ctrl-R retry." : candidates.length === 0 ? "No matching branches." : `${candidates.length} branches · recommendations first`;
|
|
17900
|
+
return /* @__PURE__ */ jsx5("box", {
|
|
17901
|
+
id: "review-base-backdrop",
|
|
17902
|
+
onMouse: consume,
|
|
17903
|
+
style: { position: "absolute", left: 0, top: 0, width, height, zIndex: 100, backgroundColor: "#151515" },
|
|
17904
|
+
children: /* @__PURE__ */ jsxs4("box", {
|
|
17905
|
+
id: "review-base-picker",
|
|
17906
|
+
style: { position: "absolute", left: Math.floor((width - dialogWidth) / 2), top: Math.floor((height - dialogHeight) / 2), width: dialogWidth, height: dialogHeight, border, borderColor: "#b9ca4a", flexDirection: "column", backgroundColor: "#202020", overflow: "hidden" },
|
|
17498
17907
|
children: [
|
|
17499
|
-
|
|
17500
|
-
|
|
17501
|
-
|
|
17502
|
-
|
|
17503
|
-
|
|
17504
|
-
|
|
17505
|
-
|
|
17506
|
-
|
|
17507
|
-
|
|
17508
|
-
|
|
17509
|
-
|
|
17510
|
-
|
|
17511
|
-
|
|
17512
|
-
|
|
17513
|
-
|
|
17514
|
-
|
|
17515
|
-
|
|
17516
|
-
|
|
17517
|
-
|
|
17518
|
-
|
|
17519
|
-
|
|
17520
|
-
|
|
17521
|
-
|
|
17522
|
-
|
|
17523
|
-
|
|
17524
|
-
|
|
17525
|
-
|
|
17526
|
-
|
|
17527
|
-
|
|
17528
|
-
|
|
17908
|
+
/* @__PURE__ */ jsx5("text", {
|
|
17909
|
+
content: "Choose base branch",
|
|
17910
|
+
wrapMode: "none",
|
|
17911
|
+
truncate: true
|
|
17912
|
+
}),
|
|
17913
|
+
/* @__PURE__ */ jsx5("input", {
|
|
17914
|
+
id: "review-base-filter",
|
|
17915
|
+
ref: inputRef,
|
|
17916
|
+
width: contentWidth,
|
|
17917
|
+
value: query,
|
|
17918
|
+
placeholder: "Filter branches…",
|
|
17919
|
+
focused: false,
|
|
17920
|
+
onInput: (value) => {
|
|
17921
|
+
if (busy)
|
|
17922
|
+
return;
|
|
17923
|
+
setQuery(value);
|
|
17924
|
+
setSelectedIndex(0);
|
|
17925
|
+
if (scrollRef.current)
|
|
17926
|
+
scrollRef.current.scrollTop = 0;
|
|
17927
|
+
},
|
|
17928
|
+
onKeyDown: (event) => {
|
|
17929
|
+
if (handlePickerKey(event))
|
|
17930
|
+
return;
|
|
17931
|
+
},
|
|
17932
|
+
onSubmit: () => {
|
|
17933
|
+
submit();
|
|
17934
|
+
}
|
|
17935
|
+
}),
|
|
17936
|
+
/* @__PURE__ */ jsx5("scrollbox", {
|
|
17937
|
+
id: "review-base-list",
|
|
17938
|
+
ref: scrollRef,
|
|
17939
|
+
width: "100%",
|
|
17940
|
+
height: listHeight,
|
|
17941
|
+
flexShrink: 0,
|
|
17942
|
+
scrollY: true,
|
|
17943
|
+
viewportCulling: true,
|
|
17944
|
+
verticalScrollbarOptions: { visible: false },
|
|
17945
|
+
children: /* @__PURE__ */ jsxs4("box", {
|
|
17946
|
+
style: { width: "100%", flexDirection: "column" },
|
|
17947
|
+
children: [
|
|
17948
|
+
candidates.length === 0 ? /* @__PURE__ */ jsx5("text", {
|
|
17949
|
+
content: status,
|
|
17950
|
+
wrapMode: "none",
|
|
17951
|
+
truncate: true
|
|
17952
|
+
}) : null,
|
|
17953
|
+
candidates.map((candidate, candidateIndex) => /* @__PURE__ */ jsx5("box", {
|
|
17954
|
+
id: `review-base-row:${candidate.ref}`,
|
|
17955
|
+
style: { width: "100%", height: 1, flexShrink: 0, backgroundColor: index === candidateIndex ? "#365f8a" : "#202020" },
|
|
17956
|
+
onMouseUp: (event) => {
|
|
17957
|
+
consume(event);
|
|
17958
|
+
if (busy)
|
|
17959
|
+
return;
|
|
17960
|
+
setSelectedIndex(candidateIndex);
|
|
17961
|
+
onChoose(candidate.ref);
|
|
17962
|
+
},
|
|
17963
|
+
children: /* @__PURE__ */ jsx5("text", {
|
|
17964
|
+
content: `${index === candidateIndex ? ">" : " "} ${candidate.label}${candidate.reason ? ` — ${candidate.reason}` : ""}`,
|
|
17965
|
+
selectable: false,
|
|
17966
|
+
wrapMode: "none",
|
|
17967
|
+
truncate: true
|
|
17968
|
+
})
|
|
17969
|
+
}, candidate.ref))
|
|
17970
|
+
]
|
|
17971
|
+
})
|
|
17972
|
+
}),
|
|
17973
|
+
showDetail ? /* @__PURE__ */ jsx5("text", {
|
|
17974
|
+
id: "review-base-status",
|
|
17975
|
+
content: status,
|
|
17976
|
+
fg: selection.error ? "#f0c674" : "#b4b4b4",
|
|
17977
|
+
wrapMode: "none",
|
|
17978
|
+
truncate: true
|
|
17979
|
+
}) : null,
|
|
17980
|
+
warningHeight > 0 && warning ? /* @__PURE__ */ jsx5("text", {
|
|
17981
|
+
id: "review-base-warning",
|
|
17982
|
+
content: warning,
|
|
17983
|
+
fg: "#f0c674",
|
|
17984
|
+
wrapMode: "none",
|
|
17985
|
+
truncate: true
|
|
17529
17986
|
}) : null,
|
|
17530
|
-
|
|
17531
|
-
style: { width: "100%", height:
|
|
17532
|
-
|
|
17987
|
+
/* @__PURE__ */ jsxs4("box", {
|
|
17988
|
+
style: { width: "100%", height: 1, flexShrink: 0, flexDirection: "row" },
|
|
17989
|
+
children: [
|
|
17990
|
+
selection.error || selection.candidates.length === 0 ? /* @__PURE__ */ jsx5("box", {
|
|
17991
|
+
id: "review-base-retry",
|
|
17992
|
+
onMouseUp: (event) => {
|
|
17993
|
+
consume(event);
|
|
17994
|
+
if (!busy)
|
|
17995
|
+
onRetry();
|
|
17996
|
+
},
|
|
17997
|
+
children: /* @__PURE__ */ jsx5("text", {
|
|
17998
|
+
content: "[Ctrl-R retry] "
|
|
17999
|
+
})
|
|
18000
|
+
}) : null,
|
|
18001
|
+
/* @__PURE__ */ jsx5("text", {
|
|
18002
|
+
content: selection.error !== undefined || busy ? status : "↑↓ choose · Enter select · Esc cancel",
|
|
18003
|
+
wrapMode: "none",
|
|
18004
|
+
truncate: true
|
|
18005
|
+
})
|
|
18006
|
+
]
|
|
18007
|
+
})
|
|
17533
18008
|
]
|
|
17534
18009
|
})
|
|
17535
18010
|
});
|
|
17536
18011
|
}
|
|
17537
18012
|
|
|
17538
18013
|
// src/ui/review-workspace/hooks/useReviewHighlights.ts
|
|
17539
|
-
import { useEffect as useEffect2, useMemo as
|
|
18014
|
+
import { useEffect as useEffect2, useMemo as useMemo4, useState as useState3 } from "react";
|
|
17540
18015
|
|
|
17541
18016
|
// src/review/git/highlight/highlight-cache.ts
|
|
17542
18017
|
class HighlightCache {
|
|
@@ -17600,9 +18075,9 @@ function syntaxThemeForAppearance(appearance) {
|
|
|
17600
18075
|
import { parsePatchFiles as parsePatchFiles2 } from "@pierre/diffs";
|
|
17601
18076
|
|
|
17602
18077
|
// src/review/core/identity.ts
|
|
17603
|
-
import { createHash as
|
|
18078
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
17604
18079
|
function sha256Tuple2(parts) {
|
|
17605
|
-
const hash =
|
|
18080
|
+
const hash = createHash4("sha256");
|
|
17606
18081
|
for (const part of parts) {
|
|
17607
18082
|
const bytes = new TextEncoder().encode(part);
|
|
17608
18083
|
const length = Buffer.alloc(4);
|
|
@@ -18144,7 +18619,7 @@ function findPatchChunk(metadata, chunks, index) {
|
|
|
18144
18619
|
const byIndex = chunks[index];
|
|
18145
18620
|
if (byIndex)
|
|
18146
18621
|
return byIndex;
|
|
18147
|
-
return chunks.find((
|
|
18622
|
+
return chunks.find((chunk3) => [metadata.name, metadata.prevName].map((v) => normalizeDiffPath2(v)).filter((v) => Boolean(v)).map(stripPrefixes).some((path) => chunk3.includes(`a/${path}`) || chunk3.includes(`b/${path}`) || chunk3.includes(path))) ?? "";
|
|
18148
18623
|
}
|
|
18149
18624
|
function parseHunkHeader(line) {
|
|
18150
18625
|
const m = line.match(/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/);
|
|
@@ -18156,8 +18631,8 @@ function parseHunkHeader(line) {
|
|
|
18156
18631
|
const newCount = m[4] !== undefined ? Number.parseInt(m[4], 10) : 1;
|
|
18157
18632
|
return { oldStart, oldCount, newStart, newCount };
|
|
18158
18633
|
}
|
|
18159
|
-
function extractHunksFromChunk(
|
|
18160
|
-
const lines =
|
|
18634
|
+
function extractHunksFromChunk(chunk3) {
|
|
18635
|
+
const lines = chunk3.split(`
|
|
18161
18636
|
`);
|
|
18162
18637
|
const hunks = [];
|
|
18163
18638
|
let currentHunk = null;
|
|
@@ -18307,10 +18782,10 @@ function collectHastHighlightRuns(node, appearance) {
|
|
|
18307
18782
|
}
|
|
18308
18783
|
const props = cur.properties ?? {};
|
|
18309
18784
|
const style = parseStyleValue(props["style"]);
|
|
18310
|
-
const
|
|
18785
|
+
const color3 = style.get("color");
|
|
18311
18786
|
const bg3 = style.get("background-color");
|
|
18312
18787
|
const isWordDiff = bg3 !== undefined && bg3 !== "transparent";
|
|
18313
|
-
const nextFg =
|
|
18788
|
+
const nextFg = color3 ?? fg5;
|
|
18314
18789
|
const nextWordDiff = wordDiff || isWordDiff;
|
|
18315
18790
|
const pushFg = nextFg;
|
|
18316
18791
|
const children = cur.children ?? [];
|
|
@@ -18497,9 +18972,9 @@ function useReviewHighlights(options) {
|
|
|
18497
18972
|
} = options;
|
|
18498
18973
|
const reviewId = options.reviewId ?? state?.document.identity.id ?? "review";
|
|
18499
18974
|
const generationId = options.generationId ?? state?.document.generation.id ?? "generation";
|
|
18500
|
-
const requestedKey =
|
|
18501
|
-
const [highlights, setHighlights] =
|
|
18502
|
-
const [loading, setLoading] =
|
|
18975
|
+
const requestedKey = useMemo4(() => requestedFileKeys?.join("\x00") ?? "", [requestedFileKeys]);
|
|
18976
|
+
const [highlights, setHighlights] = useState3(new Map);
|
|
18977
|
+
const [loading, setLoading] = useState3(false);
|
|
18503
18978
|
useEffect2(() => {
|
|
18504
18979
|
let cancelled = false;
|
|
18505
18980
|
if (!enabled || files.length === 0) {
|
|
@@ -19060,7 +19535,7 @@ function planReviewIntent(state, intent) {
|
|
|
19060
19535
|
}
|
|
19061
19536
|
|
|
19062
19537
|
// src/ui/review-workspace/ReviewWorkspaceApp.tsx
|
|
19063
|
-
import { jsx as
|
|
19538
|
+
import { jsx as jsx6, jsxs as jsxs5, Fragment } from "@opentui/react/jsx-runtime";
|
|
19064
19539
|
var REVIEW_SIDEBAR_DEFAULT_WIDTH = 30;
|
|
19065
19540
|
var REVIEW_SIDEBAR_MIN_WIDTH = 20;
|
|
19066
19541
|
var REVIEW_DIFF_MIN_CONTENT_WIDTH = 40;
|
|
@@ -19075,30 +19550,6 @@ var COLORS2 = {
|
|
|
19075
19550
|
changed: "#f0c674",
|
|
19076
19551
|
feedback: "#c397d8"
|
|
19077
19552
|
};
|
|
19078
|
-
var colorCache3 = new Map;
|
|
19079
|
-
function textChunk(text, style) {
|
|
19080
|
-
let fg5 = colorCache3.get(COLORS2[style]);
|
|
19081
|
-
if (!fg5) {
|
|
19082
|
-
fg5 = parseColor3(COLORS2[style]);
|
|
19083
|
-
colorCache3.set(COLORS2[style], fg5);
|
|
19084
|
-
}
|
|
19085
|
-
return { __isChunk: true, text, fg: fg5 };
|
|
19086
|
-
}
|
|
19087
|
-
function headerText(state, width, error) {
|
|
19088
|
-
const lines = [...reviewHeaderLines(state, width)];
|
|
19089
|
-
if (error)
|
|
19090
|
-
lines.push([{ text: `! ${error.title}: ${error.detail}`, style: "dim" }]);
|
|
19091
|
-
const chunks = [];
|
|
19092
|
-
for (const [index, line] of lines.entries()) {
|
|
19093
|
-
if (index > 0)
|
|
19094
|
-
chunks.push(textChunk(`
|
|
19095
|
-
`, "plain"));
|
|
19096
|
-
for (const span of line) {
|
|
19097
|
-
chunks.push(textChunk(span.text, span.style === "strong" ? "strong" : span.style === "dim" ? "dim" : "plain"));
|
|
19098
|
-
}
|
|
19099
|
-
}
|
|
19100
|
-
return new StyledText8(chunks);
|
|
19101
|
-
}
|
|
19102
19553
|
function fitText(text, width, overflowMarker = ".") {
|
|
19103
19554
|
if (cellWidth(text) <= width)
|
|
19104
19555
|
return text;
|
|
@@ -19123,7 +19574,14 @@ function padText(text, width) {
|
|
|
19123
19574
|
return text;
|
|
19124
19575
|
return text + " ".repeat(width - w);
|
|
19125
19576
|
}
|
|
19126
|
-
|
|
19577
|
+
var PROJECTION_NOTICES = {
|
|
19578
|
+
"no-previous-review": "Since last review needs a finished review to measure from",
|
|
19579
|
+
"history-rewritten": "History was rewritten since the last review — showing the full range",
|
|
19580
|
+
"already-projected": "A projection is already open",
|
|
19581
|
+
stale: "The review moved on while loading — try again",
|
|
19582
|
+
unavailable: "Not available right now"
|
|
19583
|
+
};
|
|
19584
|
+
function reviewFooter(state, layout, focus, notice) {
|
|
19127
19585
|
const selected = state.selection.fileKey ?? "none";
|
|
19128
19586
|
const entryFor = (id) => REVIEW_COMMANDS.find((candidate) => candidate.id === id);
|
|
19129
19587
|
const keyFor = (id) => {
|
|
@@ -19138,6 +19596,7 @@ function reviewFooter(state, layout, focus) {
|
|
|
19138
19596
|
command("review.focusFiles", true),
|
|
19139
19597
|
command("review.toggleFocus", true),
|
|
19140
19598
|
`${command("review.layoutCycle", true)}(${layout})`,
|
|
19599
|
+
command("review.chooseBase"),
|
|
19141
19600
|
pair("review.moveDown", "review.moveUp"),
|
|
19142
19601
|
pair("review.nextHunk", "review.prevHunk"),
|
|
19143
19602
|
pair("review.nextFile", "review.prevFile"),
|
|
@@ -19156,7 +19615,8 @@ function reviewFooter(state, layout, focus) {
|
|
|
19156
19615
|
command("review.help"),
|
|
19157
19616
|
command("review.close")
|
|
19158
19617
|
];
|
|
19159
|
-
|
|
19618
|
+
const trailer = `${focus} — ${selected}`;
|
|
19619
|
+
return notice ? `${notice} | ${hints.join(" | ")} | ${trailer}` : `${hints.join(" | ")} | ${trailer}`;
|
|
19160
19620
|
}
|
|
19161
19621
|
function feedbackDraftText(state) {
|
|
19162
19622
|
const draft = state.draft;
|
|
@@ -19200,7 +19660,7 @@ function isPrintableFilterKey(name) {
|
|
|
19200
19660
|
const codePoint = value.codePointAt(0) ?? 0;
|
|
19201
19661
|
return codePoint >= 32 && codePoint !== 127;
|
|
19202
19662
|
}
|
|
19203
|
-
function
|
|
19663
|
+
function consume2(event) {
|
|
19204
19664
|
try {
|
|
19205
19665
|
event.preventDefault?.();
|
|
19206
19666
|
} catch {}
|
|
@@ -19286,37 +19746,53 @@ function ReviewWorkspaceApp({ session }) {
|
|
|
19286
19746
|
const active2 = session.active;
|
|
19287
19747
|
const onClose = session.onClose;
|
|
19288
19748
|
const finishDialog = session.finishDialog;
|
|
19289
|
-
const [publishedState
|
|
19749
|
+
const [publishedState] = useState4(() => controller.state);
|
|
19750
|
+
useEffect3(() => {
|
|
19751
|
+
let lastSelection = controller.baseSelection;
|
|
19752
|
+
let hadState = controller.state !== undefined;
|
|
19753
|
+
return controller.subscribe(() => {
|
|
19754
|
+
const nextSelection = controller.baseSelection;
|
|
19755
|
+
const hasState = controller.state !== undefined;
|
|
19756
|
+
if (nextSelection !== lastSelection || hasState !== hadState) {
|
|
19757
|
+
lastSelection = nextSelection;
|
|
19758
|
+
hadState = hasState;
|
|
19759
|
+
session.invalidate();
|
|
19760
|
+
}
|
|
19761
|
+
});
|
|
19762
|
+
}, [controller, session]);
|
|
19290
19763
|
const state = controller.state ?? publishedState;
|
|
19291
|
-
const
|
|
19292
|
-
const [
|
|
19293
|
-
const [
|
|
19294
|
-
const [
|
|
19295
|
-
const [
|
|
19296
|
-
const [
|
|
19297
|
-
const [
|
|
19298
|
-
const [
|
|
19299
|
-
const
|
|
19300
|
-
const [
|
|
19301
|
-
const
|
|
19302
|
-
const [
|
|
19303
|
-
const [
|
|
19304
|
-
const [
|
|
19305
|
-
const [
|
|
19306
|
-
const [
|
|
19307
|
-
const [
|
|
19308
|
-
const
|
|
19309
|
-
const
|
|
19310
|
-
const
|
|
19311
|
-
const
|
|
19312
|
-
const
|
|
19313
|
-
const
|
|
19314
|
-
const
|
|
19315
|
-
const
|
|
19316
|
-
const
|
|
19317
|
-
const
|
|
19318
|
-
const
|
|
19319
|
-
const
|
|
19764
|
+
const baseSelection = controller.baseSelection;
|
|
19765
|
+
const [layoutMode, setLayoutMode] = useState4("auto");
|
|
19766
|
+
const [projectionNotice, setProjectionNotice] = useState4(null);
|
|
19767
|
+
const [sidebarWidthPreference, setSidebarWidthPreference] = useState4(REVIEW_SIDEBAR_DEFAULT_WIDTH);
|
|
19768
|
+
const [resizeBarHovered, setResizeBarHovered] = useState4(false);
|
|
19769
|
+
const [resizingSidebar, setResizingSidebar] = useState4(false);
|
|
19770
|
+
const [visibleFileKeys, setVisibleFileKeys] = useState4([]);
|
|
19771
|
+
const [focus, setFocus] = useState4("stream");
|
|
19772
|
+
const [rangeStart, setRangeStart] = useState4(null);
|
|
19773
|
+
const [pendingRangeAnchor, setPendingRangeAnchor] = useState4(null);
|
|
19774
|
+
const localRangeIdentityRef = useRef3(null);
|
|
19775
|
+
const [selectedFeedbackId, setSelectedFeedbackId] = useState4(null);
|
|
19776
|
+
const [editingFeedbackId, setEditingFeedbackId] = useState4(null);
|
|
19777
|
+
const [pendingDeleteFeedbackId, setPendingDeleteFeedbackId] = useState4(null);
|
|
19778
|
+
const [composerFocus, setComposerFocus] = useState4("body");
|
|
19779
|
+
const [composerControlIndex, setComposerControlIndex] = useState4(0);
|
|
19780
|
+
const [reanchorFeedbackId, setReanchorFeedbackId] = useState4(null);
|
|
19781
|
+
const [feedbackMessage, setFeedbackMessage] = useState4(null);
|
|
19782
|
+
const [helpOpen, setHelpOpen] = useState4(false);
|
|
19783
|
+
const diffScrollRef = useRef3(null);
|
|
19784
|
+
const resizingSidebarRef = useRef3(false);
|
|
19785
|
+
const resizeDraggedRef = useRef3(false);
|
|
19786
|
+
const resizeReleaseSuppressionRef = useRef3(false);
|
|
19787
|
+
const resizeReleaseCleanupTokenRef = useRef3(0);
|
|
19788
|
+
const filterInputRef = useRef3(null);
|
|
19789
|
+
const pendingDeleteFeedbackRef = useRef3(null);
|
|
19790
|
+
const composerBodyRef = useRef3(null);
|
|
19791
|
+
const replacementRef = useRef3(null);
|
|
19792
|
+
const finishSummaryRef = useRef3(null);
|
|
19793
|
+
const finishSubmitRef = useRef3(false);
|
|
19794
|
+
const localReviewIdentityRef = useRef3(state?.document.identity.id);
|
|
19795
|
+
const expandedSourceRef = useRef3(new Map);
|
|
19320
19796
|
const dimensions2 = { width: Math.max(1, terminal.width), height: Math.max(1, terminal.height) };
|
|
19321
19797
|
const maxSidebarWidth = Math.max(REVIEW_SIDEBAR_MIN_WIDTH, dimensions2.width - REVIEW_RESIZE_BAR_WIDTH - REVIEW_DIFF_BORDER_WIDTH - REVIEW_DIFF_MIN_CONTENT_WIDTH);
|
|
19322
19798
|
const sidebarWidth = dimensions2.width >= REVIEW_SIDEBAR_VISIBILITY_WIDTH ? Math.min(Math.max(sidebarWidthPreference, REVIEW_SIDEBAR_MIN_WIDTH), maxSidebarWidth) : 0;
|
|
@@ -19327,12 +19803,12 @@ function ReviewWorkspaceApp({ session }) {
|
|
|
19327
19803
|
const resizeBarHeight = Math.max(1, dimensions2.height - 4 - composerHeight);
|
|
19328
19804
|
const sidebarFocused = focus === "sidebar" || focus === "filter";
|
|
19329
19805
|
const diffFocused = focus === "stream";
|
|
19330
|
-
const files =
|
|
19331
|
-
const sidebarEntries =
|
|
19332
|
-
const sidebarFileEntries =
|
|
19333
|
-
const sidebarStatsWidth =
|
|
19806
|
+
const files = useMemo5(() => state ? toHunkReviewFiles(visibleReviewFiles(state)) : [], [state?.document, state?.feedback, state?.filter, state?.viewed]);
|
|
19807
|
+
const sidebarEntries = useMemo5(() => state ? buildReviewSidebarEntries(state) : [], [state]);
|
|
19808
|
+
const sidebarFileEntries = useMemo5(() => sidebarEntries.filter((entry) => entry.kind === "file"), [sidebarEntries]);
|
|
19809
|
+
const sidebarStatsWidth = useMemo5(() => Math.max(0, ...sidebarFileEntries.map((entry) => sidebarEntryStatsWidth(entry))), [sidebarFileEntries]);
|
|
19334
19810
|
const sidebarTextWidth = Math.max(8, sidebarWidth - 2);
|
|
19335
|
-
const expandedSourceByGap =
|
|
19811
|
+
const expandedSourceByGap = useMemo5(() => {
|
|
19336
19812
|
const next = typeof controller.getExpandedSourceByGap === "function" ? controller.getExpandedSourceByGap() : new Map;
|
|
19337
19813
|
const previous = expandedSourceRef.current;
|
|
19338
19814
|
if (previous.size === next.size && [...next].every(([key, lines]) => previous.get(key) === lines))
|
|
@@ -19413,9 +19889,7 @@ function ReviewWorkspaceApp({ session }) {
|
|
|
19413
19889
|
setResizingSidebar(false);
|
|
19414
19890
|
setResizeBarHovered(false);
|
|
19415
19891
|
}, []);
|
|
19416
|
-
|
|
19417
|
-
if (active2)
|
|
19418
|
-
return;
|
|
19892
|
+
const resetLocalReviewUi = useCallback(() => {
|
|
19419
19893
|
setRangeStart(null);
|
|
19420
19894
|
setPendingRangeAnchor(null);
|
|
19421
19895
|
setSelectedFeedbackId(null);
|
|
@@ -19429,8 +19903,33 @@ function ReviewWorkspaceApp({ session }) {
|
|
|
19429
19903
|
setHelpOpen(false);
|
|
19430
19904
|
setFocus("stream");
|
|
19431
19905
|
resetSidebarResize();
|
|
19432
|
-
|
|
19433
|
-
|
|
19906
|
+
setVisibleFileKeys([]);
|
|
19907
|
+
localRangeIdentityRef.current = null;
|
|
19908
|
+
if (diffScrollRef.current)
|
|
19909
|
+
diffScrollRef.current.scrollTop = 0;
|
|
19910
|
+
session.setViewportStart(0);
|
|
19911
|
+
finishDialog.close();
|
|
19912
|
+
}, [finishDialog, resetSidebarResize, session]);
|
|
19913
|
+
useLayoutEffect3(() => {
|
|
19914
|
+
if (!active2)
|
|
19915
|
+
resetLocalReviewUi();
|
|
19916
|
+
}, [active2, resetLocalReviewUi]);
|
|
19917
|
+
useLayoutEffect3(() => {
|
|
19918
|
+
const identity = state?.document.identity.id;
|
|
19919
|
+
if (identity === localReviewIdentityRef.current)
|
|
19920
|
+
return;
|
|
19921
|
+
localReviewIdentityRef.current = identity;
|
|
19922
|
+
resetLocalReviewUi();
|
|
19923
|
+
}, [resetLocalReviewUi, state?.document.identity.id]);
|
|
19924
|
+
useLayoutEffect3(() => {
|
|
19925
|
+
if (!baseSelection)
|
|
19926
|
+
return;
|
|
19927
|
+
filterInputRef.current?.blur();
|
|
19928
|
+
composerBodyRef.current?.blur();
|
|
19929
|
+
replacementRef.current?.blur();
|
|
19930
|
+
resetSidebarResize();
|
|
19931
|
+
}, [baseSelection !== undefined, resetSidebarResize]);
|
|
19932
|
+
useLayoutEffect3(() => {
|
|
19434
19933
|
if (sidebarWidth > 0)
|
|
19435
19934
|
return;
|
|
19436
19935
|
resetSidebarResize();
|
|
@@ -19439,8 +19938,8 @@ function ReviewWorkspaceApp({ session }) {
|
|
|
19439
19938
|
if (sidebarWidth === 0 && focus !== "stream")
|
|
19440
19939
|
setFocus("stream");
|
|
19441
19940
|
}, [focus, sidebarWidth]);
|
|
19442
|
-
|
|
19443
|
-
if (!state?.draft)
|
|
19941
|
+
useLayoutEffect3(() => {
|
|
19942
|
+
if (!state?.draft || baseSelection)
|
|
19444
19943
|
return;
|
|
19445
19944
|
if (composerFocus === "body") {
|
|
19446
19945
|
composerBodyRef.current?.focus();
|
|
@@ -19452,7 +19951,7 @@ function ReviewWorkspaceApp({ session }) {
|
|
|
19452
19951
|
composerBodyRef.current?.blur();
|
|
19453
19952
|
replacementRef.current?.blur();
|
|
19454
19953
|
}
|
|
19455
|
-
}, [composerFocus, state?.draft]);
|
|
19954
|
+
}, [baseSelection !== undefined, composerFocus, state?.draft]);
|
|
19456
19955
|
useEffect3(() => {
|
|
19457
19956
|
const pending = pendingRangeAnchor;
|
|
19458
19957
|
const start = rangeStart;
|
|
@@ -19701,7 +20200,48 @@ function ReviewWorkspaceApp({ session }) {
|
|
|
19701
20200
|
session.invalidate();
|
|
19702
20201
|
} catch {}
|
|
19703
20202
|
}, [controller, rangeStart, session]);
|
|
20203
|
+
const requestBaseSelection = useCallback(() => {
|
|
20204
|
+
if (controller.baseSelection || finishDialog.isOpen() || helpOpen)
|
|
20205
|
+
return;
|
|
20206
|
+
controller.requestBaseSelection();
|
|
20207
|
+
}, [controller, finishDialog, helpOpen]);
|
|
20208
|
+
const cancelBaseSelection = useCallback(() => {
|
|
20209
|
+
if (controller.baseSelection?.loading || controller.baseSelection?.selecting)
|
|
20210
|
+
return;
|
|
20211
|
+
controller.cancelBaseSelection();
|
|
20212
|
+
if (!controller.state)
|
|
20213
|
+
onClose();
|
|
20214
|
+
}, [controller, onClose]);
|
|
20215
|
+
const chooseBase = useCallback((ref) => {
|
|
20216
|
+
controller.chooseBase(ref);
|
|
20217
|
+
}, [controller]);
|
|
20218
|
+
const retryBaseSelection = useCallback(() => {
|
|
20219
|
+
controller.requestBaseSelection();
|
|
20220
|
+
}, [controller]);
|
|
19704
20221
|
const executeCommand = useCallback((commandId, payload) => {
|
|
20222
|
+
if (controller.baseSelection)
|
|
20223
|
+
return false;
|
|
20224
|
+
if (commandId === "review.chooseBase") {
|
|
20225
|
+
requestBaseSelection();
|
|
20226
|
+
return true;
|
|
20227
|
+
}
|
|
20228
|
+
if (commandId === "review.toggleSinceLastReview") {
|
|
20229
|
+
if (controller.exitProjection()) {
|
|
20230
|
+
setProjectionNotice(null);
|
|
20231
|
+
session.invalidate();
|
|
20232
|
+
return true;
|
|
20233
|
+
}
|
|
20234
|
+
setProjectionNotice(null);
|
|
20235
|
+
controller.enterSinceLastReview().then((result) => {
|
|
20236
|
+
if (result.ok) {
|
|
20237
|
+
setProjectionNotice(result.fileCount === 0 ? "Nothing changed since the last review" : null);
|
|
20238
|
+
} else {
|
|
20239
|
+
setProjectionNotice(PROJECTION_NOTICES[result.reason] ?? result.message ?? "Could not open the projection");
|
|
20240
|
+
}
|
|
20241
|
+
session.invalidate();
|
|
20242
|
+
});
|
|
20243
|
+
return true;
|
|
20244
|
+
}
|
|
19705
20245
|
const current = controller.state;
|
|
19706
20246
|
if (commandId === "review.focusDiff") {
|
|
19707
20247
|
setFocus("stream");
|
|
@@ -19944,10 +20484,17 @@ function ReviewWorkspaceApp({ session }) {
|
|
|
19944
20484
|
return true;
|
|
19945
20485
|
}
|
|
19946
20486
|
return false;
|
|
19947
|
-
}, [controller, deleteFeedback, diffWidth, editFeedback, finishDialog, focus, onClose, pendingRangeAnchor, rangeStart, reanchorFeedback, selectedFeedbackId, selectFeedback, selectDiffAddress, session, sidebarWidth, toggleGap]);
|
|
20487
|
+
}, [controller, deleteFeedback, diffWidth, editFeedback, finishDialog, focus, onClose, pendingRangeAnchor, rangeStart, reanchorFeedback, requestBaseSelection, selectedFeedbackId, selectFeedback, selectDiffAddress, session, sidebarWidth, toggleGap]);
|
|
19948
20488
|
const handleKey = useCallback((event) => {
|
|
19949
20489
|
if (!active2)
|
|
19950
20490
|
return;
|
|
20491
|
+
if (baseSelection || controller.baseSelection) {
|
|
20492
|
+
if (keyName(event) === "escape") {
|
|
20493
|
+
consume2(event);
|
|
20494
|
+
cancelBaseSelection();
|
|
20495
|
+
}
|
|
20496
|
+
return;
|
|
20497
|
+
}
|
|
19951
20498
|
const name = keyName(event);
|
|
19952
20499
|
const current = controller.state;
|
|
19953
20500
|
if (name === "escape") {
|
|
@@ -19959,25 +20506,25 @@ function ReviewWorkspaceApp({ session }) {
|
|
|
19959
20506
|
setComposerFocus("body");
|
|
19960
20507
|
setComposerControlIndex(0);
|
|
19961
20508
|
session.invalidate();
|
|
19962
|
-
|
|
20509
|
+
consume2(event);
|
|
19963
20510
|
return;
|
|
19964
20511
|
}
|
|
19965
20512
|
if (finishDialog.isOpen()) {
|
|
19966
20513
|
finishDialog.close();
|
|
19967
20514
|
session.invalidate();
|
|
19968
|
-
|
|
20515
|
+
consume2(event);
|
|
19969
20516
|
return;
|
|
19970
20517
|
}
|
|
19971
20518
|
if (helpOpen) {
|
|
19972
20519
|
setHelpOpen(false);
|
|
19973
|
-
|
|
20520
|
+
consume2(event);
|
|
19974
20521
|
return;
|
|
19975
20522
|
}
|
|
19976
20523
|
if (pendingDeleteFeedbackId) {
|
|
19977
20524
|
pendingDeleteFeedbackRef.current = null;
|
|
19978
20525
|
setPendingDeleteFeedbackId(null);
|
|
19979
20526
|
session.invalidate();
|
|
19980
|
-
|
|
20527
|
+
consume2(event);
|
|
19981
20528
|
return;
|
|
19982
20529
|
}
|
|
19983
20530
|
if (focus === "filter") {
|
|
@@ -19990,19 +20537,19 @@ function ReviewWorkspaceApp({ session }) {
|
|
|
19990
20537
|
filterInputRef.current?.blur();
|
|
19991
20538
|
setFocus("stream");
|
|
19992
20539
|
}
|
|
19993
|
-
|
|
20540
|
+
consume2(event);
|
|
19994
20541
|
return;
|
|
19995
20542
|
}
|
|
19996
20543
|
if (reanchorFeedbackId) {
|
|
19997
20544
|
setReanchorFeedbackId(null);
|
|
19998
20545
|
setFeedbackMessage(null);
|
|
19999
|
-
|
|
20546
|
+
consume2(event);
|
|
20000
20547
|
return;
|
|
20001
20548
|
}
|
|
20002
20549
|
if (rangeStart || pendingRangeAnchor) {
|
|
20003
20550
|
setRangeStart(null);
|
|
20004
20551
|
setPendingRangeAnchor(null);
|
|
20005
|
-
|
|
20552
|
+
consume2(event);
|
|
20006
20553
|
return;
|
|
20007
20554
|
}
|
|
20008
20555
|
}
|
|
@@ -20010,24 +20557,24 @@ function ReviewWorkspaceApp({ session }) {
|
|
|
20010
20557
|
if (event.ctrl && (name === "1" || name === "2" || name === "3")) {
|
|
20011
20558
|
finishDialog.setDecision(name === "1" ? "comment" : name === "2" ? "approve" : "request-changes");
|
|
20012
20559
|
session.invalidate();
|
|
20013
|
-
|
|
20560
|
+
consume2(event);
|
|
20014
20561
|
return;
|
|
20015
20562
|
}
|
|
20016
20563
|
if (event.ctrl && name.toLowerCase() === "s" || name === "enter") {
|
|
20017
20564
|
submitFinish();
|
|
20018
|
-
|
|
20565
|
+
consume2(event);
|
|
20019
20566
|
return;
|
|
20020
20567
|
}
|
|
20021
20568
|
return;
|
|
20022
20569
|
}
|
|
20023
20570
|
if (helpOpen) {
|
|
20024
|
-
|
|
20571
|
+
consume2(event);
|
|
20025
20572
|
return;
|
|
20026
20573
|
}
|
|
20027
20574
|
if (current?.draft) {
|
|
20028
20575
|
if (event.ctrl && name.toLowerCase() === "s") {
|
|
20029
20576
|
saveDraft();
|
|
20030
|
-
|
|
20577
|
+
consume2(event);
|
|
20031
20578
|
} else if (name === "tab") {
|
|
20032
20579
|
const hasReplacement = canShowReplacementDraft(current);
|
|
20033
20580
|
const backwards = event.shift === true;
|
|
@@ -20057,14 +20604,14 @@ function ReviewWorkspaceApp({ session }) {
|
|
|
20057
20604
|
setComposerControlIndex(0);
|
|
20058
20605
|
}
|
|
20059
20606
|
}
|
|
20060
|
-
|
|
20607
|
+
consume2(event);
|
|
20061
20608
|
}
|
|
20062
20609
|
return;
|
|
20063
20610
|
}
|
|
20064
20611
|
if (focus === "filter" && name === "enter") {
|
|
20065
20612
|
filterInputRef.current?.blur();
|
|
20066
20613
|
setFocus("stream");
|
|
20067
|
-
|
|
20614
|
+
consume2(event);
|
|
20068
20615
|
return;
|
|
20069
20616
|
}
|
|
20070
20617
|
if (focus === "filter" && !event.ctrl && !event.meta && !event.option && isPrintableFilterKey(name))
|
|
@@ -20074,17 +20621,30 @@ function ReviewWorkspaceApp({ session }) {
|
|
|
20074
20621
|
if (!command || current && !command.available(current))
|
|
20075
20622
|
return;
|
|
20076
20623
|
if (executeCommand(command.id))
|
|
20077
|
-
|
|
20078
|
-
}, [active2, composerControlIndex, composerFocus, controller, executeCommand, finishDialog, focus, helpOpen, onClose, pendingDeleteFeedbackId, pendingRangeAnchor, rangeStart, reanchorFeedbackId, saveDraft, session, submitFinish]);
|
|
20079
|
-
|
|
20624
|
+
consume2(event);
|
|
20625
|
+
}, [active2, baseSelection, cancelBaseSelection, composerControlIndex, composerFocus, controller, executeCommand, finishDialog, focus, helpOpen, onClose, pendingDeleteFeedbackId, pendingRangeAnchor, rangeStart, reanchorFeedbackId, saveDraft, session, submitFinish]);
|
|
20626
|
+
useKeyboard2(handleKey);
|
|
20627
|
+
const basePicker = baseSelection ? /* @__PURE__ */ jsx6(ReviewBasePicker, {
|
|
20628
|
+
selection: baseSelection,
|
|
20629
|
+
width: dimensions2.width,
|
|
20630
|
+
height: dimensions2.height,
|
|
20631
|
+
active: active2,
|
|
20632
|
+
...controller.error ? { warning: `! ${controller.error.title}: ${controller.error.detail}` } : {},
|
|
20633
|
+
onChoose: chooseBase,
|
|
20634
|
+
onCancel: cancelBaseSelection,
|
|
20635
|
+
onRetry: retryBaseSelection
|
|
20636
|
+
}) : null;
|
|
20080
20637
|
if (!state) {
|
|
20081
|
-
return /* @__PURE__ */
|
|
20638
|
+
return /* @__PURE__ */ jsxs5("box", {
|
|
20082
20639
|
id: "react-review-workspace",
|
|
20083
20640
|
visible: active2,
|
|
20084
|
-
style: { width: "100%", height: "100%" },
|
|
20085
|
-
children:
|
|
20086
|
-
|
|
20087
|
-
|
|
20641
|
+
style: { position: "relative", width: "100%", height: "100%", overflow: "hidden" },
|
|
20642
|
+
children: [
|
|
20643
|
+
/* @__PURE__ */ jsx6("text", {
|
|
20644
|
+
content: "Choose a base branch to start reviewing."
|
|
20645
|
+
}),
|
|
20646
|
+
basePicker
|
|
20647
|
+
]
|
|
20088
20648
|
});
|
|
20089
20649
|
}
|
|
20090
20650
|
function selectFile(fileKey, nextFocus2 = "stream") {
|
|
@@ -20106,28 +20666,55 @@ function ReviewWorkspaceApp({ session }) {
|
|
|
20106
20666
|
const suggestionAllowed = state.draft !== null && state.draft.anchor.kind === "range" && state.draft.anchor.side === "new" && state.document.files.some((file) => file.key === state.draft?.anchor.fileKey && file.source !== "binary" && file.source !== "too-large");
|
|
20107
20667
|
const replacementInvalid = suggestionReplacementInvalid(state);
|
|
20108
20668
|
const orphanedFeedback = state.feedback.filter((feedback) => !state.document.files.some((file) => file.key === feedback.anchor.fileKey));
|
|
20109
|
-
return /* @__PURE__ */
|
|
20669
|
+
return /* @__PURE__ */ jsxs5("box", {
|
|
20110
20670
|
id: "react-review-workspace",
|
|
20111
20671
|
visible: active2,
|
|
20112
20672
|
onMouse: handleSidebarResizeMouse,
|
|
20113
20673
|
style: { position: "relative", width: "100%", height: "100%", flexDirection: "column", overflow: "hidden" },
|
|
20114
20674
|
children: [
|
|
20115
|
-
/* @__PURE__ */
|
|
20675
|
+
/* @__PURE__ */ jsxs5("box", {
|
|
20116
20676
|
id: "react-review-header",
|
|
20117
|
-
style: { width: "100%", height: 3, flexShrink: 0 },
|
|
20118
|
-
children:
|
|
20119
|
-
|
|
20120
|
-
|
|
20121
|
-
|
|
20122
|
-
|
|
20677
|
+
style: { width: "100%", height: 3, flexShrink: 0, flexDirection: "column" },
|
|
20678
|
+
children: [
|
|
20679
|
+
reviewHeaderLines(state, dimensions2.width).map((line, lineIndex) => /* @__PURE__ */ jsx6("box", {
|
|
20680
|
+
style: { width: "100%", height: 1, flexShrink: 0, flexDirection: "row" },
|
|
20681
|
+
children: line.map((span, spanIndex) => span.action === "choose-base" ? /* @__PURE__ */ jsx6("box", {
|
|
20682
|
+
id: "review-base-selector",
|
|
20683
|
+
style: { width: cellWidth(span.text), height: 1, flexShrink: 0 },
|
|
20684
|
+
onMouseUp: (event) => {
|
|
20685
|
+
event.preventDefault();
|
|
20686
|
+
event.stopPropagation();
|
|
20687
|
+
requestBaseSelection();
|
|
20688
|
+
},
|
|
20689
|
+
children: /* @__PURE__ */ jsx6("text", {
|
|
20690
|
+
content: span.text,
|
|
20691
|
+
fg: COLORS2.strong,
|
|
20692
|
+
selectable: false,
|
|
20693
|
+
wrapMode: "none",
|
|
20694
|
+
truncate: true
|
|
20695
|
+
})
|
|
20696
|
+
}, spanIndex) : /* @__PURE__ */ jsx6("text", {
|
|
20697
|
+
content: span.text,
|
|
20698
|
+
fg: span.style === "dim" ? COLORS2.dim : COLORS2.strong,
|
|
20699
|
+
wrapMode: "none",
|
|
20700
|
+
truncate: true
|
|
20701
|
+
}, spanIndex))
|
|
20702
|
+
}, lineIndex)),
|
|
20703
|
+
controller.error ? /* @__PURE__ */ jsx6("text", {
|
|
20704
|
+
content: `! ${controller.error.title}: ${controller.error.detail}`,
|
|
20705
|
+
fg: COLORS2.dim,
|
|
20706
|
+
wrapMode: "none",
|
|
20707
|
+
truncate: true
|
|
20708
|
+
}) : null
|
|
20709
|
+
]
|
|
20123
20710
|
}),
|
|
20124
|
-
/* @__PURE__ */
|
|
20711
|
+
/* @__PURE__ */ jsxs5("box", {
|
|
20125
20712
|
id: "react-review-body",
|
|
20126
20713
|
style: { width: "100%", flexGrow: 1, flexDirection: "row", overflow: "hidden" },
|
|
20127
20714
|
children: [
|
|
20128
|
-
sidebarWidth > 0 ? /* @__PURE__ */
|
|
20715
|
+
sidebarWidth > 0 ? /* @__PURE__ */ jsxs5(Fragment, {
|
|
20129
20716
|
children: [
|
|
20130
|
-
/* @__PURE__ */
|
|
20717
|
+
/* @__PURE__ */ jsxs5("box", {
|
|
20131
20718
|
id: "react-review-sidebar",
|
|
20132
20719
|
borderColor: sidebarFocused ? ANSI_GREEN : DEFAULT_FOREGROUND,
|
|
20133
20720
|
title: `[1] Files ${sidebarFileEntries.length}/${state.document.files.length}`,
|
|
@@ -20135,21 +20722,21 @@ function ReviewWorkspaceApp({ session }) {
|
|
|
20135
20722
|
style: { width: sidebarWidth, height: "100%", flexShrink: 0, border: true, flexDirection: "column" },
|
|
20136
20723
|
onMouseDown: () => setFocus("sidebar"),
|
|
20137
20724
|
children: [
|
|
20138
|
-
/* @__PURE__ */
|
|
20725
|
+
/* @__PURE__ */ jsxs5("box", {
|
|
20139
20726
|
id: "review-file-filter",
|
|
20140
20727
|
style: { width: "100%", height: 1, flexShrink: 0, flexDirection: "row" },
|
|
20141
20728
|
children: [
|
|
20142
|
-
/* @__PURE__ */
|
|
20729
|
+
/* @__PURE__ */ jsx6("text", {
|
|
20143
20730
|
content: "/ ",
|
|
20144
20731
|
fg: COLORS2.dim
|
|
20145
20732
|
}),
|
|
20146
|
-
/* @__PURE__ */
|
|
20733
|
+
/* @__PURE__ */ jsx6("input", {
|
|
20147
20734
|
id: "review-file-filter-input",
|
|
20148
20735
|
ref: filterInputRef,
|
|
20149
20736
|
width: Math.max(4, sidebarWidth - 4),
|
|
20150
20737
|
value: state.filter.query,
|
|
20151
20738
|
placeholder: "filter files",
|
|
20152
|
-
focused: focus === "filter",
|
|
20739
|
+
focused: focus === "filter" && !baseSelection,
|
|
20153
20740
|
onMouseUp: () => {
|
|
20154
20741
|
if (resizingSidebarRef.current || resizeReleaseSuppressionRef.current)
|
|
20155
20742
|
return;
|
|
@@ -20176,28 +20763,28 @@ function ReviewWorkspaceApp({ session }) {
|
|
|
20176
20763
|
filterInputRef.current?.blur();
|
|
20177
20764
|
setFocus("stream");
|
|
20178
20765
|
}
|
|
20179
|
-
|
|
20766
|
+
consume2(event);
|
|
20180
20767
|
}
|
|
20181
20768
|
})
|
|
20182
20769
|
]
|
|
20183
20770
|
}),
|
|
20184
|
-
/* @__PURE__ */
|
|
20771
|
+
/* @__PURE__ */ jsx6("scrollbox", {
|
|
20185
20772
|
id: "react-review-sidebar-scrollbox",
|
|
20186
|
-
focused: focus === "sidebar",
|
|
20773
|
+
focused: focus === "sidebar" && !baseSelection,
|
|
20187
20774
|
width: "100%",
|
|
20188
20775
|
flexGrow: 1,
|
|
20189
20776
|
scrollY: true,
|
|
20190
20777
|
viewportCulling: true,
|
|
20191
20778
|
verticalScrollbarOptions: { visible: false },
|
|
20192
20779
|
onMouseDown: () => setFocus("sidebar"),
|
|
20193
|
-
children: /* @__PURE__ */
|
|
20780
|
+
children: /* @__PURE__ */ jsx6("box", {
|
|
20194
20781
|
style: { width: "100%", flexDirection: "column" },
|
|
20195
20782
|
children: sidebarEntries.map((entry) => {
|
|
20196
20783
|
if (entry.kind === "group") {
|
|
20197
|
-
return /* @__PURE__ */
|
|
20784
|
+
return /* @__PURE__ */ jsx6("box", {
|
|
20198
20785
|
id: `review-file-group:${entry.label}`,
|
|
20199
20786
|
style: { width: "100%", height: 1, backgroundColor: REVIEW_SIDEBAR_THEME.panel, paddingLeft: 1 },
|
|
20200
|
-
children: /* @__PURE__ */
|
|
20787
|
+
children: /* @__PURE__ */ jsx6("text", {
|
|
20201
20788
|
fg: REVIEW_SIDEBAR_THEME.muted,
|
|
20202
20789
|
children: fitText(entry.label, sidebarTextWidth)
|
|
20203
20790
|
})
|
|
@@ -20206,11 +20793,11 @@ function ReviewWorkspaceApp({ session }) {
|
|
|
20206
20793
|
const selected = entry.id === state.selection.fileKey;
|
|
20207
20794
|
const rowBackground = selected ? REVIEW_SIDEBAR_THEME.panelAlt : REVIEW_SIDEBAR_THEME.panel;
|
|
20208
20795
|
const stats = sidebarEntryStats(entry);
|
|
20209
|
-
const { icon, color:
|
|
20796
|
+
const { icon, color: color3 } = getFileStateIcon(entry);
|
|
20210
20797
|
const iconWidth = icon ? 2 : 0;
|
|
20211
20798
|
const statsSectionWidth = sidebarStatsWidth > 0 ? sidebarStatsWidth + 1 : 0;
|
|
20212
20799
|
const nameWidth = Math.max(1, sidebarTextWidth - 1 - iconWidth - statsSectionWidth);
|
|
20213
|
-
return /* @__PURE__ */
|
|
20800
|
+
return /* @__PURE__ */ jsxs5("box", {
|
|
20214
20801
|
id: `review-file-row:${entry.id}`,
|
|
20215
20802
|
style: { width: "100%", height: 1, backgroundColor: rowBackground, flexDirection: "row" },
|
|
20216
20803
|
onMouseDown: () => setFocus("sidebar"),
|
|
@@ -20218,30 +20805,30 @@ function ReviewWorkspaceApp({ session }) {
|
|
|
20218
20805
|
executeCommand("review.selectFile", { fileKey: entry.id, nextFocus: "sidebar" });
|
|
20219
20806
|
},
|
|
20220
20807
|
children: [
|
|
20221
|
-
/* @__PURE__ */
|
|
20808
|
+
/* @__PURE__ */ jsx6("box", {
|
|
20222
20809
|
style: { width: 1, height: 1, backgroundColor: selected ? REVIEW_SIDEBAR_THEME.accent : rowBackground }
|
|
20223
20810
|
}),
|
|
20224
|
-
/* @__PURE__ */
|
|
20811
|
+
/* @__PURE__ */ jsxs5("box", {
|
|
20225
20812
|
style: { flexGrow: 1, height: 1, paddingLeft: 0, flexDirection: "row", backgroundColor: rowBackground },
|
|
20226
20813
|
children: [
|
|
20227
|
-
icon ? /* @__PURE__ */
|
|
20228
|
-
fg:
|
|
20814
|
+
icon ? /* @__PURE__ */ jsx6("text", {
|
|
20815
|
+
fg: color3,
|
|
20229
20816
|
children: `${icon} `
|
|
20230
20817
|
}) : null,
|
|
20231
|
-
/* @__PURE__ */
|
|
20818
|
+
/* @__PURE__ */ jsx6("text", {
|
|
20232
20819
|
fg: REVIEW_SIDEBAR_THEME.text,
|
|
20233
20820
|
children: padText(fitText(entry.name, nameWidth), nameWidth)
|
|
20234
20821
|
}),
|
|
20235
|
-
statsSectionWidth > 0 ? /* @__PURE__ */
|
|
20822
|
+
statsSectionWidth > 0 ? /* @__PURE__ */ jsx6("box", {
|
|
20236
20823
|
style: { width: statsSectionWidth, height: 1, flexDirection: "row", justifyContent: "flex-end", backgroundColor: rowBackground },
|
|
20237
|
-
children: stats.map((stat3, index) => /* @__PURE__ */
|
|
20824
|
+
children: stats.map((stat3, index) => /* @__PURE__ */ jsxs5("box", {
|
|
20238
20825
|
style: { height: 1, flexDirection: "row", backgroundColor: rowBackground },
|
|
20239
20826
|
children: [
|
|
20240
|
-
index > 0 ? /* @__PURE__ */
|
|
20827
|
+
index > 0 ? /* @__PURE__ */ jsx6("text", {
|
|
20241
20828
|
fg: selected ? REVIEW_SIDEBAR_THEME.text : REVIEW_SIDEBAR_THEME.muted,
|
|
20242
20829
|
children: " "
|
|
20243
20830
|
}) : null,
|
|
20244
|
-
/* @__PURE__ */
|
|
20831
|
+
/* @__PURE__ */ jsx6("text", {
|
|
20245
20832
|
fg: stat3.kind === "agent-comment" ? REVIEW_SIDEBAR_THEME.noteBorder : stat3.kind === "addition" ? REVIEW_SIDEBAR_THEME.badgeAdded : REVIEW_SIDEBAR_THEME.badgeRemoved,
|
|
20246
20833
|
children: stat3.text
|
|
20247
20834
|
})
|
|
@@ -20257,7 +20844,7 @@ function ReviewWorkspaceApp({ session }) {
|
|
|
20257
20844
|
})
|
|
20258
20845
|
]
|
|
20259
20846
|
}),
|
|
20260
|
-
/* @__PURE__ */
|
|
20847
|
+
/* @__PURE__ */ jsx6("box", {
|
|
20261
20848
|
id: "review-pane-resize-bar",
|
|
20262
20849
|
style: { width: REVIEW_RESIZE_BAR_WIDTH, height: "100%", flexShrink: 0 },
|
|
20263
20850
|
onMouseOver: () => setResizeBarHovered(true),
|
|
@@ -20290,7 +20877,7 @@ function ReviewWorkspaceApp({ session }) {
|
|
|
20290
20877
|
event.preventDefault();
|
|
20291
20878
|
event.stopPropagation();
|
|
20292
20879
|
},
|
|
20293
|
-
children: /* @__PURE__ */
|
|
20880
|
+
children: /* @__PURE__ */ jsx6("text", {
|
|
20294
20881
|
id: "review-pane-resize-bar-glyphs",
|
|
20295
20882
|
selectable: false,
|
|
20296
20883
|
content: splitterGlyphs("vertical", REVIEW_RESIZE_BAR_WIDTH, resizeBarHeight, resizeBarHovered || resizingSidebar),
|
|
@@ -20303,20 +20890,20 @@ function ReviewWorkspaceApp({ session }) {
|
|
|
20303
20890
|
})
|
|
20304
20891
|
]
|
|
20305
20892
|
}) : null,
|
|
20306
|
-
/* @__PURE__ */
|
|
20893
|
+
/* @__PURE__ */ jsx6("box", {
|
|
20307
20894
|
id: "react-review-diff",
|
|
20308
20895
|
borderColor: diffFocused ? ANSI_GREEN : DEFAULT_FOREGROUND,
|
|
20309
20896
|
title: `[0] Diff — ${layout}`,
|
|
20310
20897
|
titleColor: diffFocused ? ANSI_GREEN : DEFAULT_FOREGROUND,
|
|
20311
20898
|
style: { width: "100%", height: "100%", flexGrow: 1, border: true, minWidth: 0 },
|
|
20312
20899
|
onMouseDown: () => setFocus("stream"),
|
|
20313
|
-
children: /* @__PURE__ */
|
|
20900
|
+
children: /* @__PURE__ */ jsx6(ReviewDiffPane, {
|
|
20314
20901
|
files,
|
|
20315
20902
|
state,
|
|
20316
20903
|
layout,
|
|
20317
20904
|
width: diffWidth,
|
|
20318
20905
|
height: diffHeight,
|
|
20319
|
-
focused: focus === "stream",
|
|
20906
|
+
focused: focus === "stream" && !baseSelection,
|
|
20320
20907
|
scrollRef: diffScrollRef,
|
|
20321
20908
|
selectedFileKey: state.selection.fileKey,
|
|
20322
20909
|
selectedHunkIndex: state.selection.hunkIndex,
|
|
@@ -20337,14 +20924,14 @@ function ReviewWorkspaceApp({ session }) {
|
|
|
20337
20924
|
},
|
|
20338
20925
|
selectedFeedbackId,
|
|
20339
20926
|
onViewportChange: session.setViewportStart
|
|
20340
|
-
})
|
|
20927
|
+
}, state.document.identity.id)
|
|
20341
20928
|
})
|
|
20342
20929
|
]
|
|
20343
20930
|
}),
|
|
20344
|
-
orphanedFeedback.length > 0 ? /* @__PURE__ */
|
|
20931
|
+
orphanedFeedback.length > 0 ? /* @__PURE__ */ jsx6("box", {
|
|
20345
20932
|
id: "review-orphaned-feedback",
|
|
20346
20933
|
style: { position: "absolute", left: 1, bottom: 1, width: Math.max(20, dimensions2.width - 2), height: Math.min(4, orphanedFeedback.length), zIndex: 50, border: true, flexDirection: "column", backgroundColor: "#202020" },
|
|
20347
|
-
children: orphanedFeedback.slice(0, 4).map((feedback) => /* @__PURE__ */
|
|
20934
|
+
children: orphanedFeedback.slice(0, 4).map((feedback) => /* @__PURE__ */ jsxs5("box", {
|
|
20348
20935
|
style: { width: "100%", height: 1, flexDirection: "row" },
|
|
20349
20936
|
onMouseUp: () => {
|
|
20350
20937
|
if (resizingSidebarRef.current || resizeReleaseSuppressionRef.current)
|
|
@@ -20352,32 +20939,32 @@ function ReviewWorkspaceApp({ session }) {
|
|
|
20352
20939
|
executeCommand("review.selectFeedback", feedback.id);
|
|
20353
20940
|
},
|
|
20354
20941
|
children: [
|
|
20355
|
-
/* @__PURE__ */
|
|
20942
|
+
/* @__PURE__ */ jsx6("text", {
|
|
20356
20943
|
content: `${feedback.resolution} feedback ${feedback.id} — ${feedback.anchor.kind === "range" ? `${feedback.anchor.side}:${feedback.anchor.startLine === feedback.anchor.endLine ? feedback.anchor.startLine : `${feedback.anchor.startLine}-${feedback.anchor.endLine}`}` : "file"} — [a]nchor`,
|
|
20357
20944
|
wrapMode: "none",
|
|
20358
20945
|
truncate: true
|
|
20359
20946
|
}),
|
|
20360
|
-
/* @__PURE__ */
|
|
20947
|
+
/* @__PURE__ */ jsx6("box", {
|
|
20361
20948
|
id: `review-delete-feedback:${feedback.id}`,
|
|
20362
20949
|
onMouseUp: () => {
|
|
20363
20950
|
if (resizingSidebarRef.current || resizeReleaseSuppressionRef.current)
|
|
20364
20951
|
return;
|
|
20365
20952
|
deleteFeedback(feedback.id);
|
|
20366
20953
|
},
|
|
20367
|
-
children: /* @__PURE__ */
|
|
20954
|
+
children: /* @__PURE__ */ jsx6("text", {
|
|
20368
20955
|
content: pendingDeleteFeedbackId === feedback.id ? "[delete again]" : "[delete]",
|
|
20369
20956
|
wrapMode: "none",
|
|
20370
20957
|
truncate: true
|
|
20371
20958
|
})
|
|
20372
20959
|
}),
|
|
20373
|
-
/* @__PURE__ */
|
|
20960
|
+
/* @__PURE__ */ jsx6("box", {
|
|
20374
20961
|
id: `review-reanchor-feedback:${feedback.id}`,
|
|
20375
20962
|
onMouseUp: () => {
|
|
20376
20963
|
if (resizingSidebarRef.current || resizeReleaseSuppressionRef.current)
|
|
20377
20964
|
return;
|
|
20378
20965
|
reanchorFeedback(feedback.id);
|
|
20379
20966
|
},
|
|
20380
|
-
children: /* @__PURE__ */
|
|
20967
|
+
children: /* @__PURE__ */ jsx6("text", {
|
|
20381
20968
|
content: "[re-anchor]",
|
|
20382
20969
|
wrapMode: "none",
|
|
20383
20970
|
truncate: true
|
|
@@ -20386,29 +20973,29 @@ function ReviewWorkspaceApp({ session }) {
|
|
|
20386
20973
|
]
|
|
20387
20974
|
}, feedback.id))
|
|
20388
20975
|
}) : null,
|
|
20389
|
-
feedbackMessage ? /* @__PURE__ */
|
|
20976
|
+
feedbackMessage ? /* @__PURE__ */ jsx6("box", {
|
|
20390
20977
|
id: "review-feedback-message",
|
|
20391
20978
|
style: { position: "absolute", left: 1, bottom: orphanedFeedback.length > 0 ? Math.min(5, orphanedFeedback.length + 1) : 1, width: Math.max(20, dimensions2.width - 2), height: 1, zIndex: 55, backgroundColor: "#202020" },
|
|
20392
|
-
children: /* @__PURE__ */
|
|
20979
|
+
children: /* @__PURE__ */ jsx6("text", {
|
|
20393
20980
|
content: feedbackMessage,
|
|
20394
20981
|
wrapMode: "none",
|
|
20395
20982
|
truncate: true
|
|
20396
20983
|
})
|
|
20397
20984
|
}) : null,
|
|
20398
|
-
state.draft ? /* @__PURE__ */
|
|
20985
|
+
state.draft ? /* @__PURE__ */ jsxs5("box", {
|
|
20399
20986
|
id: "review-feedback-composer",
|
|
20400
20987
|
style: { width: "100%", height: composerHeight, flexShrink: 0, border: true, flexDirection: "column" },
|
|
20401
20988
|
children: [
|
|
20402
|
-
/* @__PURE__ */
|
|
20989
|
+
/* @__PURE__ */ jsx6("text", {
|
|
20403
20990
|
content: feedbackDraftText(state),
|
|
20404
20991
|
wrapMode: "none",
|
|
20405
20992
|
truncate: true
|
|
20406
20993
|
}),
|
|
20407
|
-
/* @__PURE__ */
|
|
20994
|
+
/* @__PURE__ */ jsxs5("box", {
|
|
20408
20995
|
id: "review-feedback-controls",
|
|
20409
20996
|
style: { width: "100%", height: 1, flexDirection: "row" },
|
|
20410
20997
|
children: [
|
|
20411
|
-
/* @__PURE__ */
|
|
20998
|
+
/* @__PURE__ */ jsx6("box", {
|
|
20412
20999
|
id: "review-feedback-kind-note",
|
|
20413
21000
|
style: composerFocus === "controls" && composerControlIndex === 0 ? { backgroundColor: "#365f8a" } : {},
|
|
20414
21001
|
onMouseUp: () => {
|
|
@@ -20424,11 +21011,11 @@ function ReviewWorkspaceApp({ session }) {
|
|
|
20424
21011
|
session.invalidate();
|
|
20425
21012
|
} catch {}
|
|
20426
21013
|
},
|
|
20427
|
-
children: /* @__PURE__ */
|
|
21014
|
+
children: /* @__PURE__ */ jsx6("text", {
|
|
20428
21015
|
content: state.draft.kind === "note" ? "[Note]" : " Note "
|
|
20429
21016
|
})
|
|
20430
21017
|
}),
|
|
20431
|
-
/* @__PURE__ */
|
|
21018
|
+
/* @__PURE__ */ jsx6("box", {
|
|
20432
21019
|
id: "review-feedback-kind-suggestion",
|
|
20433
21020
|
style: composerFocus === "controls" && composerControlIndex === 1 ? { backgroundColor: "#365f8a" } : {},
|
|
20434
21021
|
onMouseUp: () => {
|
|
@@ -20445,11 +21032,11 @@ function ReviewWorkspaceApp({ session }) {
|
|
|
20445
21032
|
session.invalidate();
|
|
20446
21033
|
} catch {}
|
|
20447
21034
|
},
|
|
20448
|
-
children: /* @__PURE__ */
|
|
21035
|
+
children: /* @__PURE__ */ jsx6("text", {
|
|
20449
21036
|
content: state.draft.kind === "suggestion" ? "[Suggestion]" : " Suggestion "
|
|
20450
21037
|
})
|
|
20451
21038
|
}),
|
|
20452
|
-
/* @__PURE__ */
|
|
21039
|
+
/* @__PURE__ */ jsx6("box", {
|
|
20453
21040
|
id: "review-feedback-severity-comment",
|
|
20454
21041
|
style: composerFocus === "controls" && composerControlIndex === 2 ? { backgroundColor: "#365f8a" } : {},
|
|
20455
21042
|
onMouseUp: () => {
|
|
@@ -20465,11 +21052,11 @@ function ReviewWorkspaceApp({ session }) {
|
|
|
20465
21052
|
session.invalidate();
|
|
20466
21053
|
} catch {}
|
|
20467
21054
|
},
|
|
20468
|
-
children: /* @__PURE__ */
|
|
21055
|
+
children: /* @__PURE__ */ jsx6("text", {
|
|
20469
21056
|
content: state.draft.severity === "comment" ? "[Comment]" : " Comment "
|
|
20470
21057
|
})
|
|
20471
21058
|
}),
|
|
20472
|
-
/* @__PURE__ */
|
|
21059
|
+
/* @__PURE__ */ jsx6("box", {
|
|
20473
21060
|
id: "review-feedback-severity-blocking",
|
|
20474
21061
|
style: composerFocus === "controls" && composerControlIndex === 3 ? { backgroundColor: "#365f8a" } : {},
|
|
20475
21062
|
onMouseUp: () => {
|
|
@@ -20485,11 +21072,11 @@ function ReviewWorkspaceApp({ session }) {
|
|
|
20485
21072
|
session.invalidate();
|
|
20486
21073
|
} catch {}
|
|
20487
21074
|
},
|
|
20488
|
-
children: /* @__PURE__ */
|
|
21075
|
+
children: /* @__PURE__ */ jsx6("text", {
|
|
20489
21076
|
content: state.draft.severity === "blocking" ? "[Blocking]" : " Blocking "
|
|
20490
21077
|
})
|
|
20491
21078
|
}),
|
|
20492
|
-
/* @__PURE__ */
|
|
21079
|
+
/* @__PURE__ */ jsx6("box", {
|
|
20493
21080
|
id: "review-feedback-save",
|
|
20494
21081
|
style: composerFocus === "controls" && composerControlIndex === 4 ? { backgroundColor: "#365f8a" } : {},
|
|
20495
21082
|
onMouseUp: () => {
|
|
@@ -20499,11 +21086,11 @@ function ReviewWorkspaceApp({ session }) {
|
|
|
20499
21086
|
setComposerControlIndex(4);
|
|
20500
21087
|
saveDraft();
|
|
20501
21088
|
},
|
|
20502
|
-
children: /* @__PURE__ */
|
|
21089
|
+
children: /* @__PURE__ */ jsx6("text", {
|
|
20503
21090
|
content: replacementInvalid ? "[Save disabled]" : " [Save] "
|
|
20504
21091
|
})
|
|
20505
21092
|
}),
|
|
20506
|
-
/* @__PURE__ */
|
|
21093
|
+
/* @__PURE__ */ jsx6("box", {
|
|
20507
21094
|
id: "review-feedback-cancel",
|
|
20508
21095
|
style: composerFocus === "controls" && composerControlIndex === 5 ? { backgroundColor: "#365f8a" } : {},
|
|
20509
21096
|
onMouseUp: () => {
|
|
@@ -20521,19 +21108,19 @@ function ReviewWorkspaceApp({ session }) {
|
|
|
20521
21108
|
session.invalidate();
|
|
20522
21109
|
} catch {}
|
|
20523
21110
|
},
|
|
20524
|
-
children: /* @__PURE__ */
|
|
21111
|
+
children: /* @__PURE__ */ jsx6("text", {
|
|
20525
21112
|
content: " [Cancel] "
|
|
20526
21113
|
})
|
|
20527
21114
|
})
|
|
20528
21115
|
]
|
|
20529
21116
|
}),
|
|
20530
|
-
/* @__PURE__ */
|
|
21117
|
+
/* @__PURE__ */ jsx6("textarea", {
|
|
20531
21118
|
id: "review-feedback-body",
|
|
20532
21119
|
ref: composerBodyRef,
|
|
20533
21120
|
width: "100%",
|
|
20534
21121
|
height: 2,
|
|
20535
21122
|
initialValue: state.draft.body,
|
|
20536
|
-
focused: composerFocus === "body",
|
|
21123
|
+
focused: composerFocus === "body" && !baseSelection,
|
|
20537
21124
|
keyBindings: [{ name: "escape", action: "submit" }],
|
|
20538
21125
|
onSubmit: () => {
|
|
20539
21126
|
const latest = controller.state;
|
|
@@ -20558,10 +21145,10 @@ function ReviewWorkspaceApp({ session }) {
|
|
|
20558
21145
|
setComposerFocus("body");
|
|
20559
21146
|
setComposerControlIndex(0);
|
|
20560
21147
|
session.invalidate();
|
|
20561
|
-
|
|
21148
|
+
consume2(event);
|
|
20562
21149
|
} else if (event.ctrl && name.toLowerCase() === "s" && latest?.draft) {
|
|
20563
21150
|
saveDraft();
|
|
20564
|
-
|
|
21151
|
+
consume2(event);
|
|
20565
21152
|
}
|
|
20566
21153
|
},
|
|
20567
21154
|
onContentChange: () => {
|
|
@@ -20573,16 +21160,16 @@ function ReviewWorkspaceApp({ session }) {
|
|
|
20573
21160
|
controller.dispatch(planReviewIntent(latest, { type: "feedback/update-draft", body }));
|
|
20574
21161
|
} catch {}
|
|
20575
21162
|
}
|
|
20576
|
-
}),
|
|
20577
|
-
canShowReplacementDraft(state) ? /* @__PURE__ */
|
|
21163
|
+
}, state.document.identity.id),
|
|
21164
|
+
canShowReplacementDraft(state) ? /* @__PURE__ */ jsxs5(Fragment, {
|
|
20578
21165
|
children: [
|
|
20579
|
-
/* @__PURE__ */
|
|
21166
|
+
/* @__PURE__ */ jsx6("textarea", {
|
|
20580
21167
|
id: "review-feedback-replacement",
|
|
20581
21168
|
ref: replacementRef,
|
|
20582
21169
|
width: "100%",
|
|
20583
21170
|
height: 2,
|
|
20584
21171
|
initialValue: state.draft.replacement ?? "",
|
|
20585
|
-
focused: composerFocus === "replacement",
|
|
21172
|
+
focused: composerFocus === "replacement" && !baseSelection,
|
|
20586
21173
|
wrapMode: "char",
|
|
20587
21174
|
placeholder: "Replacement text",
|
|
20588
21175
|
onContentChange: () => {
|
|
@@ -20595,8 +21182,8 @@ function ReviewWorkspaceApp({ session }) {
|
|
|
20595
21182
|
session.invalidate();
|
|
20596
21183
|
} catch {}
|
|
20597
21184
|
}
|
|
20598
|
-
}),
|
|
20599
|
-
replacementInvalid ? /* @__PURE__ */
|
|
21185
|
+
}, state.document.identity.id),
|
|
21186
|
+
replacementInvalid ? /* @__PURE__ */ jsx6("text", {
|
|
20600
21187
|
id: "review-feedback-replacement-error",
|
|
20601
21188
|
content: "Invalid replacement: enter non-whitespace text.",
|
|
20602
21189
|
wrapMode: "none",
|
|
@@ -20606,10 +21193,10 @@ function ReviewWorkspaceApp({ session }) {
|
|
|
20606
21193
|
}) : null
|
|
20607
21194
|
]
|
|
20608
21195
|
}) : null,
|
|
20609
|
-
helpOpen ? /* @__PURE__ */
|
|
21196
|
+
helpOpen ? /* @__PURE__ */ jsx6("box", {
|
|
20610
21197
|
id: "review-help-dialog",
|
|
20611
|
-
style: { position: "absolute", left: Math.max(1, Math.floor(dimensions2.width / 10)), top: 2, width: Math.max(50, Math.floor(dimensions2.width * 4 / 5)), height: Math.min(
|
|
20612
|
-
children: /* @__PURE__ */
|
|
21198
|
+
style: { position: "absolute", left: Math.max(1, Math.floor(dimensions2.width / 10)), top: 2, width: Math.max(50, Math.floor(dimensions2.width * 4 / 5)), height: Math.min(27, Math.max(14, dimensions2.height - 3)), zIndex: 70, border: true, flexDirection: "column", backgroundColor: "#202020" },
|
|
21199
|
+
children: /* @__PURE__ */ jsx6("text", {
|
|
20613
21200
|
content: `Review commands
|
|
20614
21201
|
${reviewHelp(focus, state)}
|
|
20615
21202
|
Esc close this help`,
|
|
@@ -20617,16 +21204,16 @@ Esc close this help`,
|
|
|
20617
21204
|
truncate: true
|
|
20618
21205
|
})
|
|
20619
21206
|
}) : null,
|
|
20620
|
-
finishDialog.isOpen() ? /* @__PURE__ */
|
|
21207
|
+
finishDialog.isOpen() ? /* @__PURE__ */ jsxs5("box", {
|
|
20621
21208
|
id: "review-finish-dialog",
|
|
20622
21209
|
style: { position: "absolute", left: Math.max(1, Math.floor(dimensions2.width / 8)), top: 3, width: Math.max(40, Math.floor(dimensions2.width * 3 / 4)), height: 10, zIndex: 60, border: true, flexDirection: "column", backgroundColor: "#202020" },
|
|
20623
21210
|
children: [
|
|
20624
|
-
/* @__PURE__ */
|
|
21211
|
+
/* @__PURE__ */ jsx6("text", {
|
|
20625
21212
|
content: `Finish review — ${finishDialog.getDecision()}`,
|
|
20626
21213
|
wrapMode: "none",
|
|
20627
21214
|
truncate: true
|
|
20628
21215
|
}),
|
|
20629
|
-
/* @__PURE__ */
|
|
21216
|
+
/* @__PURE__ */ jsx6("textarea", {
|
|
20630
21217
|
id: "review-finish-summary",
|
|
20631
21218
|
ref: finishSummaryRef,
|
|
20632
21219
|
width: "100%",
|
|
@@ -20640,10 +21227,10 @@ Esc close this help`,
|
|
|
20640
21227
|
if (name === "escape") {
|
|
20641
21228
|
finishDialog.close();
|
|
20642
21229
|
session.invalidate();
|
|
20643
|
-
|
|
21230
|
+
consume2(event);
|
|
20644
21231
|
} else if (event.ctrl && name.toLowerCase() === "s") {
|
|
20645
21232
|
submitFinish();
|
|
20646
|
-
|
|
21233
|
+
consume2(event);
|
|
20647
21234
|
}
|
|
20648
21235
|
},
|
|
20649
21236
|
keyBindings: [{ name: "enter", action: "submit" }],
|
|
@@ -20653,15 +21240,15 @@ Esc close this help`,
|
|
|
20653
21240
|
session.invalidate();
|
|
20654
21241
|
}
|
|
20655
21242
|
}),
|
|
20656
|
-
/* @__PURE__ */
|
|
21243
|
+
/* @__PURE__ */ jsx6("text", {
|
|
20657
21244
|
content: finishDialog.getValidationMessage(),
|
|
20658
21245
|
wrapMode: "none",
|
|
20659
21246
|
truncate: true
|
|
20660
21247
|
}),
|
|
20661
|
-
/* @__PURE__ */
|
|
21248
|
+
/* @__PURE__ */ jsxs5("box", {
|
|
20662
21249
|
style: { flexDirection: "row", height: 1 },
|
|
20663
21250
|
children: [
|
|
20664
|
-
/* @__PURE__ */
|
|
21251
|
+
/* @__PURE__ */ jsx6("box", {
|
|
20665
21252
|
id: "review-finish-comment",
|
|
20666
21253
|
onMouseUp: () => {
|
|
20667
21254
|
if (resizingSidebarRef.current || resizeReleaseSuppressionRef.current)
|
|
@@ -20669,11 +21256,11 @@ Esc close this help`,
|
|
|
20669
21256
|
finishDialog.setDecision("comment");
|
|
20670
21257
|
session.invalidate();
|
|
20671
21258
|
},
|
|
20672
|
-
children: /* @__PURE__ */
|
|
21259
|
+
children: /* @__PURE__ */ jsx6("text", {
|
|
20673
21260
|
content: finishDialog.getDecision() === "comment" ? "[Comment]" : " Comment "
|
|
20674
21261
|
})
|
|
20675
21262
|
}),
|
|
20676
|
-
/* @__PURE__ */
|
|
21263
|
+
/* @__PURE__ */ jsx6("box", {
|
|
20677
21264
|
id: "review-finish-approve",
|
|
20678
21265
|
onMouseUp: () => {
|
|
20679
21266
|
if (resizingSidebarRef.current || resizeReleaseSuppressionRef.current)
|
|
@@ -20681,11 +21268,11 @@ Esc close this help`,
|
|
|
20681
21268
|
finishDialog.setDecision("approve");
|
|
20682
21269
|
session.invalidate();
|
|
20683
21270
|
},
|
|
20684
|
-
children: /* @__PURE__ */
|
|
21271
|
+
children: /* @__PURE__ */ jsx6("text", {
|
|
20685
21272
|
content: finishDialog.getDecision() === "approve" ? "[Approve]" : " Approve "
|
|
20686
21273
|
})
|
|
20687
21274
|
}),
|
|
20688
|
-
/* @__PURE__ */
|
|
21275
|
+
/* @__PURE__ */ jsx6("box", {
|
|
20689
21276
|
id: "review-finish-request-changes",
|
|
20690
21277
|
onMouseUp: () => {
|
|
20691
21278
|
if (resizingSidebarRef.current || resizeReleaseSuppressionRef.current)
|
|
@@ -20693,39 +21280,40 @@ Esc close this help`,
|
|
|
20693
21280
|
finishDialog.setDecision("request-changes");
|
|
20694
21281
|
session.invalidate();
|
|
20695
21282
|
},
|
|
20696
|
-
children: /* @__PURE__ */
|
|
21283
|
+
children: /* @__PURE__ */ jsx6("text", {
|
|
20697
21284
|
content: finishDialog.getDecision() === "request-changes" ? "[Request Changes]" : " Request Changes "
|
|
20698
21285
|
})
|
|
20699
21286
|
})
|
|
20700
21287
|
]
|
|
20701
21288
|
}),
|
|
20702
|
-
/* @__PURE__ */
|
|
21289
|
+
/* @__PURE__ */ jsx6("box", {
|
|
20703
21290
|
id: "review-finish-submit",
|
|
20704
21291
|
onMouseUp: () => {
|
|
20705
21292
|
if (resizingSidebarRef.current || resizeReleaseSuppressionRef.current)
|
|
20706
21293
|
return;
|
|
20707
21294
|
submitFinish();
|
|
20708
21295
|
},
|
|
20709
|
-
children: /* @__PURE__ */
|
|
21296
|
+
children: /* @__PURE__ */ jsx6("text", {
|
|
20710
21297
|
content: " Submit "
|
|
20711
21298
|
})
|
|
20712
21299
|
}),
|
|
20713
|
-
/* @__PURE__ */
|
|
21300
|
+
/* @__PURE__ */ jsx6("text", {
|
|
20714
21301
|
content: "Ctrl-1 comment · Ctrl-2 approve · Ctrl-3 request changes · Enter/Ctrl-S submit · Esc cancel",
|
|
20715
21302
|
wrapMode: "none",
|
|
20716
21303
|
truncate: true
|
|
20717
21304
|
})
|
|
20718
21305
|
]
|
|
20719
21306
|
}) : null,
|
|
20720
|
-
/* @__PURE__ */
|
|
21307
|
+
/* @__PURE__ */ jsx6("box", {
|
|
20721
21308
|
id: "react-review-footer",
|
|
20722
21309
|
style: { width: "100%", height: 1, flexShrink: 0 },
|
|
20723
|
-
children: /* @__PURE__ */
|
|
20724
|
-
content: reviewFooter(state, layout, focus),
|
|
21310
|
+
children: /* @__PURE__ */ jsx6("text", {
|
|
21311
|
+
content: reviewFooter(state, layout, focus, projectionNotice),
|
|
20725
21312
|
wrapMode: "none",
|
|
20726
21313
|
truncate: true
|
|
20727
21314
|
})
|
|
20728
|
-
})
|
|
21315
|
+
}),
|
|
21316
|
+
basePicker
|
|
20729
21317
|
]
|
|
20730
21318
|
});
|
|
20731
21319
|
}
|
|
@@ -21051,7 +21639,7 @@ var persistedReviewStateSchema = z.object({
|
|
|
21051
21639
|
lastSubmission: submittedReviewRefSchema.nullable(),
|
|
21052
21640
|
submissionInProgress: submissionInProgressSchema.nullable().optional()
|
|
21053
21641
|
}).strict();
|
|
21054
|
-
var baseByHeadSchema = z.record(z.string(), z.object({ baseRef: z.string().min(1) }).strict()).superRefine((val, ctx) => {
|
|
21642
|
+
var baseByHeadSchema = z.record(z.string(), z.object({ baseRef: z.string().min(1), confirmed: z.boolean().optional() }).strict()).superRefine((val, ctx) => {
|
|
21055
21643
|
for (const key of Object.keys(val)) {
|
|
21056
21644
|
if (!isValidBaseByHeadKey(key)) {
|
|
21057
21645
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: `invalid baseByHead key: ${key}`, path: [key] });
|
|
@@ -21205,8 +21793,9 @@ function toDatabase(raw) {
|
|
|
21205
21793
|
for (const [k, v] of Object.entries(raw.reviews))
|
|
21206
21794
|
reviews[k] = toPersistedReviewState(v);
|
|
21207
21795
|
const baseByHead = {};
|
|
21208
|
-
for (const [k, v] of Object.entries(raw.baseByHead))
|
|
21209
|
-
baseByHead[k] = { baseRef: v.baseRef };
|
|
21796
|
+
for (const [k, v] of Object.entries(raw.baseByHead)) {
|
|
21797
|
+
baseByHead[k] = { baseRef: v.baseRef, ...v.confirmed === undefined ? {} : { confirmed: v.confirmed } };
|
|
21798
|
+
}
|
|
21210
21799
|
return { version: 2, baseByHead, reviews };
|
|
21211
21800
|
}
|
|
21212
21801
|
function toIdentity(raw) {
|
|
@@ -21572,7 +22161,7 @@ class ReactReviewSession {
|
|
|
21572
22161
|
}
|
|
21573
22162
|
|
|
21574
22163
|
// src/ui/review-workspace/react-review-host.tsx
|
|
21575
|
-
import { jsx as
|
|
22164
|
+
import { jsx as jsx7 } from "@opentui/react/jsx-runtime";
|
|
21576
22165
|
var rootsByRenderer = new WeakMap;
|
|
21577
22166
|
|
|
21578
22167
|
class ReactReviewHost {
|
|
@@ -21595,7 +22184,7 @@ class ReactReviewHost {
|
|
|
21595
22184
|
if (!existing.mounted) {
|
|
21596
22185
|
existing.mounted = true;
|
|
21597
22186
|
flushSync(() => {
|
|
21598
|
-
this.reactRoot.render(/* @__PURE__ */
|
|
22187
|
+
this.reactRoot.render(/* @__PURE__ */ jsx7(ReviewWorkspaceApp, {
|
|
21599
22188
|
session: this.session
|
|
21600
22189
|
}));
|
|
21601
22190
|
});
|
|
@@ -21607,7 +22196,7 @@ class ReactReviewHost {
|
|
|
21607
22196
|
this.reactRoot = createRoot(renderer);
|
|
21608
22197
|
rootsByRenderer.set(renderer, { root: this.reactRoot, session: this.session, mounted: true });
|
|
21609
22198
|
flushSync(() => {
|
|
21610
|
-
this.reactRoot.render(/* @__PURE__ */
|
|
22199
|
+
this.reactRoot.render(/* @__PURE__ */ jsx7(ReviewWorkspaceApp, {
|
|
21611
22200
|
session: this.session
|
|
21612
22201
|
}));
|
|
21613
22202
|
});
|
|
@@ -21924,7 +22513,7 @@ class AppScreenController {
|
|
|
21924
22513
|
}
|
|
21925
22514
|
|
|
21926
22515
|
// src/ui/review-workspace/controller.ts
|
|
21927
|
-
import { createHash as
|
|
22516
|
+
import { createHash as createHash6 } from "node:crypto";
|
|
21928
22517
|
|
|
21929
22518
|
// src/review/core/state.ts
|
|
21930
22519
|
function createInitialReviewState(document) {
|
|
@@ -22188,6 +22777,24 @@ function reduceReviewState(state, action) {
|
|
|
22188
22777
|
revision: state.revision + 1
|
|
22189
22778
|
};
|
|
22190
22779
|
}
|
|
22780
|
+
case "projection/apply": {
|
|
22781
|
+
const firstFile = action.document.files[0] ?? null;
|
|
22782
|
+
return {
|
|
22783
|
+
...state,
|
|
22784
|
+
document: action.document,
|
|
22785
|
+
projection: action.projection,
|
|
22786
|
+
selection: { fileKey: firstFile?.key ?? null, hunkIndex: 0 },
|
|
22787
|
+
lineSelection: null,
|
|
22788
|
+
expandedGaps: [],
|
|
22789
|
+
reveal: {
|
|
22790
|
+
fileTopToken: state.reveal.fileTopToken + 1,
|
|
22791
|
+
fileTopRequestToken: state.reveal.fileTopRequestToken + 1,
|
|
22792
|
+
hunkToken: state.reveal.hunkToken + 1,
|
|
22793
|
+
scrollToFeedback: false
|
|
22794
|
+
},
|
|
22795
|
+
revision: state.revision + 1
|
|
22796
|
+
};
|
|
22797
|
+
}
|
|
22191
22798
|
case "projection/set": {
|
|
22192
22799
|
if (projectionsEqual(state.projection, action.projection))
|
|
22193
22800
|
return state;
|
|
@@ -22767,65 +23374,97 @@ async function currentBranchRef(runner) {
|
|
|
22767
23374
|
const ref = (await output(runner, ["symbolic-ref", "--quiet", "HEAD"]))?.trim();
|
|
22768
23375
|
return ref?.startsWith("refs/heads/") ? ref : undefined;
|
|
22769
23376
|
}
|
|
22770
|
-
async function
|
|
22771
|
-
const
|
|
22772
|
-
|
|
22773
|
-
|
|
22774
|
-
|
|
22775
|
-
|
|
22776
|
-
|
|
22777
|
-
|
|
22778
|
-
|
|
22779
|
-
|
|
22780
|
-
}
|
|
22781
|
-
|
|
22782
|
-
const
|
|
22783
|
-
const
|
|
22784
|
-
const
|
|
22785
|
-
const
|
|
22786
|
-
|
|
22787
|
-
|
|
22788
|
-
|
|
22789
|
-
|
|
22790
|
-
|
|
22791
|
-
|
|
22792
|
-
|
|
22793
|
-
|
|
22794
|
-
|
|
23377
|
+
async function inferReviewBase(runner, preferredRef) {
|
|
23378
|
+
const [refResult, branchResult, headResult, remoteResult] = await Promise.all([
|
|
23379
|
+
runner.run([
|
|
23380
|
+
"for-each-ref",
|
|
23381
|
+
"--format=%(refname)%09%(objectname)%09%(objecttype)%09%(symref)%09%(upstream:remotename)",
|
|
23382
|
+
"refs/heads",
|
|
23383
|
+
"refs/remotes"
|
|
23384
|
+
], { readOnly: true }),
|
|
23385
|
+
runner.run(["symbolic-ref", "--quiet", "HEAD"], { readOnly: true, acceptedExitCodes: [0, 1] }),
|
|
23386
|
+
runner.run(["rev-parse", "--verify", "--quiet", "HEAD^{commit}"], { readOnly: true, acceptedExitCodes: [0, 1] }),
|
|
23387
|
+
runner.run(["remote"], { readOnly: true })
|
|
23388
|
+
]);
|
|
23389
|
+
const branchRef = branchResult.exitCode === 0 ? branchResult.stdout.trim() : undefined;
|
|
23390
|
+
const headOid = headResult.exitCode === 0 ? headResult.stdout.trim() : undefined;
|
|
23391
|
+
const branchName = branchRef?.startsWith("refs/heads/") ? branchRef.slice("refs/heads/".length) : undefined;
|
|
23392
|
+
const refs = [];
|
|
23393
|
+
for (const line of refResult.stdout.split(/\r?\n/)) {
|
|
23394
|
+
const [ref, oid, objectType, symbolic = "", upstreamRemote2 = ""] = line.split("\t");
|
|
23395
|
+
if (ref === undefined || oid === undefined || objectType !== "commit")
|
|
23396
|
+
continue;
|
|
23397
|
+
refs.push({ ref, oid, symbolic, upstreamRemote: upstreamRemote2 });
|
|
23398
|
+
}
|
|
23399
|
+
const upstreamRemote = refs.find(({ ref }) => ref === branchRef)?.upstreamRemote;
|
|
23400
|
+
const remoteNames = remoteResult.stdout.split(/\r?\n/).filter(Boolean).sort((left2, right2) => right2.length - left2.length || (left2 < right2 ? -1 : left2 > right2 ? 1 : 0));
|
|
23401
|
+
const remoteOrder = (remote) => remote === upstreamRemote ? 0 : remote === "origin" ? 1 : 2;
|
|
23402
|
+
const defaults = refs.filter(({ ref, symbolic }) => ref.startsWith("refs/remotes/") && ref.endsWith("/HEAD") && symbolic !== "").sort((left2, right2) => {
|
|
23403
|
+
const leftRemote = left2.ref.slice("refs/remotes/".length, -"/HEAD".length);
|
|
23404
|
+
const rightRemote = right2.ref.slice("refs/remotes/".length, -"/HEAD".length);
|
|
23405
|
+
return remoteOrder(leftRemote) - remoteOrder(rightRemote) || (left2.ref < right2.ref ? -1 : left2.ref > right2.ref ? 1 : 0);
|
|
22795
23406
|
});
|
|
22796
|
-
|
|
22797
|
-
}
|
|
22798
|
-
|
|
22799
|
-
|
|
22800
|
-
|
|
22801
|
-
|
|
22802
|
-
|
|
22803
|
-
|
|
22804
|
-
|
|
22805
|
-
|
|
22806
|
-
|
|
22807
|
-
|
|
22808
|
-
const
|
|
22809
|
-
|
|
22810
|
-
|
|
22811
|
-
const
|
|
22812
|
-
|
|
22813
|
-
|
|
22814
|
-
|
|
22815
|
-
|
|
22816
|
-
|
|
22817
|
-
const
|
|
22818
|
-
|
|
22819
|
-
|
|
22820
|
-
|
|
22821
|
-
|
|
22822
|
-
|
|
22823
|
-
|
|
22824
|
-
|
|
22825
|
-
|
|
23407
|
+
const defaultRanks = new Map(defaults.map(({ symbolic }, index) => [symbolic, index]));
|
|
23408
|
+
const defaultRemotes = new Map(defaults.map(({ ref, symbolic }) => [symbolic, ref.slice("refs/remotes/".length, -"/HEAD".length)]));
|
|
23409
|
+
const branches = refs.filter(({ ref, symbolic }) => ref !== branchRef && symbolic === "" && !(ref.startsWith("refs/remotes/") && ref.endsWith("/HEAD")));
|
|
23410
|
+
const firstParentDistances = new Map;
|
|
23411
|
+
if (headOid !== undefined && branches.length > 0) {
|
|
23412
|
+
const history = await runner.run(["rev-list", "--first-parent", "--max-count=2049", headOid, "--"], { readOnly: true });
|
|
23413
|
+
let distance = 0;
|
|
23414
|
+
for (const oid of history.stdout.trim().split(/\r?\n/)) {
|
|
23415
|
+
if (oid !== "")
|
|
23416
|
+
firstParentDistances.set(oid, distance++);
|
|
23417
|
+
}
|
|
23418
|
+
}
|
|
23419
|
+
const ranked = branches.map(({ ref, oid }) => {
|
|
23420
|
+
const local = ref.startsWith("refs/heads/");
|
|
23421
|
+
const label = ref.slice(local ? "refs/heads/".length : "refs/remotes/".length);
|
|
23422
|
+
const remote = local ? undefined : remoteNames.find((name) => label.startsWith(`${name}/`));
|
|
23423
|
+
const shortName = local ? label : remote === undefined ? undefined : label.slice(remote.length + 1);
|
|
23424
|
+
const trackingHead = !local && branchName !== undefined && shortName === branchName;
|
|
23425
|
+
const sameHead = headOid !== undefined && oid === headOid;
|
|
23426
|
+
const distance = firstParentDistances.get(oid);
|
|
23427
|
+
const defaultRank = defaultRanks.get(ref);
|
|
23428
|
+
const defaultRemote = defaultRemotes.get(ref);
|
|
23429
|
+
const conventionalRank = shortName === "main" ? 0 : shortName === "master" ? 1 : shortName === "develop" ? 2 : 3;
|
|
23430
|
+
const defaultReason = defaultRemote === undefined ? undefined : `Default branch of ${defaultRemote === upstreamRemote ? "upstream remote " : "remote "}${defaultRemote}`;
|
|
23431
|
+
let priority;
|
|
23432
|
+
let reason;
|
|
23433
|
+
if (ref === preferredRef) {
|
|
23434
|
+
priority = 0;
|
|
23435
|
+
reason = "Previously selected review base";
|
|
23436
|
+
} else if (trackingHead || sameHead) {
|
|
23437
|
+
priority = 5;
|
|
23438
|
+
reason = trackingHead ? "Same-name remote branch for current HEAD" : "Points at current HEAD";
|
|
23439
|
+
} else if (distance !== undefined && distance > 0) {
|
|
23440
|
+
priority = 1;
|
|
23441
|
+
reason = `Branch tip ${distance} first-parent ${distance === 1 ? "commit" : "commits"} behind HEAD`;
|
|
23442
|
+
if (defaultReason !== undefined)
|
|
23443
|
+
reason += `; ${defaultReason}`;
|
|
23444
|
+
} else if (defaultRank !== undefined) {
|
|
23445
|
+
priority = 2;
|
|
23446
|
+
reason = defaultReason;
|
|
23447
|
+
} else if (conventionalRank < 3) {
|
|
23448
|
+
priority = 3;
|
|
23449
|
+
reason = `Conventional ${shortName} base branch`;
|
|
23450
|
+
} else {
|
|
23451
|
+
priority = 4;
|
|
23452
|
+
reason = local ? "Local branch" : "Remote branch";
|
|
22826
23453
|
}
|
|
22827
|
-
|
|
22828
|
-
|
|
23454
|
+
return {
|
|
23455
|
+
candidate: { ref, label, reason },
|
|
23456
|
+
priority,
|
|
23457
|
+
distance: priority === 1 ? distance : 0,
|
|
23458
|
+
defaultRank: priority === 1 || priority === 2 ? defaultRank ?? defaults.length : 0,
|
|
23459
|
+
conventionalRank: priority === 1 || priority === 3 ? conventionalRank : 0
|
|
23460
|
+
};
|
|
23461
|
+
});
|
|
23462
|
+
ranked.sort((left2, right2) => left2.priority - right2.priority || left2.distance - right2.distance || left2.defaultRank - right2.defaultRank || left2.conventionalRank - right2.conventionalRank || (left2.candidate.ref < right2.candidate.ref ? -1 : left2.candidate.ref > right2.candidate.ref ? 1 : 0));
|
|
23463
|
+
return {
|
|
23464
|
+
kind: "choose",
|
|
23465
|
+
candidates: ranked.map(({ candidate }) => candidate),
|
|
23466
|
+
reason: headOid === undefined ? "HEAD has no commit" : branchRef === undefined ? "HEAD is detached; choose a review base" : "Choose a review base; likely branches are listed first"
|
|
23467
|
+
};
|
|
22829
23468
|
}
|
|
22830
23469
|
|
|
22831
23470
|
// src/review/git/load-review-document.ts
|
|
@@ -23012,6 +23651,187 @@ async function loadReviewDocument(runner, baseRef) {
|
|
|
23012
23651
|
return createReviewDocument({ identity, generation, commits: commitsForDoc, files });
|
|
23013
23652
|
}
|
|
23014
23653
|
|
|
23654
|
+
// src/review/git/load-review-projection.ts
|
|
23655
|
+
function normalizePathForJoin2(p) {
|
|
23656
|
+
return p.replace(/[\r\n]+$/u, "");
|
|
23657
|
+
}
|
|
23658
|
+
function kindFromRaw2(rawStatus, isBinary) {
|
|
23659
|
+
if (isBinary)
|
|
23660
|
+
return "binary";
|
|
23661
|
+
const letter = rawStatus[0] ?? "";
|
|
23662
|
+
if (letter === "R")
|
|
23663
|
+
return "renamed";
|
|
23664
|
+
if (letter === "C")
|
|
23665
|
+
return "copied";
|
|
23666
|
+
if (letter === "A")
|
|
23667
|
+
return "added";
|
|
23668
|
+
if (letter === "D")
|
|
23669
|
+
return "deleted";
|
|
23670
|
+
return "modified";
|
|
23671
|
+
}
|
|
23672
|
+
function sourceFromKind2(kind, isBinary) {
|
|
23673
|
+
if (isBinary || kind === "binary")
|
|
23674
|
+
return "binary";
|
|
23675
|
+
return "available";
|
|
23676
|
+
}
|
|
23677
|
+
async function buildFilesForRange(runner, rangeArgs) {
|
|
23678
|
+
const patchResult = await runner.run(["diff", "--no-ext-diff", "--no-color", "--find-renames", "--binary", "--src-prefix=a/", "--dst-prefix=b/", ...rangeArgs, "--"], { readOnly: true });
|
|
23679
|
+
const rawResult = await runner.run(["diff", "--no-ext-diff", "--no-color", "--find-renames", "--raw", "-z", ...rangeArgs, "--"], { readOnly: true });
|
|
23680
|
+
const numstatResult = await runner.run(["diff", "--no-ext-diff", "--no-color", "--find-renames", "--numstat", "-z", ...rangeArgs, "--"], { readOnly: true });
|
|
23681
|
+
const patchText = patchResult.stdout;
|
|
23682
|
+
const rawText = rawResult.stdout;
|
|
23683
|
+
const numstatText = numstatResult.stdout;
|
|
23684
|
+
const parsedPatches = parseReviewPatch(patchText);
|
|
23685
|
+
const rawEntries = parseRawDiffZ(rawText);
|
|
23686
|
+
const numstatEntries = parseNumstatZ(numstatText);
|
|
23687
|
+
const rawByKey = new Map;
|
|
23688
|
+
for (const entry of rawEntries) {
|
|
23689
|
+
const key = `${normalizePathForJoin2(entry.path)}|${entry.previousPath ? normalizePathForJoin2(entry.previousPath) : ""}`;
|
|
23690
|
+
const list = rawByKey.get(key);
|
|
23691
|
+
if (list)
|
|
23692
|
+
list.push(entry);
|
|
23693
|
+
else
|
|
23694
|
+
rawByKey.set(key, [entry]);
|
|
23695
|
+
}
|
|
23696
|
+
const numstatByKey = new Map;
|
|
23697
|
+
for (const entry of numstatEntries) {
|
|
23698
|
+
const key = `${normalizePathForJoin2(entry.path)}|${entry.previousPath ? normalizePathForJoin2(entry.previousPath) : ""}`;
|
|
23699
|
+
const list = numstatByKey.get(key);
|
|
23700
|
+
if (list)
|
|
23701
|
+
list.push(entry);
|
|
23702
|
+
else
|
|
23703
|
+
numstatByKey.set(key, [entry]);
|
|
23704
|
+
}
|
|
23705
|
+
const seenPatchKeys = new Set;
|
|
23706
|
+
for (const pf of parsedPatches) {
|
|
23707
|
+
const key = `${normalizePathForJoin2(pf.path)}|${pf.previousPath ? normalizePathForJoin2(pf.previousPath) : ""}`;
|
|
23708
|
+
if (seenPatchKeys.has(key))
|
|
23709
|
+
throw new Error(`ambiguous patch join: duplicate path ${key}`);
|
|
23710
|
+
seenPatchKeys.add(key);
|
|
23711
|
+
}
|
|
23712
|
+
if (parsedPatches.length === 0) {
|
|
23713
|
+
if (rawEntries.length !== 0)
|
|
23714
|
+
throw new Error(`missing patch for raw entries: ${rawEntries.map((r) => r.path).join(",")}`);
|
|
23715
|
+
if (numstatEntries.length !== 0)
|
|
23716
|
+
throw new Error(`missing patch for numstat entries: ${numstatEntries.map((r) => r.path).join(",")}`);
|
|
23717
|
+
return [];
|
|
23718
|
+
}
|
|
23719
|
+
const files = [];
|
|
23720
|
+
const matchedRawKeys = new Set;
|
|
23721
|
+
const matchedNumstatKeys = new Set;
|
|
23722
|
+
for (const pf of parsedPatches) {
|
|
23723
|
+
const key = `${normalizePathForJoin2(pf.path)}|${pf.previousPath ? normalizePathForJoin2(pf.previousPath) : ""}`;
|
|
23724
|
+
const rawList = rawByKey.get(key);
|
|
23725
|
+
if (!rawList || rawList.length === 0) {
|
|
23726
|
+
throw new Error(`missing raw entry for patch file ${pf.path}${pf.previousPath ? ` (from ${pf.previousPath})` : ""}`);
|
|
23727
|
+
}
|
|
23728
|
+
if (rawList.length > 1)
|
|
23729
|
+
throw new Error(`ambiguous raw join for ${pf.path}`);
|
|
23730
|
+
const raw = rawList[0];
|
|
23731
|
+
matchedRawKeys.add(key);
|
|
23732
|
+
const numstatList = numstatByKey.get(key);
|
|
23733
|
+
let numstat = numstatList?.[0];
|
|
23734
|
+
if (!numstat) {
|
|
23735
|
+
throw new Error(`missing numstat entry for patch file ${pf.path}`);
|
|
23736
|
+
}
|
|
23737
|
+
if (numstatList && numstatList.length > 1)
|
|
23738
|
+
throw new Error(`ambiguous numstat join for ${pf.path}`);
|
|
23739
|
+
matchedNumstatKeys.add(key);
|
|
23740
|
+
const patchPrevNorm = pf.previousPath ? normalizePathForJoin2(pf.previousPath) : undefined;
|
|
23741
|
+
const numstatPrevNorm = numstat.previousPath ? normalizePathForJoin2(numstat.previousPath) : undefined;
|
|
23742
|
+
if (patchPrevNorm !== numstatPrevNorm) {
|
|
23743
|
+
if (patchPrevNorm !== undefined || numstatPrevNorm !== undefined) {
|
|
23744
|
+
throw new Error(`mismatched previousPath for ${pf.path}: patch ${patchPrevNorm} vs numstat ${numstatPrevNorm}`);
|
|
23745
|
+
}
|
|
23746
|
+
}
|
|
23747
|
+
const kind = kindFromRaw2(raw.status, pf.isBinary);
|
|
23748
|
+
const source = sourceFromKind2(kind, pf.isBinary);
|
|
23749
|
+
const normalizedHunkBody = pf.normalizedHunkBody;
|
|
23750
|
+
const contentId = sha256Tuple2([raw.oldBlobOid ?? "", raw.newBlobOid ?? "", raw.oldMode ?? "", raw.newMode ?? "", normalizedHunkBody]);
|
|
23751
|
+
const patchDigest = pf.patchDigest;
|
|
23752
|
+
const stats = { additions: numstat.additions, deletions: numstat.deletions };
|
|
23753
|
+
const hunks = pf.isBinary ? [] : pf.hunks;
|
|
23754
|
+
const file = pf.previousPath ? {
|
|
23755
|
+
key: pf.path,
|
|
23756
|
+
path: pf.path,
|
|
23757
|
+
previousPath: pf.previousPath,
|
|
23758
|
+
kind,
|
|
23759
|
+
oldBlobOid: raw.oldBlobOid,
|
|
23760
|
+
newBlobOid: raw.newBlobOid,
|
|
23761
|
+
oldMode: raw.oldMode,
|
|
23762
|
+
newMode: raw.newMode,
|
|
23763
|
+
contentId,
|
|
23764
|
+
patchDigest,
|
|
23765
|
+
stats,
|
|
23766
|
+
hunks,
|
|
23767
|
+
source
|
|
23768
|
+
} : {
|
|
23769
|
+
key: pf.path,
|
|
23770
|
+
path: pf.path,
|
|
23771
|
+
kind,
|
|
23772
|
+
oldBlobOid: raw.oldBlobOid,
|
|
23773
|
+
newBlobOid: raw.newBlobOid,
|
|
23774
|
+
oldMode: raw.oldMode,
|
|
23775
|
+
newMode: raw.newMode,
|
|
23776
|
+
contentId,
|
|
23777
|
+
patchDigest,
|
|
23778
|
+
stats,
|
|
23779
|
+
hunks,
|
|
23780
|
+
source
|
|
23781
|
+
};
|
|
23782
|
+
files.push(file);
|
|
23783
|
+
}
|
|
23784
|
+
for (const [key, list] of rawByKey) {
|
|
23785
|
+
if (!matchedRawKeys.has(key))
|
|
23786
|
+
throw new Error(`missing patch for raw entries: ${key} (${list.length})`);
|
|
23787
|
+
}
|
|
23788
|
+
for (const [key] of numstatByKey) {
|
|
23789
|
+
if (!matchedNumstatKeys.has(key))
|
|
23790
|
+
throw new Error(`missing patch for numstat entries: ${key}`);
|
|
23791
|
+
}
|
|
23792
|
+
return files;
|
|
23793
|
+
}
|
|
23794
|
+
async function isAncestor(runner, ancestorOid, descendantOid) {
|
|
23795
|
+
if (!ancestorOid || !descendantOid)
|
|
23796
|
+
return false;
|
|
23797
|
+
if (ancestorOid === descendantOid)
|
|
23798
|
+
return true;
|
|
23799
|
+
try {
|
|
23800
|
+
const result = await runner.run(["merge-base", "--is-ancestor", ancestorOid, descendantOid], {
|
|
23801
|
+
readOnly: true,
|
|
23802
|
+
acceptedExitCodes: [0, 1]
|
|
23803
|
+
});
|
|
23804
|
+
return result.exitCode === 0;
|
|
23805
|
+
} catch {
|
|
23806
|
+
return false;
|
|
23807
|
+
}
|
|
23808
|
+
}
|
|
23809
|
+
async function loadSinceLastReviewProjection(runner, aggregateDocument, lastHeadOid) {
|
|
23810
|
+
if (!lastHeadOid || lastHeadOid.trim() === "") {
|
|
23811
|
+
throw new Error("lastHeadOid must be non-empty");
|
|
23812
|
+
}
|
|
23813
|
+
const headOid = aggregateDocument.generation.headOid;
|
|
23814
|
+
const ancestor = await isAncestor(runner, lastHeadOid, headOid);
|
|
23815
|
+
if (!ancestor) {
|
|
23816
|
+
return {
|
|
23817
|
+
kind: "history-rewritten",
|
|
23818
|
+
lastHeadOid,
|
|
23819
|
+
headOid,
|
|
23820
|
+
reason: "history rewritten: last submission head is not an ancestor of current HEAD"
|
|
23821
|
+
};
|
|
23822
|
+
}
|
|
23823
|
+
const range = `${lastHeadOid}..${headOid}`;
|
|
23824
|
+
const files = await buildFilesForRange(runner, [range]);
|
|
23825
|
+
const projection = { kind: "since-last-review", fromHeadOid: lastHeadOid };
|
|
23826
|
+
const doc = {
|
|
23827
|
+
reviewId: aggregateDocument.identity.id,
|
|
23828
|
+
generationId: aggregateDocument.generation.id,
|
|
23829
|
+
projection,
|
|
23830
|
+
files
|
|
23831
|
+
};
|
|
23832
|
+
return { kind: "ok", document: doc };
|
|
23833
|
+
}
|
|
23834
|
+
|
|
23015
23835
|
// src/review/git/load-source-context.ts
|
|
23016
23836
|
var DEFAULT_MAX_BYTES = 1e6;
|
|
23017
23837
|
function isZeroOid(oid) {
|
|
@@ -23354,12 +24174,12 @@ function persistedFromReviewState(state) {
|
|
|
23354
24174
|
}
|
|
23355
24175
|
|
|
23356
24176
|
// src/review/storage/review-artifact-store.ts
|
|
23357
|
-
import { createHash as
|
|
24177
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
23358
24178
|
function artifactRelativePath(reviewId, artifactId) {
|
|
23359
24179
|
return `githunk/reviews/${reviewId}/${artifactId}.json`;
|
|
23360
24180
|
}
|
|
23361
24181
|
function artifactDigest(text) {
|
|
23362
|
-
return
|
|
24182
|
+
return createHash5("sha256").update(text, "utf8").digest("hex");
|
|
23363
24183
|
}
|
|
23364
24184
|
function artifactText(artifact) {
|
|
23365
24185
|
return serializeReviewArtifactV1(artifact) + `
|
|
@@ -23598,6 +24418,7 @@ class ReviewWorkspaceController {
|
|
|
23598
24418
|
stateStore;
|
|
23599
24419
|
artifactStore;
|
|
23600
24420
|
loadDocumentImpl;
|
|
24421
|
+
loadSinceLastReviewImpl;
|
|
23601
24422
|
loadSourceContextImpl;
|
|
23602
24423
|
nowImpl;
|
|
23603
24424
|
randomIdImpl;
|
|
@@ -23610,14 +24431,18 @@ class ReviewWorkspaceController {
|
|
|
23610
24431
|
activeReviewId;
|
|
23611
24432
|
activeGenerationId;
|
|
23612
24433
|
baseRef;
|
|
24434
|
+
_baseSelection;
|
|
24435
|
+
baseSelectionRequestId = 0;
|
|
23613
24436
|
sourceContextCache = new Map;
|
|
23614
24437
|
pendingGapRequests = new Map;
|
|
23615
24438
|
gapRequestCounter = 0;
|
|
24439
|
+
aggregateDocument;
|
|
23616
24440
|
constructor(options) {
|
|
23617
24441
|
this.runner = options.runner;
|
|
23618
24442
|
this.stateStore = options.stateStore;
|
|
23619
24443
|
this.artifactStore = options.artifactStore;
|
|
23620
24444
|
this.loadDocumentImpl = options.loadDocument ?? ((baseRef) => loadReviewDocument(options.runner, baseRef));
|
|
24445
|
+
this.loadSinceLastReviewImpl = options.loadSinceLastReview ?? ((aggregate, fromHeadOid) => loadSinceLastReviewProjection(options.runner, aggregate, fromHeadOid));
|
|
23621
24446
|
this.loadSourceContextImpl = options.loadSourceContextImpl;
|
|
23622
24447
|
this.nowImpl = options.now ?? (() => new Date().toISOString());
|
|
23623
24448
|
this.randomIdImpl = options.randomId ?? (() => {
|
|
@@ -23643,6 +24468,78 @@ class ReviewWorkspaceController {
|
|
|
23643
24468
|
get base() {
|
|
23644
24469
|
return this.baseRef;
|
|
23645
24470
|
}
|
|
24471
|
+
get baseSelection() {
|
|
24472
|
+
return this._baseSelection;
|
|
24473
|
+
}
|
|
24474
|
+
async requestBaseSelection() {
|
|
24475
|
+
if (this.destroyed || this._baseSelection?.selecting)
|
|
24476
|
+
return;
|
|
24477
|
+
const token = ++this.baseSelectionRequestId;
|
|
24478
|
+
this.requestId++;
|
|
24479
|
+
this._baseSelection = { candidates: [], loading: true, selecting: false };
|
|
24480
|
+
this.publish();
|
|
24481
|
+
try {
|
|
24482
|
+
const remembered = this.baseRef === undefined ? await this.rememberedBase() : undefined;
|
|
24483
|
+
const inferred = await inferReviewBase(this.runner, this.baseRef ?? remembered?.baseRef);
|
|
24484
|
+
if (this.destroyed || token !== this.baseSelectionRequestId)
|
|
24485
|
+
return;
|
|
24486
|
+
this._baseSelection = { candidates: inferred.candidates, loading: false, selecting: false };
|
|
24487
|
+
} catch (err) {
|
|
24488
|
+
if (this.destroyed || token !== this.baseSelectionRequestId)
|
|
24489
|
+
return;
|
|
24490
|
+
this._baseSelection = {
|
|
24491
|
+
candidates: [],
|
|
24492
|
+
loading: false,
|
|
24493
|
+
selecting: false,
|
|
24494
|
+
error: err instanceof Error ? err.message : String(err)
|
|
24495
|
+
};
|
|
24496
|
+
}
|
|
24497
|
+
this.publish();
|
|
24498
|
+
}
|
|
24499
|
+
cancelBaseSelection() {
|
|
24500
|
+
if (this._baseSelection?.selecting)
|
|
24501
|
+
return;
|
|
24502
|
+
this.baseSelectionRequestId++;
|
|
24503
|
+
this._baseSelection = undefined;
|
|
24504
|
+
this.publish();
|
|
24505
|
+
}
|
|
24506
|
+
async chooseBase(ref) {
|
|
24507
|
+
const picker = this._baseSelection;
|
|
24508
|
+
if (this.destroyed || !picker || picker.loading || picker.selecting || !picker.candidates.some((candidate) => candidate.ref === ref))
|
|
24509
|
+
return false;
|
|
24510
|
+
const token = ++this.baseSelectionRequestId;
|
|
24511
|
+
this.requestId++;
|
|
24512
|
+
this._baseSelection = { candidates: picker.candidates, loading: false, selecting: true };
|
|
24513
|
+
this.publish();
|
|
24514
|
+
return this.reviewOperationQueue.run(async () => {
|
|
24515
|
+
if (this.destroyed || token !== this.baseSelectionRequestId)
|
|
24516
|
+
return false;
|
|
24517
|
+
try {
|
|
24518
|
+
if (this._state !== undefined)
|
|
24519
|
+
await this.persistState();
|
|
24520
|
+
await this.stateStore?.flush();
|
|
24521
|
+
if (this.destroyed || token !== this.baseSelectionRequestId)
|
|
24522
|
+
return false;
|
|
24523
|
+
await this.open(ref);
|
|
24524
|
+
if (this.destroyed || token !== this.baseSelectionRequestId)
|
|
24525
|
+
return false;
|
|
24526
|
+
this._baseSelection = undefined;
|
|
24527
|
+
this.publish();
|
|
24528
|
+
return true;
|
|
24529
|
+
} catch (err) {
|
|
24530
|
+
if (this.destroyed || token !== this.baseSelectionRequestId)
|
|
24531
|
+
return false;
|
|
24532
|
+
this._baseSelection = {
|
|
24533
|
+
candidates: picker.candidates,
|
|
24534
|
+
loading: false,
|
|
24535
|
+
selecting: false,
|
|
24536
|
+
error: err instanceof Error ? err.message : String(err)
|
|
24537
|
+
};
|
|
24538
|
+
this.publish();
|
|
24539
|
+
return false;
|
|
24540
|
+
}
|
|
24541
|
+
});
|
|
24542
|
+
}
|
|
23646
24543
|
clearError() {
|
|
23647
24544
|
if (this._error === undefined)
|
|
23648
24545
|
return;
|
|
@@ -23651,7 +24548,7 @@ class ReviewWorkspaceController {
|
|
|
23651
24548
|
}
|
|
23652
24549
|
get refreshGeneration() {
|
|
23653
24550
|
return async () => {
|
|
23654
|
-
if (this.baseRef === undefined || this.destroyed)
|
|
24551
|
+
if (this.baseRef === undefined || this.destroyed || this._baseSelection !== undefined)
|
|
23655
24552
|
return;
|
|
23656
24553
|
const token = ++this.requestId;
|
|
23657
24554
|
const capturedGeneration = this.activeGenerationId;
|
|
@@ -23695,10 +24592,12 @@ class ReviewWorkspaceController {
|
|
|
23695
24592
|
this.sourceContextCache.clear();
|
|
23696
24593
|
this.pendingGapRequests.clear();
|
|
23697
24594
|
const currentState = this._state;
|
|
23698
|
-
const
|
|
24595
|
+
const reconcileFrom = currentState !== undefined && currentState.projection.kind !== "aggregate" ? { ...currentState, projection: { kind: "aggregate" } } : currentState;
|
|
24596
|
+
const nextState = reconcileFrom === undefined ? createInitialReviewState(doc) : reconcileReviewState(reconcileFrom, doc);
|
|
23699
24597
|
if (!ownsRequest())
|
|
23700
24598
|
return;
|
|
23701
24599
|
this._state = nextState;
|
|
24600
|
+
this.aggregateDocument = undefined;
|
|
23702
24601
|
this.activeReviewId = doc.identity.id;
|
|
23703
24602
|
this.activeGenerationId = doc.generation.id;
|
|
23704
24603
|
this._error = undefined;
|
|
@@ -23719,10 +24618,21 @@ class ReviewWorkspaceController {
|
|
|
23719
24618
|
throw new Error("controller destroyed");
|
|
23720
24619
|
const token = ++this.requestId;
|
|
23721
24620
|
let resolvedBase = baseRef;
|
|
23722
|
-
|
|
24621
|
+
if (resolvedBase !== undefined && this._baseSelection !== undefined && !this._baseSelection.selecting) {
|
|
24622
|
+
this.baseSelectionRequestId++;
|
|
24623
|
+
this._baseSelection = undefined;
|
|
24624
|
+
this.publish();
|
|
24625
|
+
}
|
|
24626
|
+
let corruptError = this._error?.kind === "corrupt-state" || this._error?.kind === "storage" ? this._error : undefined;
|
|
23723
24627
|
if (resolvedBase === undefined) {
|
|
23724
24628
|
try {
|
|
23725
|
-
|
|
24629
|
+
const remembered = await this.rememberedBase();
|
|
24630
|
+
if (remembered?.confirmed === true && await resolveRefOid(this.runner, remembered.baseRef) !== undefined) {
|
|
24631
|
+
resolvedBase = remembered.baseRef;
|
|
24632
|
+
} else {
|
|
24633
|
+
await this.requestBaseSelection();
|
|
24634
|
+
return;
|
|
24635
|
+
}
|
|
23726
24636
|
const warning = this.stateStore?.quarantineWarning;
|
|
23727
24637
|
if (warning) {
|
|
23728
24638
|
const pathMatch = warning.match(/moved to (\S+)/);
|
|
@@ -23738,6 +24648,8 @@ class ReviewWorkspaceController {
|
|
|
23738
24648
|
}
|
|
23739
24649
|
if (this.destroyed || token !== this.requestId)
|
|
23740
24650
|
throw new Error("open cancelled");
|
|
24651
|
+
if (resolvedBase === undefined)
|
|
24652
|
+
throw new Error("base selection required");
|
|
23741
24653
|
let doc;
|
|
23742
24654
|
try {
|
|
23743
24655
|
doc = await this.loadDocumentImpl(resolvedBase);
|
|
@@ -23762,7 +24674,20 @@ class ReviewWorkspaceController {
|
|
|
23762
24674
|
const qPath = pathMatch?.[1] ?? warning;
|
|
23763
24675
|
corruptError = createCorruptStateError(qPath, warning);
|
|
23764
24676
|
}
|
|
23765
|
-
|
|
24677
|
+
let persisted = db.reviews[doc.identity.id];
|
|
24678
|
+
if (persisted === undefined) {
|
|
24679
|
+
const headKey = doc.identity.headRef ?? `detached:${doc.identity.detachedHeadOid}`;
|
|
24680
|
+
const previousBase = db.baseByHead[headKey]?.baseRef;
|
|
24681
|
+
if (previousBase && previousBase !== resolvedBase && !previousBase.startsWith("refs/")) {
|
|
24682
|
+
const previousId = sha256Tuple2(["branch-review-v2", headKey, previousBase]);
|
|
24683
|
+
const previous = db.reviews[previousId];
|
|
24684
|
+
if (previous !== undefined) {
|
|
24685
|
+
const canonical = await this.runner.run(["rev-parse", "--symbolic-full-name", "--verify", "--end-of-options", previousBase], { readOnly: true, acceptedExitCodes: [0, 1, 128] });
|
|
24686
|
+
if (canonical.stdout.trim() === resolvedBase)
|
|
24687
|
+
persisted = previous;
|
|
24688
|
+
}
|
|
24689
|
+
}
|
|
24690
|
+
}
|
|
23766
24691
|
if (persisted !== undefined) {
|
|
23767
24692
|
const initial = createInitialReviewState(doc);
|
|
23768
24693
|
const reconstructed = {
|
|
@@ -23807,7 +24732,7 @@ class ReviewWorkspaceController {
|
|
|
23807
24732
|
}
|
|
23808
24733
|
dispatch(action) {
|
|
23809
24734
|
const current = this._state;
|
|
23810
|
-
if (current === undefined)
|
|
24735
|
+
if (current === undefined || this._baseSelection !== undefined)
|
|
23811
24736
|
return;
|
|
23812
24737
|
const next = reduceReviewState(current, action);
|
|
23813
24738
|
if (next === current)
|
|
@@ -23825,7 +24750,7 @@ class ReviewWorkspaceController {
|
|
|
23825
24750
|
}
|
|
23826
24751
|
dispatchIntent(intent) {
|
|
23827
24752
|
const current = this._state;
|
|
23828
|
-
if (current === undefined)
|
|
24753
|
+
if (current === undefined || this._baseSelection !== undefined)
|
|
23829
24754
|
return false;
|
|
23830
24755
|
try {
|
|
23831
24756
|
const action = planReviewIntent(current, intent);
|
|
@@ -23835,6 +24760,50 @@ class ReviewWorkspaceController {
|
|
|
23835
24760
|
return false;
|
|
23836
24761
|
}
|
|
23837
24762
|
}
|
|
24763
|
+
async enterSinceLastReview() {
|
|
24764
|
+
const current = this._state;
|
|
24765
|
+
if (current === undefined || this._baseSelection !== undefined)
|
|
24766
|
+
return { ok: false, reason: "unavailable" };
|
|
24767
|
+
if (current.projection.kind !== "aggregate")
|
|
24768
|
+
return { ok: false, reason: "already-projected" };
|
|
24769
|
+
const fromHeadOid = current.lastSubmission?.headOid;
|
|
24770
|
+
if (fromHeadOid === undefined || fromHeadOid.trim() === "")
|
|
24771
|
+
return { ok: false, reason: "no-previous-review" };
|
|
24772
|
+
const aggregate = current.document;
|
|
24773
|
+
let result;
|
|
24774
|
+
try {
|
|
24775
|
+
result = await this.loadSinceLastReviewImpl(aggregate, fromHeadOid);
|
|
24776
|
+
} catch (err) {
|
|
24777
|
+
return { ok: false, reason: "load-failed", message: err instanceof Error ? err.message : String(err) };
|
|
24778
|
+
}
|
|
24779
|
+
if (result.kind === "history-rewritten")
|
|
24780
|
+
return { ok: false, reason: "history-rewritten", message: result.reason };
|
|
24781
|
+
const latest = this._state;
|
|
24782
|
+
if (latest === undefined || latest.document !== aggregate || latest.projection.kind !== "aggregate") {
|
|
24783
|
+
return { ok: false, reason: "stale" };
|
|
24784
|
+
}
|
|
24785
|
+
this.aggregateDocument = aggregate;
|
|
24786
|
+
this.dispatch({
|
|
24787
|
+
type: "projection/apply",
|
|
24788
|
+
projection: result.document.projection,
|
|
24789
|
+
document: createReviewDocument({
|
|
24790
|
+
identity: aggregate.identity,
|
|
24791
|
+
generation: aggregate.generation,
|
|
24792
|
+
commits: aggregate.commits,
|
|
24793
|
+
files: result.document.files
|
|
24794
|
+
})
|
|
24795
|
+
});
|
|
24796
|
+
return { ok: true, fileCount: result.document.files.length };
|
|
24797
|
+
}
|
|
24798
|
+
exitProjection() {
|
|
24799
|
+
const current = this._state;
|
|
24800
|
+
const aggregate = this.aggregateDocument;
|
|
24801
|
+
if (current === undefined || aggregate === undefined || current.projection.kind === "aggregate")
|
|
24802
|
+
return false;
|
|
24803
|
+
this.aggregateDocument = undefined;
|
|
24804
|
+
this.dispatch({ type: "projection/apply", projection: { kind: "aggregate" }, document: aggregate });
|
|
24805
|
+
return true;
|
|
24806
|
+
}
|
|
23838
24807
|
async flushDrafts() {
|
|
23839
24808
|
if (!this.stateStore)
|
|
23840
24809
|
return;
|
|
@@ -23868,7 +24837,7 @@ class ReviewWorkspaceController {
|
|
|
23868
24837
|
artifactIdFromMarker = marker.artifactId;
|
|
23869
24838
|
const raw = await this.artifactStore.readRaw(reviewId, marker.artifactId);
|
|
23870
24839
|
if (raw !== undefined) {
|
|
23871
|
-
const digest =
|
|
24840
|
+
const digest = createHash6("sha256").update(raw, "utf8").digest("hex");
|
|
23872
24841
|
if (digest === marker.digest) {
|
|
23873
24842
|
const parsed = JSON.parse(raw);
|
|
23874
24843
|
const res = parseReviewArtifactV1(parsed);
|
|
@@ -24083,6 +25052,8 @@ class ReviewWorkspaceController {
|
|
|
24083
25052
|
this.destroyed = true;
|
|
24084
25053
|
this.listeners.clear();
|
|
24085
25054
|
this.requestId++;
|
|
25055
|
+
this.baseSelectionRequestId++;
|
|
25056
|
+
this._baseSelection = undefined;
|
|
24086
25057
|
this.pendingGapRequests.clear();
|
|
24087
25058
|
this.sourceContextCache.clear();
|
|
24088
25059
|
if (this.activeReviewId && this.stateStore) {
|
|
@@ -24115,7 +25086,7 @@ class ReviewWorkspaceController {
|
|
|
24115
25086
|
const submissionInProgress = db.reviews[reviewId]?.submissionInProgress ?? null;
|
|
24116
25087
|
return {
|
|
24117
25088
|
...db,
|
|
24118
|
-
baseByHead: { ...db.baseByHead, [headKey]: { baseRef: snapshot.document.identity.baseRef } },
|
|
25089
|
+
baseByHead: { ...db.baseByHead, [headKey]: { baseRef: snapshot.document.identity.baseRef, confirmed: true } },
|
|
24119
25090
|
reviews: { ...db.reviews, [reviewId]: { ...persisted, submissionInProgress } }
|
|
24120
25091
|
};
|
|
24121
25092
|
});
|
|
@@ -24128,37 +25099,26 @@ class ReviewWorkspaceController {
|
|
|
24128
25099
|
throw err;
|
|
24129
25100
|
}
|
|
24130
25101
|
}
|
|
24131
|
-
async
|
|
25102
|
+
async rememberedBase() {
|
|
24132
25103
|
const headRef = await currentBranchRef(this.runner);
|
|
24133
25104
|
const detachedOid = headRef === undefined ? await resolveRefOid(this.runner, "HEAD") : undefined;
|
|
24134
25105
|
const headKey = headRef ?? (detachedOid ? `detached:${detachedOid}` : undefined);
|
|
24135
|
-
if (headKey
|
|
24136
|
-
|
|
24137
|
-
const db = await this.stateStore.load();
|
|
24138
|
-
const warning = this.stateStore.quarantineWarning;
|
|
24139
|
-
if (warning) {
|
|
24140
|
-
const pathMatch = warning.match(/moved to (\S+)/);
|
|
24141
|
-
const qPath = pathMatch?.[1] ?? warning;
|
|
24142
|
-
this._error = createCorruptStateError(qPath, warning);
|
|
24143
|
-
this.publish();
|
|
24144
|
-
}
|
|
24145
|
-
const remembered = db.baseByHead[headKey]?.baseRef;
|
|
24146
|
-
if (remembered && await resolveRefOid(this.runner, remembered) !== undefined) {
|
|
24147
|
-
return remembered;
|
|
24148
|
-
}
|
|
24149
|
-
} catch {}
|
|
24150
|
-
}
|
|
25106
|
+
if (headKey === undefined || this.stateStore === undefined)
|
|
25107
|
+
return;
|
|
24151
25108
|
try {
|
|
24152
|
-
const
|
|
24153
|
-
|
|
24154
|
-
|
|
24155
|
-
|
|
24156
|
-
|
|
24157
|
-
|
|
25109
|
+
const db = await this.stateStore.load();
|
|
25110
|
+
const warning = this.stateStore.quarantineWarning;
|
|
25111
|
+
if (warning) {
|
|
25112
|
+
const qPath = warning.match(/moved to (\S+)/)?.[1] ?? warning;
|
|
25113
|
+
this._error = createCorruptStateError(qPath, warning);
|
|
25114
|
+
this.publish();
|
|
25115
|
+
}
|
|
25116
|
+
return db.baseByHead[headKey];
|
|
24158
25117
|
} catch (err) {
|
|
24159
|
-
|
|
25118
|
+
this._error = createStorageError(err instanceof Error ? err.message : String(err));
|
|
25119
|
+
this.publish();
|
|
25120
|
+
return;
|
|
24160
25121
|
}
|
|
24161
|
-
return "refs/heads/main";
|
|
24162
25122
|
}
|
|
24163
25123
|
}
|
|
24164
25124
|
|
|
@@ -24892,6 +25852,64 @@ ${detail}
|
|
|
24892
25852
|
if (false) {}
|
|
24893
25853
|
|
|
24894
25854
|
// src/cli.ts
|
|
25855
|
+
var RELEASES_API = "https://api.github.com/repos/XuHaoJun/githunk/releases/latest";
|
|
25856
|
+
var DOWNLOAD_BASE = "https://github.com/XuHaoJun/githunk/releases/download";
|
|
25857
|
+
async function fetchText(url) {
|
|
25858
|
+
const response = await fetch(url);
|
|
25859
|
+
if (!response.ok)
|
|
25860
|
+
throw new Error(`request failed: ${url} (${response.status})`);
|
|
25861
|
+
return response.text();
|
|
25862
|
+
}
|
|
25863
|
+
async function fetchBytes(url) {
|
|
25864
|
+
const response = await fetch(url);
|
|
25865
|
+
if (!response.ok)
|
|
25866
|
+
throw new Error(`request failed: ${url} (${response.status})`);
|
|
25867
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
25868
|
+
}
|
|
25869
|
+
function productionUpdateEnv() {
|
|
25870
|
+
return {
|
|
25871
|
+
executablePath: process.execPath,
|
|
25872
|
+
platform: process.platform,
|
|
25873
|
+
arch: process.arch,
|
|
25874
|
+
installedVersion: () => {
|
|
25875
|
+
const proc = spawnSync(process.execPath, ["--version"], { encoding: "utf8" });
|
|
25876
|
+
if (proc.status !== 0)
|
|
25877
|
+
throw new Error("could not read the installed version");
|
|
25878
|
+
return proc.stdout.trim();
|
|
25879
|
+
},
|
|
25880
|
+
fetchReleaseTag: async () => {
|
|
25881
|
+
const payload = JSON.parse(await fetchText(RELEASES_API));
|
|
25882
|
+
const tag = typeof payload === "object" && payload !== null && "tag_name" in payload ? payload.tag_name : undefined;
|
|
25883
|
+
if (typeof tag !== "string" || tag === "")
|
|
25884
|
+
throw new Error("could not read the newest release");
|
|
25885
|
+
return tag;
|
|
25886
|
+
},
|
|
25887
|
+
fetchAsset: async (tag, asset) => ({
|
|
25888
|
+
tarball: await fetchBytes(`${DOWNLOAD_BASE}/${tag}/${asset}`),
|
|
25889
|
+
checksums: await fetchText(`${DOWNLOAD_BASE}/${tag}/SHA256SUMS`)
|
|
25890
|
+
}),
|
|
25891
|
+
withTempDir: async (run) => {
|
|
25892
|
+
const dir = mkdtempSync(join6(tmpdir(), "githunk-update-"));
|
|
25893
|
+
try {
|
|
25894
|
+
return await run(dir);
|
|
25895
|
+
} finally {
|
|
25896
|
+
rmSync(dir, { recursive: true, force: true });
|
|
25897
|
+
}
|
|
25898
|
+
},
|
|
25899
|
+
writeFile: (path, data) => writeFile(path, data),
|
|
25900
|
+
extractTarball: async (archivePath, destDir) => {
|
|
25901
|
+
const proc = spawnSync("tar", ["-xzf", archivePath, "-C", destDir]);
|
|
25902
|
+
if (proc.status !== 0)
|
|
25903
|
+
throw new Error("could not extract the release archive (need tar on PATH)");
|
|
25904
|
+
},
|
|
25905
|
+
stagedBinary: (dir) => join6(dir, `githunk-${process.platform === "win32" ? "windows" : process.platform}-${process.arch === "arm64" ? "arm64" : "x64"}`, process.platform === "win32" ? "githunk.exe" : "githunk"),
|
|
25906
|
+
writeBinary: async (stagedPath, destPath) => {
|
|
25907
|
+
cpSync(stagedPath, `${destPath}.new`);
|
|
25908
|
+
chmodSync(`${destPath}.new`, 493);
|
|
25909
|
+
renameSync(`${destPath}.new`, destPath);
|
|
25910
|
+
}
|
|
25911
|
+
};
|
|
25912
|
+
}
|
|
24895
25913
|
var result = parseCliArgs(process.argv.slice(2));
|
|
24896
25914
|
if (result.kind === "help" || result.kind === "version") {
|
|
24897
25915
|
process.stdout.write(result.text.endsWith(`
|
|
@@ -24903,6 +25921,13 @@ if (result.kind === "help" || result.kind === "version") {
|
|
|
24903
25921
|
`) ? result.message : `${result.message}
|
|
24904
25922
|
`);
|
|
24905
25923
|
process.exitCode = result.exitCode;
|
|
25924
|
+
} else if (result.kind === "update") {
|
|
25925
|
+
const outcome = await runUpdate({ ...result.version === undefined ? {} : { version: result.version }, check: result.check }, productionUpdateEnv());
|
|
25926
|
+
const stream = outcome.exitCode === 0 ? process.stdout : process.stderr;
|
|
25927
|
+
stream.write(outcome.message.endsWith(`
|
|
25928
|
+
`) ? outcome.message : `${outcome.message}
|
|
25929
|
+
`);
|
|
25930
|
+
process.exitCode = outcome.exitCode;
|
|
24906
25931
|
} else {
|
|
24907
25932
|
process.exitCode = await startApp(result.startDirectory === undefined ? {} : { startDirectory: result.startDirectory });
|
|
24908
25933
|
}
|