dsh-vscode-mode 0.21.0 → 0.21.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 +5 -4
- package/lib/client.js +246 -16
- package/lib/client.js.map +2 -2
- package/lib/index.js +210 -1
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/missingReview.ts +40 -0
- package/src/client/ui/EditorView.ts +118 -14
- package/src/client/ui/SidecarDiffBar.tsx +4 -2
- package/src/missingReview.ts +127 -0
- package/src/rpc.ts +32 -6
- package/src/shared/rpc.ts +5 -1
- package/src/shared/types.ts +9 -0
package/lib/index.js
CHANGED
|
@@ -13114,6 +13114,147 @@ function reviewHandlers(ctx, registry) {
|
|
|
13114
13114
|
};
|
|
13115
13115
|
}
|
|
13116
13116
|
//#endregion
|
|
13117
|
+
//#region src/missingReview.ts
|
|
13118
|
+
/** Missing historical files: frozen, whole-file deletion reviews. */
|
|
13119
|
+
const frozen = /* @__PURE__ */ new Map();
|
|
13120
|
+
const locks = /* @__PURE__ */ new Set();
|
|
13121
|
+
/** @private @author ddj 2026年10月28号 @param target Resolved absolute path. @returns Platform file identity. */
|
|
13122
|
+
function fileKey(target) {
|
|
13123
|
+
const normalized = resolve(target).replace(/\\/g, "/");
|
|
13124
|
+
return process.platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
13125
|
+
}
|
|
13126
|
+
/** @private @author ddj 2026年10月28号 @param records Ordered records. @returns Full evidence and decision revision, excluding transient conflict flags. */
|
|
13127
|
+
function revisionOf(records) {
|
|
13128
|
+
const evidence = records.map(({ callId, path, at, after, hunks, decisions, archived, superseded }) => ({
|
|
13129
|
+
callId,
|
|
13130
|
+
path,
|
|
13131
|
+
at,
|
|
13132
|
+
after,
|
|
13133
|
+
hunks,
|
|
13134
|
+
decisions,
|
|
13135
|
+
archived,
|
|
13136
|
+
superseded
|
|
13137
|
+
}));
|
|
13138
|
+
return createHash("sha256").update(JSON.stringify(evidence)).digest("hex");
|
|
13139
|
+
}
|
|
13140
|
+
/** @private @author ddj 2026年10月28号 @param ctx Host capability. @param cwd Workspace. @param bucket Records. @param target Resolved file. @returns Records for the same physical path spelling, oldest first. */
|
|
13141
|
+
async function fileRecords(ctx, cwd, bucket, target) {
|
|
13142
|
+
const matches = [];
|
|
13143
|
+
const identity = fileKey(target);
|
|
13144
|
+
for (const record of bucket.values()) {
|
|
13145
|
+
if (record.superseded) continue;
|
|
13146
|
+
if (fileKey(await resolveTargetIn(ctx, cwd, record.path)) === identity) matches.push(record);
|
|
13147
|
+
}
|
|
13148
|
+
return matches.sort((left, right) => left.at.localeCompare(right.at));
|
|
13149
|
+
}
|
|
13150
|
+
/** @public @author ddj 2026年10月28号 @param ctx Host. @param cwd Workspace. @param path Requested file. @returns True only for confirmed absence; directories and IO failures are not missing files. */
|
|
13151
|
+
async function fileAbsent(ctx, cwd, path) {
|
|
13152
|
+
const fs = ctx.get("fs");
|
|
13153
|
+
if (!fs) throw new Error("缺少 fs");
|
|
13154
|
+
const target = await resolveTargetIn(ctx, cwd, path);
|
|
13155
|
+
try {
|
|
13156
|
+
return !await fs.stat(target);
|
|
13157
|
+
} catch (error) {
|
|
13158
|
+
if (["ENOENT", "FS_NOT_FOUND"].includes(String(error.code))) return true;
|
|
13159
|
+
throw error;
|
|
13160
|
+
}
|
|
13161
|
+
}
|
|
13162
|
+
/** @public @author ddj 2026年10月28号 @param ctx Host. @param session Live session. @param cwd Workspace. @param bucket Records. @param path Requested file. @returns Frozen complete snapshot; never constructs text from hunks or writes files. @throws Missing evidence or nonmissing target. */
|
|
13163
|
+
async function missingReview(ctx, session, cwd, bucket, path) {
|
|
13164
|
+
const target = await resolveTargetIn(ctx, cwd, path);
|
|
13165
|
+
if (hasOfficial(cwd, path) || hasOfficial(cwd, target)) throw new Error("该文件已有官方审查记录,请使用官方审查入口");
|
|
13166
|
+
if (!await fileAbsent(ctx, cwd, target)) throw new Error("文件已重新出现,请刷新");
|
|
13167
|
+
const records = await fileRecords(ctx, cwd, bucket, target);
|
|
13168
|
+
const latest = records.at(-1);
|
|
13169
|
+
if (!records.some((record) => !record.archived && !recordResolved(record))) throw new Error("没有待审历史记录");
|
|
13170
|
+
if (!latest || typeof latest.after !== "string") throw new Error("缺少可靠的完整历史快照,无法展示或恢复;不会用局部差异拼接文件");
|
|
13171
|
+
if (Buffer.byteLength(latest.after, "utf8") > 8388608) throw new Error("历史快照过大,无法整文件预览");
|
|
13172
|
+
for (const [token, item] of frozen) if (item.expires < Date.now()) frozen.delete(token);
|
|
13173
|
+
if (frozen.size >= 256) throw new Error("缺失文件预览过多,请稍后重试");
|
|
13174
|
+
const view = {
|
|
13175
|
+
token: randomUUID(),
|
|
13176
|
+
path,
|
|
13177
|
+
content: latest.after,
|
|
13178
|
+
callIds: records.filter((record) => !record.archived && !recordResolved(record)).map((record) => record.callId),
|
|
13179
|
+
at: latest.at
|
|
13180
|
+
};
|
|
13181
|
+
frozen.set(view.token, {
|
|
13182
|
+
view,
|
|
13183
|
+
cwd,
|
|
13184
|
+
session,
|
|
13185
|
+
revision: revisionOf(records),
|
|
13186
|
+
expires: Date.now() + 18e5
|
|
13187
|
+
});
|
|
13188
|
+
return view;
|
|
13189
|
+
}
|
|
13190
|
+
/** @private @author ddj 2026年10月28号 @param ctx Host. @param item Frozen evidence. @param target Resolved path. @param undo Restore instead of keeping absence. @sideEffects Atomically creates absent file, verifies exact bytes; retries never overwrite an existing file. */
|
|
13191
|
+
async function applyDisk(ctx, item, target, undo) {
|
|
13192
|
+
const fs = ctx.get("fs");
|
|
13193
|
+
const absent = await fileAbsent(ctx, item.cwd, target);
|
|
13194
|
+
if (!undo) {
|
|
13195
|
+
if (!absent) throw new Error("文件已重新出现,未执行 Keep");
|
|
13196
|
+
return;
|
|
13197
|
+
}
|
|
13198
|
+
const operation = item.operation;
|
|
13199
|
+
if (!absent && !operation.restored) throw new Error("文件已重新出现,未执行 Undo");
|
|
13200
|
+
if (absent && !operation.restored) {
|
|
13201
|
+
await fs.writeText(target, item.view.content, { kind: "createIfAbsent" }, void 0, policyOf(ctx, item.session));
|
|
13202
|
+
operation.restored = true;
|
|
13203
|
+
}
|
|
13204
|
+
if (absent && operation.restored && !await fs.stat(target)) throw new Error("恢复后的文件再次缺失,未结清记录");
|
|
13205
|
+
if (await fs.readText(target) !== item.view.content) throw new Error("恢复内容校验失败,未结清记录");
|
|
13206
|
+
}
|
|
13207
|
+
/** @public @author ddj 2026年10月28号 @param ctx Host. @param session Requesting session. @param cwd Workspace. @param bucket Records. @param token Frozen preview token. @param operationId Stable retry ID. @param undo True restores shown snapshot; false keeps deletion. @returns Covered call IDs. @throws Changed evidence, concurrent action or sandbox/IO failure. */
|
|
13208
|
+
async function missingDecide(ctx, session, cwd, bucket, token, operationId, undo) {
|
|
13209
|
+
const item = frozen.get(token);
|
|
13210
|
+
if (!item || item.expires < Date.now() || item.cwd !== cwd || item.session.id !== session.id) throw new Error("删除预览已过期,请刷新后重试");
|
|
13211
|
+
if (!operationId || operationId.length > 128) throw new Error("缺少有效操作 ID");
|
|
13212
|
+
const target = await resolveTargetIn(ctx, cwd, item.view.path);
|
|
13213
|
+
const key = JSON.stringify([cwd, fileKey(target)]);
|
|
13214
|
+
if (locks.has(key)) throw new Error("该文件正在处理,请稍后重试");
|
|
13215
|
+
locks.add(key);
|
|
13216
|
+
item.session = session;
|
|
13217
|
+
try {
|
|
13218
|
+
return await commitMissing(ctx, item, bucket, target, operationId, undo);
|
|
13219
|
+
} finally {
|
|
13220
|
+
locks.delete(key);
|
|
13221
|
+
}
|
|
13222
|
+
}
|
|
13223
|
+
/** @private @author ddj 2026年10月28号 @param ctx Host. @param item Frozen preview. @param bucket Live records. @param target Resolved target. @param operationId Retry identity. @param undo Restore action. @returns Archived pending call IDs; disk success survives bookkeeping failure in the same Host lifetime. */
|
|
13224
|
+
async function commitMissing(ctx, item, bucket, target, operationId, undo) {
|
|
13225
|
+
const previous = item.operation;
|
|
13226
|
+
if (previous && (previous.id !== operationId || previous.undo !== undo)) throw new Error("已有不同操作,请刷新后重试");
|
|
13227
|
+
if (previous?.done) return item.view.callIds;
|
|
13228
|
+
if (hasOfficial(item.cwd, target) || hasOfficial(item.cwd, item.view.path)) throw new Error("该文件已有官方审查记录,请刷新");
|
|
13229
|
+
if (revisionOf(await fileRecords(ctx, item.cwd, bucket, target)) !== item.revision) throw new Error("历史记录已变化,请刷新删除预览");
|
|
13230
|
+
item.operation ??= {
|
|
13231
|
+
id: operationId,
|
|
13232
|
+
undo,
|
|
13233
|
+
restored: false,
|
|
13234
|
+
done: false
|
|
13235
|
+
};
|
|
13236
|
+
await applyDisk(ctx, item, target, undo);
|
|
13237
|
+
const current = await fileRecords(ctx, item.cwd, bucket, target);
|
|
13238
|
+
if (revisionOf(current) !== item.revision) throw new Error("历史记录已变化,未结清记录,请刷新");
|
|
13239
|
+
const pending = current.filter((record) => item.view.callIds.includes(record.callId));
|
|
13240
|
+
const decisions = pending.map((record) => record.decisions);
|
|
13241
|
+
const decision = undo ? "rejected" : "accepted";
|
|
13242
|
+
for (const record of pending) record.decisions = {
|
|
13243
|
+
call: decision,
|
|
13244
|
+
perHunk: record.hunks.map(() => decision)
|
|
13245
|
+
};
|
|
13246
|
+
try {
|
|
13247
|
+
await archiveRecords(ctx, item.session, item.cwd, bucket, pending, undo ? "已恢复删除文件快照" : "已保留文件删除");
|
|
13248
|
+
} catch (error) {
|
|
13249
|
+
pending.forEach((record, index) => {
|
|
13250
|
+
record.decisions = decisions[index];
|
|
13251
|
+
});
|
|
13252
|
+
throw error;
|
|
13253
|
+
}
|
|
13254
|
+
item.operation.done = true;
|
|
13255
|
+
return item.view.callIds;
|
|
13256
|
+
}
|
|
13257
|
+
//#endregion
|
|
13117
13258
|
//#region src/rpc.ts
|
|
13118
13259
|
/** Process-local cursor signer; reads must stay on the Host filesystem capability. */
|
|
13119
13260
|
const readTextPage = createTextPages();
|
|
@@ -13202,8 +13343,13 @@ async function readTargetOf(ctx, sc, path) {
|
|
|
13202
13343
|
const target = await resolveTargetIn(ctx, sc.cwd, path);
|
|
13203
13344
|
debugRecord(ctx, sc.cwd, "[DEBUG path.resolve] input=" + String(path ?? "") + " resolved=" + fs.processPath(target), "debug");
|
|
13204
13345
|
const info = await fs.stat(target);
|
|
13205
|
-
if (!info
|
|
13346
|
+
if (!info) return {
|
|
13206
13347
|
err: "文件不存在",
|
|
13348
|
+
resolvedPath: fs.processPath(target),
|
|
13349
|
+
code: "file-missing"
|
|
13350
|
+
};
|
|
13351
|
+
if (info.type !== "file") return {
|
|
13352
|
+
err: "目标不是文件",
|
|
13207
13353
|
resolvedPath: fs.processPath(target)
|
|
13208
13354
|
};
|
|
13209
13355
|
if ((info.size ?? 0) > 33554432) return {
|
|
@@ -13216,6 +13362,10 @@ async function readTargetOf(ctx, sc, path) {
|
|
|
13216
13362
|
info
|
|
13217
13363
|
};
|
|
13218
13364
|
} catch (error) {
|
|
13365
|
+
if (["ENOENT", "FS_NOT_FOUND"].includes(String(error.code))) return {
|
|
13366
|
+
err: "文件不存在",
|
|
13367
|
+
code: "file-missing"
|
|
13368
|
+
};
|
|
13219
13369
|
return { err: "读取失败:" + String(error) };
|
|
13220
13370
|
}
|
|
13221
13371
|
}
|
|
@@ -13316,6 +13466,23 @@ async function applyDecisions(ctx, session, cwd, bucket, items) {
|
|
|
13316
13466
|
});
|
|
13317
13467
|
continue;
|
|
13318
13468
|
}
|
|
13469
|
+
try {
|
|
13470
|
+
if (await fileAbsent(ctx, cwd, record.path)) {
|
|
13471
|
+
results.push({
|
|
13472
|
+
callId: item.callId,
|
|
13473
|
+
ok: false,
|
|
13474
|
+
error: "文件已删除,请刷新后按整文件删除预览执行 Keep / Undo"
|
|
13475
|
+
});
|
|
13476
|
+
continue;
|
|
13477
|
+
}
|
|
13478
|
+
} catch (error) {
|
|
13479
|
+
results.push({
|
|
13480
|
+
callId: item.callId,
|
|
13481
|
+
ok: false,
|
|
13482
|
+
error: "无法确认文件状态:" + String(error)
|
|
13483
|
+
});
|
|
13484
|
+
continue;
|
|
13485
|
+
}
|
|
13319
13486
|
const alreadyDone = (item.scope === "hunk" ? record.decisions.perHunk[item.hunkIndex ?? -1] : record.decisions.call) === item.decision;
|
|
13320
13487
|
if (alreadyDone) {
|
|
13321
13488
|
if (recordResolved(record) && !record.archived) resolved.push(record);
|
|
@@ -13735,6 +13902,48 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
|
|
|
13735
13902
|
record: item.record
|
|
13736
13903
|
};
|
|
13737
13904
|
},
|
|
13905
|
+
"edrv.missingReview": async (args) => {
|
|
13906
|
+
const sc = await requireSession(ctx, args.sessionId);
|
|
13907
|
+
if ("err" in sc) return {
|
|
13908
|
+
ok: false,
|
|
13909
|
+
error: sc.err
|
|
13910
|
+
};
|
|
13911
|
+
try {
|
|
13912
|
+
const bucket = await bucketOf(registry, ctx, sc.cwd);
|
|
13913
|
+
if (!await fileAbsent(ctx, sc.cwd, args.path)) return {
|
|
13914
|
+
ok: true,
|
|
13915
|
+
review: null
|
|
13916
|
+
};
|
|
13917
|
+
return {
|
|
13918
|
+
ok: true,
|
|
13919
|
+
review: await missingReview(ctx, sc.session, sc.cwd, bucket, args.path)
|
|
13920
|
+
};
|
|
13921
|
+
} catch (error) {
|
|
13922
|
+
return {
|
|
13923
|
+
ok: false,
|
|
13924
|
+
error: String(error)
|
|
13925
|
+
};
|
|
13926
|
+
}
|
|
13927
|
+
},
|
|
13928
|
+
"edrv.missingDecide": async (args) => {
|
|
13929
|
+
const sc = await requireSession(ctx, args.sessionId);
|
|
13930
|
+
if ("err" in sc) return {
|
|
13931
|
+
ok: false,
|
|
13932
|
+
error: sc.err
|
|
13933
|
+
};
|
|
13934
|
+
try {
|
|
13935
|
+
const bucket = await bucketOf(registry, ctx, sc.cwd);
|
|
13936
|
+
return {
|
|
13937
|
+
ok: true,
|
|
13938
|
+
callIds: await missingDecide(ctx, sc.session, sc.cwd, bucket, args.token, args.operationId, args.undo === true)
|
|
13939
|
+
};
|
|
13940
|
+
} catch (error) {
|
|
13941
|
+
return {
|
|
13942
|
+
ok: false,
|
|
13943
|
+
error: String(error)
|
|
13944
|
+
};
|
|
13945
|
+
}
|
|
13946
|
+
},
|
|
13738
13947
|
"edrv.decideBatch": async (args) => {
|
|
13739
13948
|
const sc = await requireSession(ctx, args.sessionId);
|
|
13740
13949
|
if ("err" in sc) return {
|