dsh-diff-approval 0.19.2 → 0.20.0
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/lib/client.js +2782 -726
- package/lib/index.js +521 -59
- package/lib/types/client/PathPicker.d.ts +43 -0
- package/lib/types/client/PendingPanel.d.ts +13 -17
- package/lib/types/client/highlight.d.ts +51 -8
- package/lib/types/client/lang.d.ts +8 -0
- package/lib/types/client/locales.d.ts +71 -0
- package/lib/types/client/port.d.ts +11 -5
- package/lib/types/client/search.d.ts +23 -0
- package/lib/types/client/settings.d.ts +41 -0
- package/lib/types/client/slots.d.ts +13 -5
- package/lib/types/client/split-diff.d.ts +7 -5
- package/lib/types/client/store.d.ts +12 -5
- package/lib/types/client/whole-file-diff.d.ts +21 -0
- package/lib/types/client/windowed-highlight.d.ts +66 -0
- package/lib/types/index.d.ts +1 -1
- package/lib/types/pending.d.ts +10 -0
- package/lib/types/types.d.ts +67 -0
- package/lib/types/vcs.d.ts +6 -0
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -138,6 +138,18 @@ var PendingDiffStore = class {
|
|
|
138
138
|
return true;
|
|
139
139
|
}
|
|
140
140
|
/**
|
|
141
|
+
* Admit one entry into the list without the change guard {@link fold} applies.
|
|
142
|
+
* A listed entry may carry no diff at all: a path the user added by hand has
|
|
143
|
+
* no local change until one appears, and a fully-resolved file has already
|
|
144
|
+
* folded its diff away. This is the guard-free put {@link restore} performs,
|
|
145
|
+
* named for its other caller.
|
|
146
|
+
* @param entry - the entry to insert or replace by path.
|
|
147
|
+
* @returns whether the store changed.
|
|
148
|
+
*/
|
|
149
|
+
insert(entry) {
|
|
150
|
+
return this.restore(entry);
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
141
153
|
* Merge persisted entries into the store, one per path after folding. A live
|
|
142
154
|
* entry wins over a persisted one only when its time is newer (folders are
|
|
143
155
|
* applied in capture order, so a later persisted capture is strictly newer).
|
|
@@ -440,6 +452,18 @@ function isPathInside(absolutePath, root) {
|
|
|
440
452
|
if (path === base) return true;
|
|
441
453
|
return path.startsWith(base + sep);
|
|
442
454
|
}
|
|
455
|
+
/**
|
|
456
|
+
* Whether one changed path belongs to the scan: inside the workspace, and inside
|
|
457
|
+
* the narrowed `scope` when the caller set one.
|
|
458
|
+
* @param absolutePath - the changed file's absolute path.
|
|
459
|
+
* @param workspaceRoot - the session's workspace root.
|
|
460
|
+
* @param scope - one path to restrict the scan to, or undefined for all of it.
|
|
461
|
+
* @returns whether the change is in scope.
|
|
462
|
+
*/
|
|
463
|
+
function inScanScope(absolutePath, workspaceRoot, scope) {
|
|
464
|
+
if (!isPathInside(absolutePath, workspaceRoot)) return false;
|
|
465
|
+
return scope === void 0 || isPathInside(absolutePath, scope);
|
|
466
|
+
}
|
|
443
467
|
/** The VCS marker of one directory, or undefined when it holds none. */
|
|
444
468
|
function markerOf(directory) {
|
|
445
469
|
if (existsSync(resolve(directory, ".git"))) return "git";
|
|
@@ -511,12 +535,12 @@ function parseGitPorcelainZ(output) {
|
|
|
511
535
|
}
|
|
512
536
|
/** Enumerate the workspace's local changes in a git checkout. */
|
|
513
537
|
async function gitChanges(input) {
|
|
514
|
-
const { root, workspaceRoot, includeUntracked, shell, readText, signal } = input;
|
|
538
|
+
const { root, workspaceRoot, includeUntracked, scope, shell, readText, signal } = input;
|
|
515
539
|
const stdout = await runShell(shell, "git -c status.renames=false status --porcelain=v1 -z --untracked-files=all", root, signal);
|
|
516
540
|
const changes = [];
|
|
517
541
|
for (const { xy, rel } of parseGitPorcelainZ(stdout)) {
|
|
518
542
|
const absolute = resolve(root, rel);
|
|
519
|
-
if (!
|
|
543
|
+
if (!inScanScope(absolute, workspaceRoot, scope)) continue;
|
|
520
544
|
if (xy === "??") {
|
|
521
545
|
if (!includeUntracked) continue;
|
|
522
546
|
const newText = await readText(absolute) ?? "";
|
|
@@ -559,7 +583,7 @@ function xmlUnescape(value) {
|
|
|
559
583
|
}
|
|
560
584
|
/** Enumerate the workspace's local changes in an svn working copy. */
|
|
561
585
|
async function svnChanges(input) {
|
|
562
|
-
const { root, workspaceRoot, includeUntracked, shell, readText, signal } = input;
|
|
586
|
+
const { root, workspaceRoot, includeUntracked, scope, shell, readText, signal } = input;
|
|
563
587
|
const stdout = await runShell(shell, "svn status --xml", root, signal);
|
|
564
588
|
const changes = [];
|
|
565
589
|
const entryPattern = /<entry[^>]*path="([^"]*)"[^>]*>\s*<wc-status[^>]*item="([^"]*)"/g;
|
|
@@ -568,7 +592,7 @@ async function svnChanges(input) {
|
|
|
568
592
|
const rel = xmlUnescape(match[1]);
|
|
569
593
|
const item = match[2];
|
|
570
594
|
const absolute = resolve(root, rel);
|
|
571
|
-
if (!
|
|
595
|
+
if (!inScanScope(absolute, workspaceRoot, scope)) continue;
|
|
572
596
|
if (item === "modified" || item === "deleted") {
|
|
573
597
|
let oldText = "";
|
|
574
598
|
try {
|
|
@@ -623,7 +647,7 @@ function p4ChangeOf(line) {
|
|
|
623
647
|
* not yet opened for add; off keeps to already-opened files (`p4 opened`) so
|
|
624
648
|
* the scan — which can be slow — is skipped. */
|
|
625
649
|
async function p4Changes(input) {
|
|
626
|
-
const { root, workspaceRoot, includeUntracked, shell, readText, signal } = input;
|
|
650
|
+
const { root, workspaceRoot, includeUntracked, scope, shell, readText, signal } = input;
|
|
627
651
|
const stdout = await runShell(shell, includeUntracked ? "p4 status" : "p4 opened", root, signal);
|
|
628
652
|
const changes = [];
|
|
629
653
|
for (const line of stdout.split("\n")) {
|
|
@@ -632,7 +656,7 @@ async function p4Changes(input) {
|
|
|
632
656
|
const local = (await runShell(shell, `p4 where ${shq(opened.depot)}`, root, signal)).trim().split(/\s+/).pop();
|
|
633
657
|
if (local === void 0 || local.length === 0) continue;
|
|
634
658
|
const absolute = resolve(local);
|
|
635
|
-
if (!
|
|
659
|
+
if (!inScanScope(absolute, workspaceRoot, scope)) continue;
|
|
636
660
|
const deleted = opened.action === "delete" || opened.action === "move/delete";
|
|
637
661
|
const created = opened.action === "add" || opened.action === "move/add";
|
|
638
662
|
const newText = deleted ? "" : await readText(absolute) ?? "";
|
|
@@ -808,6 +832,50 @@ function writeOutcomeOf(value) {
|
|
|
808
832
|
function errorMessage(error) {
|
|
809
833
|
return error instanceof Error ? error.message : String(error);
|
|
810
834
|
}
|
|
835
|
+
/** Directories one browse level hides: VCS/build noise a review list never wants.
|
|
836
|
+
* The path box can still reach them by typing the path outright. */
|
|
837
|
+
const BROWSE_HIDDEN_NAMES = /* @__PURE__ */ new Set([".git", "node_modules"]);
|
|
838
|
+
/** Cap on one browse level's children: the panel renders a list, not a dump of a
|
|
839
|
+
* huge directory, and it reports `truncated` rather than silently cutting. */
|
|
840
|
+
const BROWSE_ENTRY_CAP = 500;
|
|
841
|
+
/** Cap on the files one no-change walk reads: ticking the box on a directory
|
|
842
|
+
* asks for "everything under here", which must not mean reading a whole tree
|
|
843
|
+
* (every file's text) inside one call. */
|
|
844
|
+
const ADD_UNCHANGED_CAP = 300;
|
|
845
|
+
/**
|
|
846
|
+
* One absolute path as a workspace-relative path with `/` separators, or
|
|
847
|
+
* `undefined` when it lies outside the root. `''` is the root itself.
|
|
848
|
+
* @param root - the workspace root.
|
|
849
|
+
* @param absolute - the path to express relative to it.
|
|
850
|
+
* @returns the relative path, or undefined when outside.
|
|
851
|
+
*/
|
|
852
|
+
function workspaceRelativeOf(root, absolute) {
|
|
853
|
+
const rel = relative(resolve(root), resolve(absolute));
|
|
854
|
+
if (rel === "") return "";
|
|
855
|
+
if (isAbsolute(rel)) return void 0;
|
|
856
|
+
const parts = rel.split(/[\\/]/);
|
|
857
|
+
if (parts[0] === "..") return void 0;
|
|
858
|
+
return parts.join("/");
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* Resolve one caller-supplied path against the workspace root. A relative path
|
|
862
|
+
* is taken as workspace-relative; an absolute one must still land inside.
|
|
863
|
+
* @param root - the workspace root.
|
|
864
|
+
* @param input - the caller's path (absolute or workspace-relative).
|
|
865
|
+
* @returns the absolute path, or undefined when it escapes the workspace.
|
|
866
|
+
*/
|
|
867
|
+
function resolveInsideWorkspace(root, input) {
|
|
868
|
+
const absolute = isAbsolute(input) ? resolve(input) : resolve(root, input);
|
|
869
|
+
return workspaceRelativeOf(root, absolute) === void 0 ? void 0 : absolute;
|
|
870
|
+
}
|
|
871
|
+
/** The workspace-relative parent of a relative directory path (`''` at the root). */
|
|
872
|
+
/** Fold one path for comparison: absolute, `/`-separated, case-folded on Windows.
|
|
873
|
+
* Entries are keyed by the path spelling their capture carried (a tool's display
|
|
874
|
+
* path or a scan's absolute one), so an equality test has to normalize first. */
|
|
875
|
+
function pathIdentity(absolute) {
|
|
876
|
+
const unified = resolve(absolute).split(/[\\/]/).join("/");
|
|
877
|
+
return process.platform === "win32" ? unified.toLowerCase() : unified;
|
|
878
|
+
}
|
|
811
879
|
/** Narrow a tool-execution-shaped value to its name, call id, and agent. */
|
|
812
880
|
function actorOf(value) {
|
|
813
881
|
if (typeof value !== "object" || value === null) return void 0;
|
|
@@ -1091,6 +1159,138 @@ function apply(ctx, config) {
|
|
|
1091
1159
|
};
|
|
1092
1160
|
}
|
|
1093
1161
|
/**
|
|
1162
|
+
* Fold a batch of entries into one session's list as a single undoable action.
|
|
1163
|
+
* Nothing touches the files, so the batch is undone by restoring each affected
|
|
1164
|
+
* path's pre-fold entry (or removing it when the path was not listed), which is
|
|
1165
|
+
* what the import and the hand-add path both want.
|
|
1166
|
+
* @param sessionId - the session whose list gains the entries.
|
|
1167
|
+
* @param entries - the entries to fold, in capture order.
|
|
1168
|
+
* @param admitNoDiff - also admit entries that carry no diff at all (the
|
|
1169
|
+
* guard-free insert), which is how a hand-added clean path is listed.
|
|
1170
|
+
* @returns how many entries landed; 0 leaves the store, persistence, and the
|
|
1171
|
+
* undo queue untouched.
|
|
1172
|
+
*/
|
|
1173
|
+
async function foldBatch(sessionId, entries, admitNoDiff = false) {
|
|
1174
|
+
await ensureLoaded();
|
|
1175
|
+
const before = new Map(store.list(sessionId).map((entry) => [entry.path, entry]));
|
|
1176
|
+
let folded = 0;
|
|
1177
|
+
const changedPaths = [];
|
|
1178
|
+
for (const entry of entries) if (entry.oldText === entry.newText ? admitNoDiff && store.insert(entry) : store.fold(entry)) {
|
|
1179
|
+
folded += 1;
|
|
1180
|
+
changedPaths.push(entry.path);
|
|
1181
|
+
}
|
|
1182
|
+
if (folded === 0) return 0;
|
|
1183
|
+
persistSession(true);
|
|
1184
|
+
const after = new Map(store.list(sessionId).map((entry) => [entry.path, entry]));
|
|
1185
|
+
const batchBefore = [];
|
|
1186
|
+
const batchAfter = [];
|
|
1187
|
+
for (const path of changedPaths) {
|
|
1188
|
+
const final = after.get(path);
|
|
1189
|
+
if (final === void 0) continue;
|
|
1190
|
+
const pre = before.get(path);
|
|
1191
|
+
batchAfter.push({
|
|
1192
|
+
id: final.id,
|
|
1193
|
+
path,
|
|
1194
|
+
entry: final,
|
|
1195
|
+
fileText: void 0
|
|
1196
|
+
});
|
|
1197
|
+
batchBefore.push({
|
|
1198
|
+
id: final.id,
|
|
1199
|
+
path,
|
|
1200
|
+
entry: pre,
|
|
1201
|
+
fileText: void 0
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
if (batchBefore.length > 0) pushUndo(sessionId, {
|
|
1205
|
+
id: batchBefore[0].id,
|
|
1206
|
+
path: batchBefore[0].path,
|
|
1207
|
+
entry: void 0,
|
|
1208
|
+
fileText: void 0,
|
|
1209
|
+
batch: batchBefore
|
|
1210
|
+
}, {
|
|
1211
|
+
id: batchAfter[0].id,
|
|
1212
|
+
path: batchAfter[0].path,
|
|
1213
|
+
entry: void 0,
|
|
1214
|
+
fileText: void 0,
|
|
1215
|
+
batch: batchAfter
|
|
1216
|
+
});
|
|
1217
|
+
return folded;
|
|
1218
|
+
}
|
|
1219
|
+
/**
|
|
1220
|
+
* One path's text, or `undefined` when it is absent or not readable as text
|
|
1221
|
+
* (a binary, an unreadable permission). An empty file reads as `''`, which is
|
|
1222
|
+
* a value: a listed entry may carry no content at all.
|
|
1223
|
+
* @param absolute - the path to read.
|
|
1224
|
+
* @param signal - caller lifetime.
|
|
1225
|
+
* @returns the text, or undefined.
|
|
1226
|
+
*/
|
|
1227
|
+
async function readTextOrNone(absolute, signal) {
|
|
1228
|
+
try {
|
|
1229
|
+
return await ctx.fs.readText(await ctx.fs.resolve(absolute, { signal }), signal);
|
|
1230
|
+
} catch {
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
/**
|
|
1235
|
+
* Every regular file under one directory, with its text. Breadth-first in the
|
|
1236
|
+
* backend's name order, hiding the browse's noise names, skipping whatever is
|
|
1237
|
+
* not readable as text, and stopping at {@link ADD_UNCHANGED_CAP} files so
|
|
1238
|
+
* "include paths with no change" cannot read an unbounded tree in one call.
|
|
1239
|
+
* Symlinked directories are followed once, so a cycle ends rather than loops.
|
|
1240
|
+
* @param root - the resolved directory target to walk.
|
|
1241
|
+
* @param rootAbsolute - that directory's absolute path.
|
|
1242
|
+
* @param signal - caller lifetime.
|
|
1243
|
+
* @returns the files found and whether the cap cut the walk short.
|
|
1244
|
+
*/
|
|
1245
|
+
async function collectFilesUnder(root, rootAbsolute, signal) {
|
|
1246
|
+
const files = [];
|
|
1247
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1248
|
+
if (typeof root.targetKey === "string" && root.targetKey !== "") visited.add(root.targetKey);
|
|
1249
|
+
const queue = [{
|
|
1250
|
+
absolute: rootAbsolute,
|
|
1251
|
+
target: root
|
|
1252
|
+
}];
|
|
1253
|
+
while (queue.length > 0) {
|
|
1254
|
+
const current = queue.shift();
|
|
1255
|
+
if (current === void 0) break;
|
|
1256
|
+
let children;
|
|
1257
|
+
try {
|
|
1258
|
+
children = await ctx.fs.listDir(current.target, signal);
|
|
1259
|
+
} catch {
|
|
1260
|
+
continue;
|
|
1261
|
+
}
|
|
1262
|
+
for (const child of children) {
|
|
1263
|
+
if (BROWSE_HIDDEN_NAMES.has(child.name)) continue;
|
|
1264
|
+
const absolute = resolve(current.absolute, child.name);
|
|
1265
|
+
if (child.type === "directory") {
|
|
1266
|
+
const key = child.target.targetKey;
|
|
1267
|
+
if (typeof key === "string" && key !== "") {
|
|
1268
|
+
if (visited.has(key)) continue;
|
|
1269
|
+
visited.add(key);
|
|
1270
|
+
}
|
|
1271
|
+
queue.push({
|
|
1272
|
+
absolute,
|
|
1273
|
+
target: child.target
|
|
1274
|
+
});
|
|
1275
|
+
continue;
|
|
1276
|
+
}
|
|
1277
|
+
if (child.type !== "file") continue;
|
|
1278
|
+
if (files.length >= ADD_UNCHANGED_CAP) return {
|
|
1279
|
+
files,
|
|
1280
|
+
truncated: true
|
|
1281
|
+
};
|
|
1282
|
+
files.push({
|
|
1283
|
+
path: absolute,
|
|
1284
|
+
content: await readTextOrNone(absolute, signal)
|
|
1285
|
+
});
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
return {
|
|
1289
|
+
files,
|
|
1290
|
+
truncated: false
|
|
1291
|
+
};
|
|
1292
|
+
}
|
|
1293
|
+
/**
|
|
1094
1294
|
* The workspace whose session account holds `sessionId`. Web sessions are
|
|
1095
1295
|
* attached to a workspace at creation, so an unowned session is the
|
|
1096
1296
|
* memory-only edge (its entries never persist).
|
|
@@ -1298,6 +1498,33 @@ function apply(ctx, config) {
|
|
|
1298
1498
|
value: { outcome: "missing" }
|
|
1299
1499
|
};
|
|
1300
1500
|
}
|
|
1501
|
+
if (payload.keepListed === true) {
|
|
1502
|
+
store.update(target.id, { oldText: entry.newText });
|
|
1503
|
+
const afterEntry = {
|
|
1504
|
+
...entry,
|
|
1505
|
+
oldText: entry.newText,
|
|
1506
|
+
updatedAt: Date.now()
|
|
1507
|
+
};
|
|
1508
|
+
pushUndo(target.sessionId, {
|
|
1509
|
+
id: entry.path,
|
|
1510
|
+
path: entry.path,
|
|
1511
|
+
entry,
|
|
1512
|
+
fileText: void 0
|
|
1513
|
+
}, {
|
|
1514
|
+
id: entry.path,
|
|
1515
|
+
path: entry.path,
|
|
1516
|
+
entry: afterEntry,
|
|
1517
|
+
fileText: void 0
|
|
1518
|
+
});
|
|
1519
|
+
persistSession(true);
|
|
1520
|
+
return {
|
|
1521
|
+
ok: true,
|
|
1522
|
+
value: {
|
|
1523
|
+
outcome: "kept",
|
|
1524
|
+
resolved: true
|
|
1525
|
+
}
|
|
1526
|
+
};
|
|
1527
|
+
}
|
|
1301
1528
|
store.remove(target.id);
|
|
1302
1529
|
pushUndo(target.sessionId, {
|
|
1303
1530
|
id: entry.path,
|
|
@@ -1334,6 +1561,34 @@ function apply(ctx, config) {
|
|
|
1334
1561
|
} catch (error) {
|
|
1335
1562
|
return rpcError(`revert failed: ${errorMessage(error)}`);
|
|
1336
1563
|
}
|
|
1564
|
+
if (payload.keepListed === true) {
|
|
1565
|
+
const content = undo?.after.fileText ?? "";
|
|
1566
|
+
store.update(target.id, { newText: content });
|
|
1567
|
+
const afterEntry = {
|
|
1568
|
+
...entry,
|
|
1569
|
+
newText: content,
|
|
1570
|
+
updatedAt: Date.now()
|
|
1571
|
+
};
|
|
1572
|
+
if (undo !== void 0) pushUndo(target.sessionId, {
|
|
1573
|
+
id: entry.path,
|
|
1574
|
+
path: entry.path,
|
|
1575
|
+
entry,
|
|
1576
|
+
fileText: undo.before.fileText
|
|
1577
|
+
}, {
|
|
1578
|
+
id: entry.path,
|
|
1579
|
+
path: entry.path,
|
|
1580
|
+
entry: afterEntry,
|
|
1581
|
+
fileText: content
|
|
1582
|
+
});
|
|
1583
|
+
persistSession(true);
|
|
1584
|
+
return {
|
|
1585
|
+
ok: true,
|
|
1586
|
+
value: {
|
|
1587
|
+
outcome: "reverted",
|
|
1588
|
+
resolved: true
|
|
1589
|
+
}
|
|
1590
|
+
};
|
|
1591
|
+
}
|
|
1337
1592
|
store.remove(target.id);
|
|
1338
1593
|
if (undo !== void 0) pushUndo(target.sessionId, undo.before, undo.after);
|
|
1339
1594
|
persistSession(true);
|
|
@@ -1623,67 +1878,256 @@ function apply(ctx, config) {
|
|
|
1623
1878
|
} catch (error) {
|
|
1624
1879
|
return rpcError(`import failed: ${errorMessage(error)}`);
|
|
1625
1880
|
}
|
|
1881
|
+
return {
|
|
1882
|
+
ok: true,
|
|
1883
|
+
value: {
|
|
1884
|
+
imported: await foldBatch(sessionId, changes.map((change) => ({
|
|
1885
|
+
id: change.path,
|
|
1886
|
+
sessionId,
|
|
1887
|
+
path: change.path,
|
|
1888
|
+
kind: change.kind,
|
|
1889
|
+
oldText: change.oldText,
|
|
1890
|
+
newText: change.newText,
|
|
1891
|
+
updatedAt: Date.now(),
|
|
1892
|
+
sessionIds: [sessionId]
|
|
1893
|
+
}))),
|
|
1894
|
+
detected: true
|
|
1895
|
+
}
|
|
1896
|
+
};
|
|
1897
|
+
}
|
|
1898
|
+
case "vcs-refresh": {
|
|
1899
|
+
const target = targetOf(payload);
|
|
1900
|
+
if (target === void 0) return rpcError("sessionId and id must be non-empty strings");
|
|
1626
1901
|
await ensureLoaded();
|
|
1627
|
-
const
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1902
|
+
const entry = store.get(target.id);
|
|
1903
|
+
if (entry === void 0) return {
|
|
1904
|
+
ok: true,
|
|
1905
|
+
value: { outcome: "missing" }
|
|
1906
|
+
};
|
|
1907
|
+
const workspace = workspaceOf(target.sessionId);
|
|
1908
|
+
if (workspace === void 0) return rpcError("refresh unavailable: the session has no workspace");
|
|
1909
|
+
const root = detectVcsRoot(workspace.path);
|
|
1910
|
+
if (root === void 0) return {
|
|
1911
|
+
ok: true,
|
|
1912
|
+
value: { outcome: "no-vcs" }
|
|
1913
|
+
};
|
|
1914
|
+
const shell = ctx.get("shell");
|
|
1915
|
+
if (shell === void 0) return rpcError("refresh unavailable: the deployment has no shell executor");
|
|
1916
|
+
let changes;
|
|
1917
|
+
try {
|
|
1918
|
+
changes = await listVcsChanges({
|
|
1919
|
+
kind: root.kind,
|
|
1920
|
+
root: root.root,
|
|
1921
|
+
workspaceRoot: workspace.path,
|
|
1922
|
+
includeUntracked: payload.includeUntracked === true,
|
|
1923
|
+
scope: entry.path,
|
|
1924
|
+
shell,
|
|
1925
|
+
readText: (path) => readFile(path, "utf8").catch(() => void 0),
|
|
1926
|
+
signal
|
|
1927
|
+
});
|
|
1928
|
+
} catch (error) {
|
|
1929
|
+
return rpcError(`refresh failed: ${errorMessage(error)}`);
|
|
1930
|
+
}
|
|
1931
|
+
const change = changes.find((candidate) => pathIdentity(candidate.path) === pathIdentity(entry.path));
|
|
1932
|
+
if (change === void 0) return {
|
|
1933
|
+
ok: true,
|
|
1934
|
+
value: { outcome: "no-change" }
|
|
1935
|
+
};
|
|
1936
|
+
if (change.kind === entry.kind && change.oldText === entry.oldText && change.newText === entry.newText) return {
|
|
1937
|
+
ok: true,
|
|
1938
|
+
value: { outcome: "unchanged" }
|
|
1939
|
+
};
|
|
1940
|
+
const refreshed = {
|
|
1941
|
+
...entry,
|
|
1942
|
+
kind: change.kind,
|
|
1943
|
+
oldText: change.oldText,
|
|
1944
|
+
newText: change.newText,
|
|
1945
|
+
updatedAt: Date.now()
|
|
1946
|
+
};
|
|
1947
|
+
store.restore(refreshed);
|
|
1948
|
+
pushUndo(target.sessionId, {
|
|
1949
|
+
id: entry.path,
|
|
1950
|
+
path: entry.path,
|
|
1951
|
+
entry,
|
|
1952
|
+
fileText: void 0
|
|
1953
|
+
}, {
|
|
1954
|
+
id: entry.path,
|
|
1955
|
+
path: entry.path,
|
|
1956
|
+
entry: refreshed,
|
|
1957
|
+
fileText: void 0
|
|
1958
|
+
});
|
|
1959
|
+
persistSession(true);
|
|
1960
|
+
return {
|
|
1961
|
+
ok: true,
|
|
1962
|
+
value: { outcome: "refreshed" }
|
|
1963
|
+
};
|
|
1964
|
+
}
|
|
1965
|
+
case "list-path": {
|
|
1966
|
+
const sessionId = sessionOf(payload);
|
|
1967
|
+
if (sessionId === void 0) return rpcError("sessionId must be a non-empty string");
|
|
1968
|
+
const workspace = workspaceOf(sessionId);
|
|
1969
|
+
if (workspace === void 0) return rpcError("browse unavailable: the session has no workspace");
|
|
1970
|
+
const requested = pathFieldOf(payload) ?? "";
|
|
1971
|
+
const absolute = resolveInsideWorkspace(workspace.path, requested);
|
|
1972
|
+
if (absolute === void 0) return rpcError("browse failed: the path is outside the workspace");
|
|
1973
|
+
let children;
|
|
1974
|
+
try {
|
|
1975
|
+
const target = await ctx.fs.resolve(absolute, { signal });
|
|
1976
|
+
const info = await ctx.fs.stat(target, signal);
|
|
1977
|
+
if (info === void 0 || info.type !== "directory") return rpcError("browse failed: not a directory");
|
|
1978
|
+
children = await ctx.fs.listDir(target, signal);
|
|
1979
|
+
} catch (error) {
|
|
1980
|
+
return rpcError(`browse failed: ${errorMessage(error)}`);
|
|
1981
|
+
}
|
|
1982
|
+
const entries = [];
|
|
1983
|
+
for (const child of children) {
|
|
1984
|
+
if (BROWSE_HIDDEN_NAMES.has(child.name)) continue;
|
|
1985
|
+
const childAbsolute = resolve(absolute, child.name);
|
|
1986
|
+
if (workspaceRelativeOf(workspace.path, childAbsolute) === void 0) continue;
|
|
1987
|
+
entries.push({
|
|
1988
|
+
name: child.name,
|
|
1989
|
+
type: child.type === "directory" ? "directory" : child.type === "file" ? "file" : "other",
|
|
1990
|
+
path: childAbsolute,
|
|
1991
|
+
size: child.type === "file" ? child.size : void 0
|
|
1992
|
+
});
|
|
1993
|
+
}
|
|
1994
|
+
entries.sort((left, right) => {
|
|
1995
|
+
const rank = (value) => value.type === "directory" ? 0 : 1;
|
|
1996
|
+
const byKind = rank(left) - rank(right);
|
|
1997
|
+
return byKind !== 0 ? byKind : left.name.localeCompare(right.name, void 0, {
|
|
1998
|
+
numeric: true,
|
|
1999
|
+
sensitivity: "base"
|
|
2000
|
+
});
|
|
2001
|
+
});
|
|
2002
|
+
const truncated = entries.length > BROWSE_ENTRY_CAP;
|
|
2003
|
+
return {
|
|
2004
|
+
ok: true,
|
|
2005
|
+
value: {
|
|
2006
|
+
path: absolute,
|
|
2007
|
+
entries: truncated ? entries.slice(0, BROWSE_ENTRY_CAP) : entries,
|
|
2008
|
+
truncated
|
|
1644
2009
|
}
|
|
2010
|
+
};
|
|
2011
|
+
}
|
|
2012
|
+
case "add-path": {
|
|
2013
|
+
const target = addTargetOf(payload);
|
|
2014
|
+
if (target === void 0) return rpcError("sessionId and path must be non-empty strings");
|
|
2015
|
+
const workspace = workspaceOf(target.sessionId);
|
|
2016
|
+
if (workspace === void 0) return rpcError("add unavailable: the session has no workspace");
|
|
2017
|
+
const absolute = resolveInsideWorkspace(workspace.path, target.path);
|
|
2018
|
+
if (absolute === void 0) return {
|
|
2019
|
+
ok: true,
|
|
2020
|
+
value: {
|
|
2021
|
+
outcome: "outside",
|
|
2022
|
+
added: 0,
|
|
2023
|
+
duplicates: 0
|
|
2024
|
+
}
|
|
2025
|
+
};
|
|
2026
|
+
await ensureLoaded();
|
|
2027
|
+
let info;
|
|
2028
|
+
try {
|
|
2029
|
+
const target = await ctx.fs.resolve(absolute, { signal });
|
|
2030
|
+
info = await ctx.fs.stat(target, signal);
|
|
2031
|
+
} catch (error) {
|
|
2032
|
+
return rpcError(`add failed: ${errorMessage(error)}`);
|
|
1645
2033
|
}
|
|
1646
|
-
if (
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
const final = after.get(path);
|
|
1653
|
-
if (final === void 0) continue;
|
|
1654
|
-
const pre = before.get(path);
|
|
1655
|
-
batchAfter.push({
|
|
1656
|
-
id: final.id,
|
|
1657
|
-
path,
|
|
1658
|
-
entry: final,
|
|
1659
|
-
fileText: void 0
|
|
1660
|
-
});
|
|
1661
|
-
batchBefore.push({
|
|
1662
|
-
id: final.id,
|
|
1663
|
-
path,
|
|
1664
|
-
entry: pre,
|
|
1665
|
-
fileText: void 0
|
|
1666
|
-
});
|
|
2034
|
+
if (info === void 0 || info.type === "other") return {
|
|
2035
|
+
ok: true,
|
|
2036
|
+
value: {
|
|
2037
|
+
outcome: "missing",
|
|
2038
|
+
added: 0,
|
|
2039
|
+
duplicates: 0
|
|
1667
2040
|
}
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
2041
|
+
};
|
|
2042
|
+
const isDirectory = info.type === "directory";
|
|
2043
|
+
const root = detectVcsRoot(workspace.path);
|
|
2044
|
+
if (root === void 0) return {
|
|
2045
|
+
ok: true,
|
|
2046
|
+
value: {
|
|
2047
|
+
outcome: "no-vcs",
|
|
2048
|
+
added: 0,
|
|
2049
|
+
duplicates: 0
|
|
2050
|
+
}
|
|
2051
|
+
};
|
|
2052
|
+
const shell = ctx.get("shell");
|
|
2053
|
+
if (shell === void 0) return rpcError("add unavailable: the deployment has no shell executor");
|
|
2054
|
+
let changes;
|
|
2055
|
+
try {
|
|
2056
|
+
changes = await listVcsChanges({
|
|
2057
|
+
kind: root.kind,
|
|
2058
|
+
root: root.root,
|
|
2059
|
+
workspaceRoot: workspace.path,
|
|
2060
|
+
includeUntracked: true,
|
|
2061
|
+
scope: absolute,
|
|
2062
|
+
shell,
|
|
2063
|
+
readText: (path) => readFile(path, "utf8").catch(() => void 0),
|
|
2064
|
+
signal
|
|
1680
2065
|
});
|
|
2066
|
+
} catch (error) {
|
|
2067
|
+
return {
|
|
2068
|
+
ok: true,
|
|
2069
|
+
value: {
|
|
2070
|
+
outcome: "failed",
|
|
2071
|
+
added: 0,
|
|
2072
|
+
duplicates: 0,
|
|
2073
|
+
message: errorMessage(error)
|
|
2074
|
+
}
|
|
2075
|
+
};
|
|
1681
2076
|
}
|
|
2077
|
+
const now = Date.now();
|
|
2078
|
+
const candidates = changes.map((change) => ({
|
|
2079
|
+
id: change.path,
|
|
2080
|
+
sessionId: target.sessionId,
|
|
2081
|
+
path: change.path,
|
|
2082
|
+
kind: change.kind,
|
|
2083
|
+
oldText: change.oldText,
|
|
2084
|
+
newText: change.newText,
|
|
2085
|
+
updatedAt: now,
|
|
2086
|
+
sessionIds: [target.sessionId]
|
|
2087
|
+
}));
|
|
2088
|
+
let truncated = false;
|
|
2089
|
+
if (target.includeUnchanged) {
|
|
2090
|
+
const scanned = new Set(candidates.map((entry) => pathIdentity(entry.path)));
|
|
2091
|
+
const found = isDirectory ? await collectFilesUnder(await ctx.fs.resolve(absolute, { signal }), absolute, signal) : {
|
|
2092
|
+
files: [{
|
|
2093
|
+
path: absolute,
|
|
2094
|
+
content: await readTextOrNone(absolute, signal)
|
|
2095
|
+
}],
|
|
2096
|
+
truncated: false
|
|
2097
|
+
};
|
|
2098
|
+
truncated = found.truncated;
|
|
2099
|
+
for (const file of found.files) {
|
|
2100
|
+
if (file.content === void 0) continue;
|
|
2101
|
+
if (scanned.has(pathIdentity(file.path))) continue;
|
|
2102
|
+
scanned.add(pathIdentity(file.path));
|
|
2103
|
+
candidates.push({
|
|
2104
|
+
id: file.path,
|
|
2105
|
+
sessionId: target.sessionId,
|
|
2106
|
+
path: file.path,
|
|
2107
|
+
kind: "edit",
|
|
2108
|
+
oldText: file.content,
|
|
2109
|
+
newText: file.content,
|
|
2110
|
+
updatedAt: now,
|
|
2111
|
+
sessionIds: [target.sessionId]
|
|
2112
|
+
});
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
const listed = new Set(store.list(target.sessionId).map((entry) => pathIdentity(entry.path)));
|
|
2116
|
+
const fresh = candidates.filter((entry) => !listed.has(pathIdentity(entry.path)));
|
|
2117
|
+
const duplicates = candidates.length - fresh.length;
|
|
2118
|
+
const added = await foldBatch(target.sessionId, fresh, true);
|
|
2119
|
+
const outcome = added > 0 ? "added" : duplicates > 0 ? "duplicate" : isDirectory ? "empty" : "unchanged";
|
|
1682
2120
|
return {
|
|
1683
2121
|
ok: true,
|
|
1684
|
-
value: {
|
|
1685
|
-
|
|
1686
|
-
|
|
2122
|
+
value: truncated ? {
|
|
2123
|
+
outcome,
|
|
2124
|
+
added,
|
|
2125
|
+
duplicates,
|
|
2126
|
+
truncated
|
|
2127
|
+
} : {
|
|
2128
|
+
outcome,
|
|
2129
|
+
added,
|
|
2130
|
+
duplicates
|
|
1687
2131
|
}
|
|
1688
2132
|
};
|
|
1689
2133
|
}
|
|
@@ -1845,6 +2289,24 @@ function previewImageTargetOf(payload) {
|
|
|
1845
2289
|
path
|
|
1846
2290
|
};
|
|
1847
2291
|
}
|
|
2292
|
+
/** One payload's optional `path` field; absent (or not a string) is undefined. */
|
|
2293
|
+
function pathFieldOf(payload) {
|
|
2294
|
+
if (typeof payload !== "object" || payload === null || Array.isArray(payload)) return void 0;
|
|
2295
|
+
const path = payload.path;
|
|
2296
|
+
return typeof path === "string" ? path : void 0;
|
|
2297
|
+
}
|
|
2298
|
+
/** Narrow a wire payload to one hand-added path. */
|
|
2299
|
+
function addTargetOf(payload) {
|
|
2300
|
+
const sessionId = sessionOf(payload);
|
|
2301
|
+
if (sessionId === void 0) return void 0;
|
|
2302
|
+
const path = pathFieldOf(payload)?.trim();
|
|
2303
|
+
if (path === void 0 || path === "") return void 0;
|
|
2304
|
+
return {
|
|
2305
|
+
sessionId,
|
|
2306
|
+
path,
|
|
2307
|
+
includeUnchanged: payload.includeUnchanged === true
|
|
2308
|
+
};
|
|
2309
|
+
}
|
|
1848
2310
|
/** Narrow a wire payload to one open target: the keep/revert pair plus the action. */
|
|
1849
2311
|
function openTargetOf(payload) {
|
|
1850
2312
|
const target = targetOf(payload);
|