dsh-vscode-mode 0.19.1 → 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/README.md +8 -4
- package/lib/client.js +733 -321
- package/lib/client.js.map +1 -1
- package/lib/index.js +535 -42
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/archiveDb.ts +8 -8
- package/src/capture.ts +2 -2
- package/src/client/sidebar/panels/FileExplorer.ts +34 -5
- package/src/client/state/diffSummary.ts +30 -0
- package/src/client/state/reviewFeed.ts +60 -0
- package/src/client/ui/ConversationDiffDock.ts +9 -10
- package/src/client/ui/DiffBadge.ts +8 -6
- package/src/client/ui/DiffBox.ts +18 -0
- package/src/client/ui/DiffLauncher.ts +21 -5
- package/src/client/ui/EditorView.ts +17 -5
- package/src/client/ui/OfficialReview.ts +93 -0
- package/src/index.ts +3 -0
- package/src/model.ts +7 -3
- package/src/officialCapture.ts +199 -0
- package/src/reviewDb.ts +214 -0
- package/src/reviewRpc.ts +33 -0
- package/src/rpc.ts +14 -18
- package/src/shared/review.ts +91 -0
- package/src/shared/rpc.ts +7 -0
- package/src/store.ts +2 -2
package/lib/client.js
CHANGED
|
@@ -1219,7 +1219,7 @@ window.__ModuleLoader__.load({
|
|
|
1219
1219
|
const listeners$4 = /* @__PURE__ */ new Set();
|
|
1220
1220
|
let version$1 = 0;
|
|
1221
1221
|
/** @private @author ddj 2026年10月14号 Notify every mounted window after an order change. */
|
|
1222
|
-
function notify() {
|
|
1222
|
+
function notify$1() {
|
|
1223
1223
|
version$1 += 1;
|
|
1224
1224
|
for (const listener of listeners$4) listener();
|
|
1225
1225
|
}
|
|
@@ -1240,14 +1240,14 @@ window.__ModuleLoader__.load({
|
|
|
1240
1240
|
if (index === windows.length - 1 && index >= 0) return;
|
|
1241
1241
|
if (index >= 0) windows.splice(index, 1);
|
|
1242
1242
|
windows.push(id);
|
|
1243
|
-
notify();
|
|
1243
|
+
notify$1();
|
|
1244
1244
|
}
|
|
1245
1245
|
/** @public @author ddj 2026年10月14号 @param id Unmounted window identity. */
|
|
1246
1246
|
function dropFloat(id) {
|
|
1247
1247
|
const index = windows.indexOf(id);
|
|
1248
1248
|
if (index < 0) return;
|
|
1249
1249
|
windows.splice(index, 1);
|
|
1250
|
-
notify();
|
|
1250
|
+
notify$1();
|
|
1251
1251
|
}
|
|
1252
1252
|
/** @public @author ddj 2026年10月14号 @param id Window identity. @returns Layer below menus/modals (190+). */
|
|
1253
1253
|
function floatLayer(id) {
|
|
@@ -3278,7 +3278,7 @@ window.__ModuleLoader__.load({
|
|
|
3278
3278
|
//#region src/client/state/recordsFeed.ts
|
|
3279
3279
|
/** 兜底轮询周期(毫秒):仅作事件丢失时的兜底,正常由 edrv:refresh 驱动。 */
|
|
3280
3280
|
const FEED_INTERVAL_MS = 5e3;
|
|
3281
|
-
const feeds = /* @__PURE__ */ new Map();
|
|
3281
|
+
const feeds$1 = /* @__PURE__ */ new Map();
|
|
3282
3282
|
/** 取全局 window(Node 测试环境无 window)。 */
|
|
3283
3283
|
function win() {
|
|
3284
3284
|
return typeof window === "undefined" ? void 0 : window;
|
|
@@ -3290,7 +3290,7 @@ window.__ModuleLoader__.load({
|
|
|
3290
3290
|
* @returns 订阅槽位
|
|
3291
3291
|
*/
|
|
3292
3292
|
function entryOf(sessionId) {
|
|
3293
|
-
let entry = feeds.get(sessionId);
|
|
3293
|
+
let entry = feeds$1.get(sessionId);
|
|
3294
3294
|
if (!entry) {
|
|
3295
3295
|
entry = {
|
|
3296
3296
|
records: null,
|
|
@@ -3302,7 +3302,7 @@ window.__ModuleLoader__.load({
|
|
|
3302
3302
|
seq: 0,
|
|
3303
3303
|
inflight: false
|
|
3304
3304
|
};
|
|
3305
|
-
feeds.set(sessionId, entry);
|
|
3305
|
+
feeds$1.set(sessionId, entry);
|
|
3306
3306
|
}
|
|
3307
3307
|
return entry;
|
|
3308
3308
|
}
|
|
@@ -3382,7 +3382,7 @@ window.__ModuleLoader__.load({
|
|
|
3382
3382
|
const target = win();
|
|
3383
3383
|
if (target) target.removeEventListener("edrv:refresh", entry.onRefresh);
|
|
3384
3384
|
}
|
|
3385
|
-
feeds.delete(sessionId);
|
|
3385
|
+
feeds$1.delete(sessionId);
|
|
3386
3386
|
}
|
|
3387
3387
|
/**
|
|
3388
3388
|
* 订阅会话的差异记录。
|
|
@@ -3436,6 +3436,415 @@ window.__ModuleLoader__.load({
|
|
|
3436
3436
|
};
|
|
3437
3437
|
}
|
|
3438
3438
|
//#endregion
|
|
3439
|
+
//#region src/client/state/reviewFeed.ts
|
|
3440
|
+
/** One official metadata request loop per viewed session, lazy historical patches. @author ddj 2026年10月26号 */
|
|
3441
|
+
const feeds = /* @__PURE__ */ new Map();
|
|
3442
|
+
/** @private @author ddj 2026年10月26号 @param entry Shared feed. Notify mounted observers only. */
|
|
3443
|
+
function notify(entry) {
|
|
3444
|
+
for (const listener of entry.listeners) listener(entry.value);
|
|
3445
|
+
}
|
|
3446
|
+
/** @private @author ddj 2026年10月26号 @param sessionId Scope. @param entry Current generation. @returns First-page metadata update; no overlap or late publication. */
|
|
3447
|
+
async function refresh(sessionId, entry) {
|
|
3448
|
+
if (entry.controller.signal.aborted) return;
|
|
3449
|
+
if (entry.loading) {
|
|
3450
|
+
entry.again = true;
|
|
3451
|
+
return;
|
|
3452
|
+
}
|
|
3453
|
+
clearTimeout(entry.timer);
|
|
3454
|
+
entry.loading = true;
|
|
3455
|
+
entry.again = false;
|
|
3456
|
+
try {
|
|
3457
|
+
const result = await rpc("edrv.review.list", { sessionId }, entry.controller.signal);
|
|
3458
|
+
if (!entry.controller.signal.aborted) entry.value = result.ok ? {
|
|
3459
|
+
page: result.page,
|
|
3460
|
+
error: result.page.overview ? "" : "Host 尚未提供官方文件导航,请重启加载新版插件;当前仅可浏览已返回的官方历史,角标不含官方记录。"
|
|
3461
|
+
} : {
|
|
3462
|
+
...entry.value,
|
|
3463
|
+
error: result.error
|
|
3464
|
+
};
|
|
3465
|
+
} catch (error) {
|
|
3466
|
+
if (!entry.controller.signal.aborted) entry.value = {
|
|
3467
|
+
...entry.value,
|
|
3468
|
+
error: String(error)
|
|
3469
|
+
};
|
|
3470
|
+
} finally {
|
|
3471
|
+
entry.loading = false;
|
|
3472
|
+
if (!entry.controller.signal.aborted) {
|
|
3473
|
+
notify(entry);
|
|
3474
|
+
if (entry.again) refresh(sessionId, entry);
|
|
3475
|
+
else entry.timer = setTimeout(() => {
|
|
3476
|
+
refresh(sessionId, entry);
|
|
3477
|
+
}, 5e3);
|
|
3478
|
+
}
|
|
3479
|
+
}
|
|
3480
|
+
}
|
|
3481
|
+
/** @public @author ddj 2026年10月26号 @param sessionId Live scope. @param listener Mounted observer. @returns Cleanup cancelling requests and timers when unused. */
|
|
3482
|
+
function subscribeReview(sessionId, listener) {
|
|
3483
|
+
let entry = feeds.get(sessionId);
|
|
3484
|
+
if (!entry) {
|
|
3485
|
+
/** @private @author ddj 2026年10月26号 Refresh the shared official metadata after edits/decisions. */
|
|
3486
|
+
const onRefresh = () => refreshReview(sessionId);
|
|
3487
|
+
entry = {
|
|
3488
|
+
listeners: /* @__PURE__ */ new Set(),
|
|
3489
|
+
value: {
|
|
3490
|
+
page: null,
|
|
3491
|
+
error: ""
|
|
3492
|
+
},
|
|
3493
|
+
controller: new AbortController(),
|
|
3494
|
+
loading: false,
|
|
3495
|
+
again: false,
|
|
3496
|
+
onRefresh
|
|
3497
|
+
};
|
|
3498
|
+
feeds.set(sessionId, entry);
|
|
3499
|
+
if (typeof window !== "undefined") window.addEventListener("edrv:refresh", onRefresh);
|
|
3500
|
+
refresh(sessionId, entry);
|
|
3501
|
+
}
|
|
3502
|
+
entry.listeners.add(listener);
|
|
3503
|
+
listener(entry.value);
|
|
3504
|
+
return () => {
|
|
3505
|
+
entry.listeners.delete(listener);
|
|
3506
|
+
if (entry.listeners.size) return;
|
|
3507
|
+
entry.controller.abort();
|
|
3508
|
+
clearTimeout(entry.timer);
|
|
3509
|
+
feeds.delete(sessionId);
|
|
3510
|
+
if (typeof window !== "undefined") window.removeEventListener("edrv:refresh", entry.onRefresh);
|
|
3511
|
+
};
|
|
3512
|
+
}
|
|
3513
|
+
/** @public @author ddj 2026年10月26号 @param sessionId Refresh metadata after a decision; does not alter legacy or disk. */
|
|
3514
|
+
function refreshReview(sessionId) {
|
|
3515
|
+
const entry = feeds.get(sessionId);
|
|
3516
|
+
if (entry) {
|
|
3517
|
+
clearTimeout(entry.timer);
|
|
3518
|
+
refresh(sessionId, entry);
|
|
3519
|
+
}
|
|
3520
|
+
}
|
|
3521
|
+
//#endregion
|
|
3522
|
+
//#region src/shared/diff.ts
|
|
3523
|
+
/** 统一取对应 hunk;callHunk 只用于旧记录缺失 hunks 时兼容。 */
|
|
3524
|
+
function preciseHunk(record, idx) {
|
|
3525
|
+
return hunkOf(record, idx);
|
|
3526
|
+
}
|
|
3527
|
+
/** 轻量、确定性的字符串指纹;不引入 Node crypto。 */
|
|
3528
|
+
function fingerprint(text) {
|
|
3529
|
+
if (typeof text !== "string") return null;
|
|
3530
|
+
let hash = 2166136261;
|
|
3531
|
+
for (let i = 0; i < text.length; i++) {
|
|
3532
|
+
hash ^= text.charCodeAt(i);
|
|
3533
|
+
hash = Math.imul(hash, 16777619);
|
|
3534
|
+
}
|
|
3535
|
+
return "fnv1a:" + (hash >>> 0).toString(16) + ":" + text.length;
|
|
3536
|
+
}
|
|
3537
|
+
/** 取记录第 idx 个有效 hunk;只有记录缺 hunk 时才回退 callHunk。 */
|
|
3538
|
+
function hunkOf(record, idx) {
|
|
3539
|
+
const hunk = Array.isArray(record.hunks) ? record.hunks[idx] : void 0;
|
|
3540
|
+
if (hunk && typeof hunk.newText === "string") return hunk;
|
|
3541
|
+
if (idx === 0 && record.callHunk && typeof record.callHunk.newText === "string") return record.callHunk;
|
|
3542
|
+
return null;
|
|
3543
|
+
}
|
|
3544
|
+
/**
|
|
3545
|
+
* 差异比较口径归一化:剥离 UTF-8 BOM、统一 CRLF 为 LF。
|
|
3546
|
+
* 背景:外部工具(pwsh/编辑器)可能改变 BOM/行尾,导致 after 指纹与 hunk
|
|
3547
|
+
* 定位在内容语义未变的情况下误报 conflict,把待确认修改从 UI 排除。
|
|
3548
|
+
* @author ddj 2026年09月09号
|
|
3549
|
+
*/
|
|
3550
|
+
function normalizeForCompare(text) {
|
|
3551
|
+
return text.replace(/^\uFEFF/, "").replace(/\r\n/g, "\n");
|
|
3552
|
+
}
|
|
3553
|
+
/** 归一化一个 hunk 的两侧文本(与 normalizeForCompare 同口径)。 */
|
|
3554
|
+
function normalizeHunk(hunk) {
|
|
3555
|
+
return {
|
|
3556
|
+
oldText: hunk.oldText === null ? null : normalizeForCompare(hunk.oldText),
|
|
3557
|
+
newText: normalizeForCompare(hunk.newText)
|
|
3558
|
+
};
|
|
3559
|
+
}
|
|
3560
|
+
/** splitLines 的空文本语义:真实空文件没有一行变更内容。 */
|
|
3561
|
+
function splitLines(text) {
|
|
3562
|
+
return text.length ? text.split("\n") : [];
|
|
3563
|
+
}
|
|
3564
|
+
/** 计算 index 前的 0 基行数。 */
|
|
3565
|
+
function lineBefore(text, index) {
|
|
3566
|
+
let n = 0;
|
|
3567
|
+
const end = Math.max(0, Math.min(index, text.length));
|
|
3568
|
+
for (let i = 0; i < end; i++) if (text.charCodeAt(i) === 10) n++;
|
|
3569
|
+
return n;
|
|
3570
|
+
}
|
|
3571
|
+
/**
|
|
3572
|
+
* 懒扫描候选:先找 cursor 后未占用项,找不到再回退至首个未占用项。
|
|
3573
|
+
* @author ddj 2026年09月29号
|
|
3574
|
+
* @param content 当前内容
|
|
3575
|
+
* @param needle 搜索串
|
|
3576
|
+
* @param cursor 前一块的结束位置
|
|
3577
|
+
* @param occupied 已选区间
|
|
3578
|
+
* @param state 此搜索串已扫描的候选
|
|
3579
|
+
* @returns 与完整候选列表双轮查找相同的匹配
|
|
3580
|
+
*/
|
|
3581
|
+
function findCandidate(content, needle, cursor, occupied, state) {
|
|
3582
|
+
if (!needle.length) return void 0;
|
|
3583
|
+
let fallback;
|
|
3584
|
+
for (let i = 0; i < state.ranges.length; i++) {
|
|
3585
|
+
const range = state.ranges[i];
|
|
3586
|
+
if (occupied.some((used) => overlaps$1(range, used))) continue;
|
|
3587
|
+
if (range.start >= cursor) return range;
|
|
3588
|
+
if (!fallback) fallback = range;
|
|
3589
|
+
}
|
|
3590
|
+
while (!state.done) {
|
|
3591
|
+
const at = content.indexOf(needle, state.next);
|
|
3592
|
+
if (at < 0) {
|
|
3593
|
+
state.done = true;
|
|
3594
|
+
break;
|
|
3595
|
+
}
|
|
3596
|
+
const range = {
|
|
3597
|
+
start: at,
|
|
3598
|
+
end: at + needle.length
|
|
3599
|
+
};
|
|
3600
|
+
state.ranges.push(range);
|
|
3601
|
+
state.next = range.end;
|
|
3602
|
+
if (occupied.some((used) => overlaps$1(range, used))) continue;
|
|
3603
|
+
if (range.start >= cursor) return range;
|
|
3604
|
+
if (!fallback) fallback = range;
|
|
3605
|
+
}
|
|
3606
|
+
return fallback;
|
|
3607
|
+
}
|
|
3608
|
+
/**
|
|
3609
|
+
* 从 after 快照定位所有 hunk,按 hunk 顺序优先选未占用且靠近前一块的候选。
|
|
3610
|
+
* 若 hunk 带 afterStart/afterEnd,则优先验证快照坐标,避免重复文本错配。
|
|
3611
|
+
* @author ddj 2026年08月24号
|
|
3612
|
+
* @param content 当前文件内容
|
|
3613
|
+
* @param hunks 待定位 hunk
|
|
3614
|
+
* @returns 每个 hunk 一个结果,matched=false 表示冲突/stale
|
|
3615
|
+
*/
|
|
3616
|
+
function locateHunks(content, hunks) {
|
|
3617
|
+
const locations = [];
|
|
3618
|
+
let cursor = 0;
|
|
3619
|
+
const occupied = [];
|
|
3620
|
+
const matches = /* @__PURE__ */ new Map();
|
|
3621
|
+
for (let idx = 0; idx < hunks.length; idx++) {
|
|
3622
|
+
const hunk = hunks[idx];
|
|
3623
|
+
const newText = hunk.newText;
|
|
3624
|
+
const snapshot = Number.isInteger(hunk.afterStart) && Number.isInteger(hunk.afterEnd) ? {
|
|
3625
|
+
start: hunk.afterStart,
|
|
3626
|
+
end: hunk.afterEnd
|
|
3627
|
+
} : null;
|
|
3628
|
+
const snapshotMatches = snapshot && snapshot.start >= 0 && snapshot.end >= snapshot.start && (newText.length === 0 || content.slice(snapshot.start, snapshot.end) === newText) ? snapshot : null;
|
|
3629
|
+
let candidate;
|
|
3630
|
+
if (snapshotMatches) {
|
|
3631
|
+
if (!occupied.some((used) => overlaps$1(snapshotMatches, used))) candidate = snapshotMatches;
|
|
3632
|
+
} else if (newText.length) {
|
|
3633
|
+
let state = matches.get(newText);
|
|
3634
|
+
if (!state) {
|
|
3635
|
+
state = {
|
|
3636
|
+
ranges: [],
|
|
3637
|
+
next: 0,
|
|
3638
|
+
done: false
|
|
3639
|
+
};
|
|
3640
|
+
matches.set(newText, state);
|
|
3641
|
+
}
|
|
3642
|
+
candidate = findCandidate(content, newText, cursor, occupied, state);
|
|
3643
|
+
}
|
|
3644
|
+
if (candidate) {
|
|
3645
|
+
locations.push({
|
|
3646
|
+
idx,
|
|
3647
|
+
hunk,
|
|
3648
|
+
...candidate,
|
|
3649
|
+
matched: true
|
|
3650
|
+
});
|
|
3651
|
+
occupied.push(candidate);
|
|
3652
|
+
cursor = Math.max(cursor, candidate.end);
|
|
3653
|
+
continue;
|
|
3654
|
+
}
|
|
3655
|
+
if (newText.length === 0 && hunk.afterStart === void 0 && hunk.afterEnd === void 0 && content.length === 0) {
|
|
3656
|
+
locations.push({
|
|
3657
|
+
idx,
|
|
3658
|
+
hunk,
|
|
3659
|
+
start: 0,
|
|
3660
|
+
end: 0,
|
|
3661
|
+
matched: true
|
|
3662
|
+
});
|
|
3663
|
+
continue;
|
|
3664
|
+
}
|
|
3665
|
+
if (newText.length === 0 && hunk.afterStart !== void 0 && hunk.afterEnd !== void 0 && hunk.afterStart === hunk.afterEnd && hunk.afterStart >= 0 && hunk.afterStart <= content.length) {
|
|
3666
|
+
const point = {
|
|
3667
|
+
start: hunk.afterStart,
|
|
3668
|
+
end: hunk.afterEnd
|
|
3669
|
+
};
|
|
3670
|
+
if (!occupied.some((used) => overlaps$1(point, used))) {
|
|
3671
|
+
locations.push({
|
|
3672
|
+
idx,
|
|
3673
|
+
hunk,
|
|
3674
|
+
...point,
|
|
3675
|
+
matched: true
|
|
3676
|
+
});
|
|
3677
|
+
occupied.push(point);
|
|
3678
|
+
cursor = Math.max(cursor, point.end);
|
|
3679
|
+
continue;
|
|
3680
|
+
}
|
|
3681
|
+
}
|
|
3682
|
+
locations.push({
|
|
3683
|
+
idx,
|
|
3684
|
+
hunk,
|
|
3685
|
+
start: -1,
|
|
3686
|
+
end: -1,
|
|
3687
|
+
matched: false
|
|
3688
|
+
});
|
|
3689
|
+
}
|
|
3690
|
+
return locations;
|
|
3691
|
+
}
|
|
3692
|
+
/** 判断两个半开区间是否重叠。零长度区间只与同一位置的零长度区间重叠。 */
|
|
3693
|
+
function overlaps$1(left, right) {
|
|
3694
|
+
if (left.start === left.end || right.start === right.end) return left.start === right.start && left.end === right.end;
|
|
3695
|
+
return left.start < right.end && right.start < left.end;
|
|
3696
|
+
}
|
|
3697
|
+
/** 在可定位区间上执行一次 hunk 反向替换。 */
|
|
3698
|
+
function replaceRange(content, location, reverse = false) {
|
|
3699
|
+
if (!location.matched || location.start < 0) return null;
|
|
3700
|
+
const replacement = reverse ? location.hunk.oldText ?? "" : location.hunk.newText;
|
|
3701
|
+
return content.slice(0, location.start) + replacement + content.slice(location.end);
|
|
3702
|
+
}
|
|
3703
|
+
/**
|
|
3704
|
+
* 按位置倒序收集片段,最后一次拼接;非互斥区间退回原替换语义。
|
|
3705
|
+
* @author ddj 2026年09月29号
|
|
3706
|
+
* @param content 当前文件内容
|
|
3707
|
+
* @param locations 已定位区间
|
|
3708
|
+
* @param reverse 是否反向应用
|
|
3709
|
+
* @returns 合成内容及失效块索引
|
|
3710
|
+
*/
|
|
3711
|
+
function applyLocations(content, locations, reverse = false) {
|
|
3712
|
+
const stale = locations.filter((location) => !location.matched).map((location) => location.idx);
|
|
3713
|
+
const matched = locations.filter((location) => location.matched).sort((a, b) => b.start - a.start);
|
|
3714
|
+
const parts = [];
|
|
3715
|
+
let end = content.length;
|
|
3716
|
+
for (const location of matched) {
|
|
3717
|
+
if (location.start < 0 || location.end > end) {
|
|
3718
|
+
let out = content;
|
|
3719
|
+
for (const item of matched) {
|
|
3720
|
+
const next = replaceRange(out, item, reverse);
|
|
3721
|
+
if (next === null) stale.push(item.idx);
|
|
3722
|
+
else out = next;
|
|
3723
|
+
}
|
|
3724
|
+
return {
|
|
3725
|
+
content: out,
|
|
3726
|
+
stale: stale.sort((a, b) => a - b)
|
|
3727
|
+
};
|
|
3728
|
+
}
|
|
3729
|
+
parts.push(content.slice(location.end, end), reverse ? location.hunk.oldText ?? "" : location.hunk.newText);
|
|
3730
|
+
end = location.start;
|
|
3731
|
+
}
|
|
3732
|
+
parts.push(content.slice(0, end));
|
|
3733
|
+
parts.reverse();
|
|
3734
|
+
return {
|
|
3735
|
+
content: parts.join(""),
|
|
3736
|
+
stale: stale.sort((a, b) => a - b)
|
|
3737
|
+
};
|
|
3738
|
+
}
|
|
3739
|
+
//#endregion
|
|
3740
|
+
//#region src/client/state/records.ts
|
|
3741
|
+
/** 差异决策状态。 */
|
|
3742
|
+
const ST = {
|
|
3743
|
+
PENDING: "pending",
|
|
3744
|
+
ACCEPTED: "accepted",
|
|
3745
|
+
REJECTED: "rejected"
|
|
3746
|
+
};
|
|
3747
|
+
/** hunk 的 DOM 定位 key(callId:idx)。 */
|
|
3748
|
+
function callIdAttr(callId, idx) {
|
|
3749
|
+
return String(callId) + ":" + String(idx);
|
|
3750
|
+
}
|
|
3751
|
+
/** 某 hunk 的当前决策状态(缺省 pending)。 */
|
|
3752
|
+
function statusAt(rec, idx) {
|
|
3753
|
+
if (!rec) return ST.PENDING;
|
|
3754
|
+
if (Array.isArray(rec.decisions?.perHunk) && rec.decisions.perHunk[idx] !== void 0) return rec.decisions.perHunk[idx];
|
|
3755
|
+
return rec.decisions?.call ?? ST.PENDING;
|
|
3756
|
+
}
|
|
3757
|
+
/** hunk 是否为空差异(old===new,无实际内容变化,不可操作)。create 记录不视为空差异。 */
|
|
3758
|
+
function noopHunk(rec, h) {
|
|
3759
|
+
if (!rec || rec.create === true) return false;
|
|
3760
|
+
const precise = rec && h && rec.hunks.indexOf(h) >= 0 ? preciseHunk(rec, rec.hunks.indexOf(h)) : h;
|
|
3761
|
+
const oldText = precise?.oldText ?? null;
|
|
3762
|
+
const newText = precise?.newText ?? null;
|
|
3763
|
+
return oldText !== null && oldText === newText;
|
|
3764
|
+
}
|
|
3765
|
+
/** 记录是否仍有待处理差异(superseded / 全部已决策 = false)。 */
|
|
3766
|
+
function isRecPending(rec) {
|
|
3767
|
+
if (!rec || rec.superseded === true) return false;
|
|
3768
|
+
const perHunk = Array.isArray(rec.decisions?.perHunk) ? rec.decisions.perHunk : [];
|
|
3769
|
+
if (perHunk.length) {
|
|
3770
|
+
for (let i = 0; i < perHunk.length; i++) if (perHunk[i] !== "accepted" && perHunk[i] !== "rejected" && !noopHunk(rec, (rec.hunks || [])[i])) return true;
|
|
3771
|
+
return false;
|
|
3772
|
+
}
|
|
3773
|
+
return rec.decisions.call === "pending" || rec.decisions.call === void 0;
|
|
3774
|
+
}
|
|
3775
|
+
/** 记录待处理差异"处"数(按 hunk,跳过空差异)。 */
|
|
3776
|
+
function pendingCount(recs) {
|
|
3777
|
+
let n = 0;
|
|
3778
|
+
for (const rec of recs) {
|
|
3779
|
+
if (!isRecPending(rec)) continue;
|
|
3780
|
+
const perHunk = Array.isArray(rec.decisions?.perHunk) ? rec.decisions.perHunk : [];
|
|
3781
|
+
if (perHunk.length) {
|
|
3782
|
+
for (let i = 0; i < perHunk.length; i++) if (perHunk[i] === "pending" && !noopHunk(rec, (rec.hunks || [])[i])) n++;
|
|
3783
|
+
} else {
|
|
3784
|
+
const hunks = Array.isArray(rec.hunks) ? rec.hunks : [];
|
|
3785
|
+
for (const h of hunks) if (!noopHunk(rec, h)) n++;
|
|
3786
|
+
if (!hunks.length && !noopHunk(rec, rec.callHunk)) n = Math.max(n, 1);
|
|
3787
|
+
}
|
|
3788
|
+
}
|
|
3789
|
+
return n;
|
|
3790
|
+
}
|
|
3791
|
+
/** 按文件分组的差异摘要(角标/Launcher 用)。 */
|
|
3792
|
+
function summarize(records) {
|
|
3793
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
3794
|
+
for (const r of records) {
|
|
3795
|
+
if (!byPath.has(r.path)) byPath.set(r.path, []);
|
|
3796
|
+
byPath.get(r.path).push(r);
|
|
3797
|
+
}
|
|
3798
|
+
const files = [];
|
|
3799
|
+
for (const [path, recs] of byPath) {
|
|
3800
|
+
const label = recs.find((r) => !!r.displayPath)?.displayPath ?? path;
|
|
3801
|
+
files.push({
|
|
3802
|
+
path,
|
|
3803
|
+
label,
|
|
3804
|
+
recs,
|
|
3805
|
+
pending: pendingCount(recs)
|
|
3806
|
+
});
|
|
3807
|
+
}
|
|
3808
|
+
files.sort((a, b) => a.path < b.path ? -1 : 1);
|
|
3809
|
+
const pendingFiles = files.filter((f) => f.pending > 0);
|
|
3810
|
+
return {
|
|
3811
|
+
files,
|
|
3812
|
+
pendingFiles,
|
|
3813
|
+
totalFiles: pendingFiles.length
|
|
3814
|
+
};
|
|
3815
|
+
}
|
|
3816
|
+
//#endregion
|
|
3817
|
+
//#region src/client/state/diffSummary.ts
|
|
3818
|
+
/** @public @author ddj 2026年10月26号 @param path Functional local or remote identity. @returns Comparison key without rewriting remote case. */
|
|
3819
|
+
function diffPathKey(path) {
|
|
3820
|
+
if (/^[a-z]+:\/\//i.test(path)) return path;
|
|
3821
|
+
const normalized = path.replace(/\\/g, "/");
|
|
3822
|
+
return /^[a-z]:\//i.test(normalized) ? normalized.toLowerCase() : normalized;
|
|
3823
|
+
}
|
|
3824
|
+
/** @public @author ddj 2026年10月26号 @param path Current editor file. @param page Authoritative metadata, including decided paths. @returns Whether legacy actions must be hidden for this file. */
|
|
3825
|
+
function officialPath(path, page) {
|
|
3826
|
+
const key = diffPathKey(path);
|
|
3827
|
+
return !!page?.overview?.coveredPaths.some((candidate) => diffPathKey(candidate) === key);
|
|
3828
|
+
}
|
|
3829
|
+
/** @public @author ddj 2026年10月26号 @param records Tool-call history. @param page Official file navigation, never only its first 50 rows. @returns Official-first file counts; legacy paths remain explicitly labelled. */
|
|
3830
|
+
function diffSummary(records, page) {
|
|
3831
|
+
const legacy = summarize(records.filter((record) => !officialPath(record.path, page)));
|
|
3832
|
+
const files = (page?.overview?.files ?? []).map((file) => ({
|
|
3833
|
+
...file,
|
|
3834
|
+
recs: []
|
|
3835
|
+
}));
|
|
3836
|
+
for (const file of legacy.files) files.push({
|
|
3837
|
+
...file,
|
|
3838
|
+
sourceKind: "legacy"
|
|
3839
|
+
});
|
|
3840
|
+
const pendingFiles = files.filter((file) => file.pending > 0);
|
|
3841
|
+
return {
|
|
3842
|
+
files,
|
|
3843
|
+
pendingFiles,
|
|
3844
|
+
totalFiles: pendingFiles.length
|
|
3845
|
+
};
|
|
3846
|
+
}
|
|
3847
|
+
//#endregion
|
|
3439
3848
|
//#region src/client/monaco/theme.ts
|
|
3440
3849
|
/**
|
|
3441
3850
|
* dsh-vscode-mode client — Monaco 主题:官方令牌跟随 + 现役双套回落。
|
|
@@ -6855,301 +7264,6 @@ window.__ModuleLoader__.load({
|
|
|
6855
7264
|
};
|
|
6856
7265
|
}
|
|
6857
7266
|
//#endregion
|
|
6858
|
-
//#region src/shared/diff.ts
|
|
6859
|
-
/** 统一取对应 hunk;callHunk 只用于旧记录缺失 hunks 时兼容。 */
|
|
6860
|
-
function preciseHunk(record, idx) {
|
|
6861
|
-
return hunkOf(record, idx);
|
|
6862
|
-
}
|
|
6863
|
-
/** 轻量、确定性的字符串指纹;不引入 Node crypto。 */
|
|
6864
|
-
function fingerprint(text) {
|
|
6865
|
-
if (typeof text !== "string") return null;
|
|
6866
|
-
let hash = 2166136261;
|
|
6867
|
-
for (let i = 0; i < text.length; i++) {
|
|
6868
|
-
hash ^= text.charCodeAt(i);
|
|
6869
|
-
hash = Math.imul(hash, 16777619);
|
|
6870
|
-
}
|
|
6871
|
-
return "fnv1a:" + (hash >>> 0).toString(16) + ":" + text.length;
|
|
6872
|
-
}
|
|
6873
|
-
/** 取记录第 idx 个有效 hunk;只有记录缺 hunk 时才回退 callHunk。 */
|
|
6874
|
-
function hunkOf(record, idx) {
|
|
6875
|
-
const hunk = Array.isArray(record.hunks) ? record.hunks[idx] : void 0;
|
|
6876
|
-
if (hunk && typeof hunk.newText === "string") return hunk;
|
|
6877
|
-
if (idx === 0 && record.callHunk && typeof record.callHunk.newText === "string") return record.callHunk;
|
|
6878
|
-
return null;
|
|
6879
|
-
}
|
|
6880
|
-
/**
|
|
6881
|
-
* 差异比较口径归一化:剥离 UTF-8 BOM、统一 CRLF 为 LF。
|
|
6882
|
-
* 背景:外部工具(pwsh/编辑器)可能改变 BOM/行尾,导致 after 指纹与 hunk
|
|
6883
|
-
* 定位在内容语义未变的情况下误报 conflict,把待确认修改从 UI 排除。
|
|
6884
|
-
* @author ddj 2026年09月09号
|
|
6885
|
-
*/
|
|
6886
|
-
function normalizeForCompare(text) {
|
|
6887
|
-
return text.replace(/^\uFEFF/, "").replace(/\r\n/g, "\n");
|
|
6888
|
-
}
|
|
6889
|
-
/** 归一化一个 hunk 的两侧文本(与 normalizeForCompare 同口径)。 */
|
|
6890
|
-
function normalizeHunk(hunk) {
|
|
6891
|
-
return {
|
|
6892
|
-
oldText: hunk.oldText === null ? null : normalizeForCompare(hunk.oldText),
|
|
6893
|
-
newText: normalizeForCompare(hunk.newText)
|
|
6894
|
-
};
|
|
6895
|
-
}
|
|
6896
|
-
/** splitLines 的空文本语义:真实空文件没有一行变更内容。 */
|
|
6897
|
-
function splitLines(text) {
|
|
6898
|
-
return text.length ? text.split("\n") : [];
|
|
6899
|
-
}
|
|
6900
|
-
/** 计算 index 前的 0 基行数。 */
|
|
6901
|
-
function lineBefore(text, index) {
|
|
6902
|
-
let n = 0;
|
|
6903
|
-
const end = Math.max(0, Math.min(index, text.length));
|
|
6904
|
-
for (let i = 0; i < end; i++) if (text.charCodeAt(i) === 10) n++;
|
|
6905
|
-
return n;
|
|
6906
|
-
}
|
|
6907
|
-
/**
|
|
6908
|
-
* 懒扫描候选:先找 cursor 后未占用项,找不到再回退至首个未占用项。
|
|
6909
|
-
* @author ddj 2026年09月29号
|
|
6910
|
-
* @param content 当前内容
|
|
6911
|
-
* @param needle 搜索串
|
|
6912
|
-
* @param cursor 前一块的结束位置
|
|
6913
|
-
* @param occupied 已选区间
|
|
6914
|
-
* @param state 此搜索串已扫描的候选
|
|
6915
|
-
* @returns 与完整候选列表双轮查找相同的匹配
|
|
6916
|
-
*/
|
|
6917
|
-
function findCandidate(content, needle, cursor, occupied, state) {
|
|
6918
|
-
if (!needle.length) return void 0;
|
|
6919
|
-
let fallback;
|
|
6920
|
-
for (let i = 0; i < state.ranges.length; i++) {
|
|
6921
|
-
const range = state.ranges[i];
|
|
6922
|
-
if (occupied.some((used) => overlaps$1(range, used))) continue;
|
|
6923
|
-
if (range.start >= cursor) return range;
|
|
6924
|
-
if (!fallback) fallback = range;
|
|
6925
|
-
}
|
|
6926
|
-
while (!state.done) {
|
|
6927
|
-
const at = content.indexOf(needle, state.next);
|
|
6928
|
-
if (at < 0) {
|
|
6929
|
-
state.done = true;
|
|
6930
|
-
break;
|
|
6931
|
-
}
|
|
6932
|
-
const range = {
|
|
6933
|
-
start: at,
|
|
6934
|
-
end: at + needle.length
|
|
6935
|
-
};
|
|
6936
|
-
state.ranges.push(range);
|
|
6937
|
-
state.next = range.end;
|
|
6938
|
-
if (occupied.some((used) => overlaps$1(range, used))) continue;
|
|
6939
|
-
if (range.start >= cursor) return range;
|
|
6940
|
-
if (!fallback) fallback = range;
|
|
6941
|
-
}
|
|
6942
|
-
return fallback;
|
|
6943
|
-
}
|
|
6944
|
-
/**
|
|
6945
|
-
* 从 after 快照定位所有 hunk,按 hunk 顺序优先选未占用且靠近前一块的候选。
|
|
6946
|
-
* 若 hunk 带 afterStart/afterEnd,则优先验证快照坐标,避免重复文本错配。
|
|
6947
|
-
* @author ddj 2026年08月24号
|
|
6948
|
-
* @param content 当前文件内容
|
|
6949
|
-
* @param hunks 待定位 hunk
|
|
6950
|
-
* @returns 每个 hunk 一个结果,matched=false 表示冲突/stale
|
|
6951
|
-
*/
|
|
6952
|
-
function locateHunks(content, hunks) {
|
|
6953
|
-
const locations = [];
|
|
6954
|
-
let cursor = 0;
|
|
6955
|
-
const occupied = [];
|
|
6956
|
-
const matches = /* @__PURE__ */ new Map();
|
|
6957
|
-
for (let idx = 0; idx < hunks.length; idx++) {
|
|
6958
|
-
const hunk = hunks[idx];
|
|
6959
|
-
const newText = hunk.newText;
|
|
6960
|
-
const snapshot = Number.isInteger(hunk.afterStart) && Number.isInteger(hunk.afterEnd) ? {
|
|
6961
|
-
start: hunk.afterStart,
|
|
6962
|
-
end: hunk.afterEnd
|
|
6963
|
-
} : null;
|
|
6964
|
-
const snapshotMatches = snapshot && snapshot.start >= 0 && snapshot.end >= snapshot.start && (newText.length === 0 || content.slice(snapshot.start, snapshot.end) === newText) ? snapshot : null;
|
|
6965
|
-
let candidate;
|
|
6966
|
-
if (snapshotMatches) {
|
|
6967
|
-
if (!occupied.some((used) => overlaps$1(snapshotMatches, used))) candidate = snapshotMatches;
|
|
6968
|
-
} else if (newText.length) {
|
|
6969
|
-
let state = matches.get(newText);
|
|
6970
|
-
if (!state) {
|
|
6971
|
-
state = {
|
|
6972
|
-
ranges: [],
|
|
6973
|
-
next: 0,
|
|
6974
|
-
done: false
|
|
6975
|
-
};
|
|
6976
|
-
matches.set(newText, state);
|
|
6977
|
-
}
|
|
6978
|
-
candidate = findCandidate(content, newText, cursor, occupied, state);
|
|
6979
|
-
}
|
|
6980
|
-
if (candidate) {
|
|
6981
|
-
locations.push({
|
|
6982
|
-
idx,
|
|
6983
|
-
hunk,
|
|
6984
|
-
...candidate,
|
|
6985
|
-
matched: true
|
|
6986
|
-
});
|
|
6987
|
-
occupied.push(candidate);
|
|
6988
|
-
cursor = Math.max(cursor, candidate.end);
|
|
6989
|
-
continue;
|
|
6990
|
-
}
|
|
6991
|
-
if (newText.length === 0 && hunk.afterStart === void 0 && hunk.afterEnd === void 0 && content.length === 0) {
|
|
6992
|
-
locations.push({
|
|
6993
|
-
idx,
|
|
6994
|
-
hunk,
|
|
6995
|
-
start: 0,
|
|
6996
|
-
end: 0,
|
|
6997
|
-
matched: true
|
|
6998
|
-
});
|
|
6999
|
-
continue;
|
|
7000
|
-
}
|
|
7001
|
-
if (newText.length === 0 && hunk.afterStart !== void 0 && hunk.afterEnd !== void 0 && hunk.afterStart === hunk.afterEnd && hunk.afterStart >= 0 && hunk.afterStart <= content.length) {
|
|
7002
|
-
const point = {
|
|
7003
|
-
start: hunk.afterStart,
|
|
7004
|
-
end: hunk.afterEnd
|
|
7005
|
-
};
|
|
7006
|
-
if (!occupied.some((used) => overlaps$1(point, used))) {
|
|
7007
|
-
locations.push({
|
|
7008
|
-
idx,
|
|
7009
|
-
hunk,
|
|
7010
|
-
...point,
|
|
7011
|
-
matched: true
|
|
7012
|
-
});
|
|
7013
|
-
occupied.push(point);
|
|
7014
|
-
cursor = Math.max(cursor, point.end);
|
|
7015
|
-
continue;
|
|
7016
|
-
}
|
|
7017
|
-
}
|
|
7018
|
-
locations.push({
|
|
7019
|
-
idx,
|
|
7020
|
-
hunk,
|
|
7021
|
-
start: -1,
|
|
7022
|
-
end: -1,
|
|
7023
|
-
matched: false
|
|
7024
|
-
});
|
|
7025
|
-
}
|
|
7026
|
-
return locations;
|
|
7027
|
-
}
|
|
7028
|
-
/** 判断两个半开区间是否重叠。零长度区间只与同一位置的零长度区间重叠。 */
|
|
7029
|
-
function overlaps$1(left, right) {
|
|
7030
|
-
if (left.start === left.end || right.start === right.end) return left.start === right.start && left.end === right.end;
|
|
7031
|
-
return left.start < right.end && right.start < left.end;
|
|
7032
|
-
}
|
|
7033
|
-
/** 在可定位区间上执行一次 hunk 反向替换。 */
|
|
7034
|
-
function replaceRange(content, location, reverse = false) {
|
|
7035
|
-
if (!location.matched || location.start < 0) return null;
|
|
7036
|
-
const replacement = reverse ? location.hunk.oldText ?? "" : location.hunk.newText;
|
|
7037
|
-
return content.slice(0, location.start) + replacement + content.slice(location.end);
|
|
7038
|
-
}
|
|
7039
|
-
/**
|
|
7040
|
-
* 按位置倒序收集片段,最后一次拼接;非互斥区间退回原替换语义。
|
|
7041
|
-
* @author ddj 2026年09月29号
|
|
7042
|
-
* @param content 当前文件内容
|
|
7043
|
-
* @param locations 已定位区间
|
|
7044
|
-
* @param reverse 是否反向应用
|
|
7045
|
-
* @returns 合成内容及失效块索引
|
|
7046
|
-
*/
|
|
7047
|
-
function applyLocations(content, locations, reverse = false) {
|
|
7048
|
-
const stale = locations.filter((location) => !location.matched).map((location) => location.idx);
|
|
7049
|
-
const matched = locations.filter((location) => location.matched).sort((a, b) => b.start - a.start);
|
|
7050
|
-
const parts = [];
|
|
7051
|
-
let end = content.length;
|
|
7052
|
-
for (const location of matched) {
|
|
7053
|
-
if (location.start < 0 || location.end > end) {
|
|
7054
|
-
let out = content;
|
|
7055
|
-
for (const item of matched) {
|
|
7056
|
-
const next = replaceRange(out, item, reverse);
|
|
7057
|
-
if (next === null) stale.push(item.idx);
|
|
7058
|
-
else out = next;
|
|
7059
|
-
}
|
|
7060
|
-
return {
|
|
7061
|
-
content: out,
|
|
7062
|
-
stale: stale.sort((a, b) => a - b)
|
|
7063
|
-
};
|
|
7064
|
-
}
|
|
7065
|
-
parts.push(content.slice(location.end, end), reverse ? location.hunk.oldText ?? "" : location.hunk.newText);
|
|
7066
|
-
end = location.start;
|
|
7067
|
-
}
|
|
7068
|
-
parts.push(content.slice(0, end));
|
|
7069
|
-
parts.reverse();
|
|
7070
|
-
return {
|
|
7071
|
-
content: parts.join(""),
|
|
7072
|
-
stale: stale.sort((a, b) => a - b)
|
|
7073
|
-
};
|
|
7074
|
-
}
|
|
7075
|
-
//#endregion
|
|
7076
|
-
//#region src/client/state/records.ts
|
|
7077
|
-
/** 差异决策状态。 */
|
|
7078
|
-
const ST = {
|
|
7079
|
-
PENDING: "pending",
|
|
7080
|
-
ACCEPTED: "accepted",
|
|
7081
|
-
REJECTED: "rejected"
|
|
7082
|
-
};
|
|
7083
|
-
/** hunk 的 DOM 定位 key(callId:idx)。 */
|
|
7084
|
-
function callIdAttr(callId, idx) {
|
|
7085
|
-
return String(callId) + ":" + String(idx);
|
|
7086
|
-
}
|
|
7087
|
-
/** 某 hunk 的当前决策状态(缺省 pending)。 */
|
|
7088
|
-
function statusAt(rec, idx) {
|
|
7089
|
-
if (!rec) return ST.PENDING;
|
|
7090
|
-
if (Array.isArray(rec.decisions?.perHunk) && rec.decisions.perHunk[idx] !== void 0) return rec.decisions.perHunk[idx];
|
|
7091
|
-
return rec.decisions?.call ?? ST.PENDING;
|
|
7092
|
-
}
|
|
7093
|
-
/** hunk 是否为空差异(old===new,无实际内容变化,不可操作)。create 记录不视为空差异。 */
|
|
7094
|
-
function noopHunk(rec, h) {
|
|
7095
|
-
if (!rec || rec.create === true) return false;
|
|
7096
|
-
const precise = rec && h && rec.hunks.indexOf(h) >= 0 ? preciseHunk(rec, rec.hunks.indexOf(h)) : h;
|
|
7097
|
-
const oldText = precise?.oldText ?? null;
|
|
7098
|
-
const newText = precise?.newText ?? null;
|
|
7099
|
-
return oldText !== null && oldText === newText;
|
|
7100
|
-
}
|
|
7101
|
-
/** 记录是否仍有待处理差异(superseded / 全部已决策 = false)。 */
|
|
7102
|
-
function isRecPending(rec) {
|
|
7103
|
-
if (!rec || rec.superseded === true) return false;
|
|
7104
|
-
const perHunk = Array.isArray(rec.decisions?.perHunk) ? rec.decisions.perHunk : [];
|
|
7105
|
-
if (perHunk.length) {
|
|
7106
|
-
for (let i = 0; i < perHunk.length; i++) if (perHunk[i] !== "accepted" && perHunk[i] !== "rejected" && !noopHunk(rec, (rec.hunks || [])[i])) return true;
|
|
7107
|
-
return false;
|
|
7108
|
-
}
|
|
7109
|
-
return rec.decisions.call === "pending" || rec.decisions.call === void 0;
|
|
7110
|
-
}
|
|
7111
|
-
/** 记录待处理差异"处"数(按 hunk,跳过空差异)。 */
|
|
7112
|
-
function pendingCount(recs) {
|
|
7113
|
-
let n = 0;
|
|
7114
|
-
for (const rec of recs) {
|
|
7115
|
-
if (!isRecPending(rec)) continue;
|
|
7116
|
-
const perHunk = Array.isArray(rec.decisions?.perHunk) ? rec.decisions.perHunk : [];
|
|
7117
|
-
if (perHunk.length) {
|
|
7118
|
-
for (let i = 0; i < perHunk.length; i++) if (perHunk[i] === "pending" && !noopHunk(rec, (rec.hunks || [])[i])) n++;
|
|
7119
|
-
} else {
|
|
7120
|
-
const hunks = Array.isArray(rec.hunks) ? rec.hunks : [];
|
|
7121
|
-
for (const h of hunks) if (!noopHunk(rec, h)) n++;
|
|
7122
|
-
if (!hunks.length && !noopHunk(rec, rec.callHunk)) n = Math.max(n, 1);
|
|
7123
|
-
}
|
|
7124
|
-
}
|
|
7125
|
-
return n;
|
|
7126
|
-
}
|
|
7127
|
-
/** 按文件分组的差异摘要(角标/Launcher 用)。 */
|
|
7128
|
-
function summarize(records) {
|
|
7129
|
-
const byPath = /* @__PURE__ */ new Map();
|
|
7130
|
-
for (const r of records) {
|
|
7131
|
-
if (!byPath.has(r.path)) byPath.set(r.path, []);
|
|
7132
|
-
byPath.get(r.path).push(r);
|
|
7133
|
-
}
|
|
7134
|
-
const files = [];
|
|
7135
|
-
for (const [path, recs] of byPath) {
|
|
7136
|
-
const label = recs.find((r) => !!r.displayPath)?.displayPath ?? path;
|
|
7137
|
-
files.push({
|
|
7138
|
-
path,
|
|
7139
|
-
label,
|
|
7140
|
-
recs,
|
|
7141
|
-
pending: pendingCount(recs)
|
|
7142
|
-
});
|
|
7143
|
-
}
|
|
7144
|
-
files.sort((a, b) => a.path < b.path ? -1 : 1);
|
|
7145
|
-
const pendingFiles = files.filter((f) => f.pending > 0);
|
|
7146
|
-
return {
|
|
7147
|
-
files,
|
|
7148
|
-
pendingFiles,
|
|
7149
|
-
totalFiles: pendingFiles.length
|
|
7150
|
-
};
|
|
7151
|
-
}
|
|
7152
|
-
//#endregion
|
|
7153
7267
|
//#region src/client/state/regions.ts
|
|
7154
7268
|
/**
|
|
7155
7269
|
* 在预计算的换行位置中找出 offset 前的行数。
|
|
@@ -10848,6 +10962,169 @@ window.__ModuleLoader__.load({
|
|
|
10848
10962
|
lineHeight: 1.55
|
|
10849
10963
|
};
|
|
10850
10964
|
//#endregion
|
|
10965
|
+
//#region src/client/ui/OfficialReview.ts
|
|
10966
|
+
/** Official immutable patch ledger; current files use the existing editor. @author ddj 2026年10月26号 */
|
|
10967
|
+
const h = react.default.createElement;
|
|
10968
|
+
/** @private @author ddj 2026年10月26号 @param props Latest first-page metadata. @returns Paginated rows independent of expanded historical patch. */
|
|
10969
|
+
function ReviewLedger(props) {
|
|
10970
|
+
const [page, setPage] = react.default.useState(props.first);
|
|
10971
|
+
const [history, setHistory] = react.default.useState(props.history ?? false);
|
|
10972
|
+
const [cursor, setCursor] = react.default.useState(0);
|
|
10973
|
+
const [detail, setDetail] = react.default.useState(null);
|
|
10974
|
+
const [error, setError] = react.default.useState("");
|
|
10975
|
+
const selected = react.default.useRef(null);
|
|
10976
|
+
react.default.useEffect(() => () => selected.current?.abort(), []);
|
|
10977
|
+
react.default.useEffect(() => {
|
|
10978
|
+
const controller = new AbortController();
|
|
10979
|
+
if (!history && !props.path && cursor === 0) {
|
|
10980
|
+
setPage(props.first);
|
|
10981
|
+
return () => controller.abort();
|
|
10982
|
+
}
|
|
10983
|
+
rpc("edrv.review.list", {
|
|
10984
|
+
sessionId: props.sessionId,
|
|
10985
|
+
history,
|
|
10986
|
+
cursor,
|
|
10987
|
+
path: props.path
|
|
10988
|
+
}, controller.signal).then((result) => {
|
|
10989
|
+
if (controller.signal.aborted) return;
|
|
10990
|
+
if (result.ok) {
|
|
10991
|
+
setPage(result.page);
|
|
10992
|
+
setError("");
|
|
10993
|
+
} else setError(result.error);
|
|
10994
|
+
}).catch((cause) => {
|
|
10995
|
+
if (!controller.signal.aborted) setError(String(cause));
|
|
10996
|
+
});
|
|
10997
|
+
return () => controller.abort();
|
|
10998
|
+
}, [
|
|
10999
|
+
props.sessionId,
|
|
11000
|
+
props.first,
|
|
11001
|
+
props.path,
|
|
11002
|
+
cursor,
|
|
11003
|
+
history
|
|
11004
|
+
]);
|
|
11005
|
+
/** @private @author ddj 2026年10月26号 @param item Frozen version identity; patch is never built from disk. */
|
|
11006
|
+
async function select(item) {
|
|
11007
|
+
selected.current?.abort();
|
|
11008
|
+
const controller = new AbortController();
|
|
11009
|
+
selected.current = controller;
|
|
11010
|
+
try {
|
|
11011
|
+
const result = await rpc("edrv.review.detail", {
|
|
11012
|
+
sessionId: props.sessionId,
|
|
11013
|
+
id: item.id
|
|
11014
|
+
}, controller.signal);
|
|
11015
|
+
if (controller.signal.aborted) return;
|
|
11016
|
+
if (result.ok) {
|
|
11017
|
+
setDetail(result.detail);
|
|
11018
|
+
setError("");
|
|
11019
|
+
} else setError(result.error);
|
|
11020
|
+
} catch (cause) {
|
|
11021
|
+
if (!controller.signal.aborted) setError(String(cause));
|
|
11022
|
+
}
|
|
11023
|
+
}
|
|
11024
|
+
return h("div", { style: { marginTop: 8 } }, h("label", null, h("input", {
|
|
11025
|
+
type: "checkbox",
|
|
11026
|
+
checked: history,
|
|
11027
|
+
onChange: () => {
|
|
11028
|
+
setHistory(!history);
|
|
11029
|
+
setCursor(0);
|
|
11030
|
+
}
|
|
11031
|
+
}), "显示已处理及被替换的历史"), page?.gaps.map((gap) => h("div", {
|
|
11032
|
+
key: `${gap.sessionId}:${gap.seq}`,
|
|
11033
|
+
role: "status"
|
|
11034
|
+
}, `轮次 ${gap.turn} 采集不完整:${gap.error}`)), error ? h("div", { role: "alert" }, error) : null, !page ? h("p", { role: "status" }, "正在读取官方审查记录…") : null, page && !page.items.length ? h("p", null, history ? "暂无已落盘历史。" : "没有待处理的官方记录;可切换查看历史。") : null, h("div", { style: {
|
|
11035
|
+
maxHeight: 210,
|
|
11036
|
+
overflow: "auto"
|
|
11037
|
+
} }, page?.items.map((item) => h("button", {
|
|
11038
|
+
key: item.id,
|
|
11039
|
+
type: "button",
|
|
11040
|
+
onClick: () => {
|
|
11041
|
+
select(item);
|
|
11042
|
+
},
|
|
11043
|
+
style: {
|
|
11044
|
+
display: "block",
|
|
11045
|
+
width: "100%",
|
|
11046
|
+
textAlign: "left",
|
|
11047
|
+
padding: "6px 4px"
|
|
11048
|
+
}
|
|
11049
|
+
}, `轮次 ${item.turn} · ${item.display} +${item.added} −${item.deleted} · ${item.decision}${item.current ? "" : "(已被同轮新版本替换)"}`))), cursor > 0 ? h("button", {
|
|
11050
|
+
type: "button",
|
|
11051
|
+
onClick: () => setCursor(Math.max(0, cursor - 50))
|
|
11052
|
+
}, "上一页") : null, page?.nextCursor !== null && page?.nextCursor !== void 0 ? h("button", {
|
|
11053
|
+
type: "button",
|
|
11054
|
+
onClick: () => setCursor(page.nextCursor)
|
|
11055
|
+
}, "下一页") : null, detail ? h(PatchDetail, {
|
|
11056
|
+
key: detail.item.id,
|
|
11057
|
+
sessionId: props.sessionId,
|
|
11058
|
+
detail,
|
|
11059
|
+
onChanged: () => {
|
|
11060
|
+
setDetail(null);
|
|
11061
|
+
refreshReview(props.sessionId);
|
|
11062
|
+
}
|
|
11063
|
+
}) : null);
|
|
11064
|
+
}
|
|
11065
|
+
/** @private @author ddj 2026年10月26号 @param props Immutable patch and current review revision. @returns Read-only source with explicit capability gates. */
|
|
11066
|
+
function PatchDetail(props) {
|
|
11067
|
+
const [error, setError] = react.default.useState("");
|
|
11068
|
+
const [busy, setBusy] = react.default.useState(false);
|
|
11069
|
+
const [operationId] = react.default.useState(() => crypto.randomUUID());
|
|
11070
|
+
const lifetime = react.default.useRef(null);
|
|
11071
|
+
react.default.useEffect(() => {
|
|
11072
|
+
lifetime.current = new AbortController();
|
|
11073
|
+
return () => lifetime.current?.abort();
|
|
11074
|
+
}, []);
|
|
11075
|
+
const { item, diff, capability } = props.detail;
|
|
11076
|
+
/** @private @author ddj 2026年10月26号 Marks reviewed only; never writes disk. Stable operation id survives retries while mounted. */
|
|
11077
|
+
async function keep() {
|
|
11078
|
+
setBusy(true);
|
|
11079
|
+
try {
|
|
11080
|
+
const result = await rpc("edrv.review.decide", {
|
|
11081
|
+
sessionId: props.sessionId,
|
|
11082
|
+
id: item.id,
|
|
11083
|
+
seq: item.seq,
|
|
11084
|
+
revision: item.revision,
|
|
11085
|
+
operationId,
|
|
11086
|
+
decision: "accepted"
|
|
11087
|
+
}, lifetime.current?.signal);
|
|
11088
|
+
if (lifetime.current?.signal.aborted) return;
|
|
11089
|
+
if (result.ok) props.onChanged();
|
|
11090
|
+
else setError(result.error);
|
|
11091
|
+
} catch (cause) {
|
|
11092
|
+
if (!lifetime.current?.signal.aborted) setError(String(cause));
|
|
11093
|
+
} finally {
|
|
11094
|
+
if (!lifetime.current?.signal.aborted) setBusy(false);
|
|
11095
|
+
}
|
|
11096
|
+
}
|
|
11097
|
+
return h("article", { style: {
|
|
11098
|
+
marginTop: 10,
|
|
11099
|
+
borderTop: "1px solid var(--border-color, #d9dee7)",
|
|
11100
|
+
paddingTop: 8
|
|
11101
|
+
} }, h("strong", null, `${item.display} · 轮次 ${item.turn} / 事件 ${item.seq}`), h("div", null, `来源会话 ${item.sessionId} · 采集于 ${props.detail.capturedAt}`), h("p", null, capability.reason), diff.kind === "text" ? h("div", null, h("div", null, `${diff.before ? "原文件存在" : "原文件不存在"} / ${diff.after ? "结果文件存在" : "结果文件不存在"}${diff.coarse ? " · 粗粒度比较" : ""}`), h("pre", {
|
|
11102
|
+
style: {
|
|
11103
|
+
overflow: "auto",
|
|
11104
|
+
maxHeight: 400,
|
|
11105
|
+
fontSize: 11,
|
|
11106
|
+
lineHeight: 1.6
|
|
11107
|
+
},
|
|
11108
|
+
"aria-label": "官方历史差异(只读)"
|
|
11109
|
+
}, diff.hunks.map((chunk, index) => h("div", { key: index }, h("div", null, `@@ -${chunk.oldStart},${chunk.oldLines} +${chunk.newStart},${chunk.newLines} @@`), chunk.lines.map((line, position) => h("div", {
|
|
11110
|
+
key: position,
|
|
11111
|
+
style: { background: line.startsWith("+") ? "light-dark(#e5f5e8, #173522)" : line.startsWith("-") ? "light-dark(#fce9e9, #452323)" : void 0 }
|
|
11112
|
+
}, line)))))) : h("p", null, diff.kind === "binary" ? "二进制文件:官方不提供文本差异。" : "文件超过官方大小限制:没有差异内容。"), error ? h("div", { role: "alert" }, error) : null, h("button", {
|
|
11113
|
+
type: "button",
|
|
11114
|
+
disabled: busy || !capability.keep,
|
|
11115
|
+
onClick: () => {
|
|
11116
|
+
keep();
|
|
11117
|
+
}
|
|
11118
|
+
}, "标记已审(不改文件)"), h("button", {
|
|
11119
|
+
type: "button",
|
|
11120
|
+
disabled: true,
|
|
11121
|
+
title: capability.reason
|
|
11122
|
+
}, "拒绝写回不可用"), h("button", {
|
|
11123
|
+
type: "button",
|
|
11124
|
+
onClick: () => openEditorView(item.currentPath)
|
|
11125
|
+
}, "编辑当前文件"));
|
|
11126
|
+
}
|
|
11127
|
+
//#endregion
|
|
10851
11128
|
//#region src/client/ui/DiffLauncher.ts
|
|
10852
11129
|
/**
|
|
10853
11130
|
* dsh-vscode-mode client — DiffLauncher:全局差异总览 + 归档 圆角下拉。
|
|
@@ -10901,6 +11178,18 @@ window.__ModuleLoader__.load({
|
|
|
10901
11178
|
const detailRef = react.default.useRef(0);
|
|
10902
11179
|
const moreRef = react.default.useRef(false);
|
|
10903
11180
|
const [error, setError] = react.default.useState(null);
|
|
11181
|
+
const [source, setSource] = react.default.useState("official");
|
|
11182
|
+
const [review, setReview] = react.default.useState({
|
|
11183
|
+
page: null,
|
|
11184
|
+
error: ""
|
|
11185
|
+
});
|
|
11186
|
+
react.default.useEffect(() => {
|
|
11187
|
+
setReview({
|
|
11188
|
+
page: null,
|
|
11189
|
+
error: ""
|
|
11190
|
+
});
|
|
11191
|
+
return subscribeReview(sessionId, setReview);
|
|
11192
|
+
}, [sessionId]);
|
|
10904
11193
|
/** @author ddj 2026年09月30号 @description 仅加载一个归档摘要页并丢弃失效响应。 */
|
|
10905
11194
|
const loadArchives = (cursor = null) => {
|
|
10906
11195
|
if (!sessionId) {
|
|
@@ -10963,7 +11252,7 @@ window.__ModuleLoader__.load({
|
|
|
10963
11252
|
setArchiveDetail(null);
|
|
10964
11253
|
setArchivePath(null);
|
|
10965
11254
|
setArchiveErr(null);
|
|
10966
|
-
if (tab === "archive") loadArchives();
|
|
11255
|
+
if (tab === "archive" && source === "legacy") loadArchives();
|
|
10967
11256
|
else {
|
|
10968
11257
|
setArchives(null);
|
|
10969
11258
|
setNextCursor(null);
|
|
@@ -10972,9 +11261,20 @@ window.__ModuleLoader__.load({
|
|
|
10972
11261
|
requestRef.current++;
|
|
10973
11262
|
detailRef.current++;
|
|
10974
11263
|
};
|
|
10975
|
-
}, [
|
|
11264
|
+
}, [
|
|
11265
|
+
tab,
|
|
11266
|
+
sessionId,
|
|
11267
|
+
source
|
|
11268
|
+
]);
|
|
10976
11269
|
let body;
|
|
10977
|
-
|
|
11270
|
+
const legacyFiles = sum.pendingFiles.filter((file) => file.sourceKind !== "official");
|
|
11271
|
+
if (source === "official") body = react.default.createElement(react.default.Fragment, null, react.default.createElement("div", { className: "edrv-search-empty" }, "官方轮次净差异 · 原始历史只读;编辑按钮打开当前文件,保存不代表已审。"), review.error ? react.default.createElement("div", { role: "alert" }, review.error) : null, react.default.createElement(ReviewLedger, {
|
|
11272
|
+
key: sessionId + ":" + tab,
|
|
11273
|
+
sessionId,
|
|
11274
|
+
first: review.page,
|
|
11275
|
+
history: tab === "archive"
|
|
11276
|
+
}));
|
|
11277
|
+
else if (tab === "pending") body = legacyFiles.length === 0 ? react.default.createElement("div", { className: "edrv-search-empty" }, "暂无待处理差异(已全部处理)") : legacyFiles.map((f) => react.default.createElement("div", {
|
|
10978
11278
|
key: f.path,
|
|
10979
11279
|
className: "edrv-launch-row",
|
|
10980
11280
|
title: "打开并跳转到差异 " + (f.label ?? f.path),
|
|
@@ -11157,7 +11457,13 @@ window.__ModuleLoader__.load({
|
|
|
11157
11457
|
}, "待处理" + (sum.totalFiles ? " (" + sum.totalFiles + ")" : "")), react.default.createElement("button", {
|
|
11158
11458
|
className: "edrv-launch-tab" + (tab === "archive" ? " on" : ""),
|
|
11159
11459
|
onClick: () => setTab("archive")
|
|
11160
|
-
}, "归档"), react.default.createElement("
|
|
11460
|
+
}, "归档"), react.default.createElement("button", {
|
|
11461
|
+
className: "edrv-launch-tab" + (source === "official" ? " on" : ""),
|
|
11462
|
+
onClick: () => setSource("official")
|
|
11463
|
+
}, "官方净差异"), react.default.createElement("button", {
|
|
11464
|
+
className: "edrv-launch-tab" + (source === "legacy" ? " on" : ""),
|
|
11465
|
+
onClick: () => setSource("legacy")
|
|
11466
|
+
}, "旧工具记录"), react.default.createElement("span", { style: { flex: 1 } }), error ? react.default.createElement("span", { style: {
|
|
11161
11467
|
fontSize: 11,
|
|
11162
11468
|
color: "var(--dsw-alias-state-error-primary,#d9534f)",
|
|
11163
11469
|
overflow: "hidden",
|
|
@@ -11631,6 +11937,41 @@ window.__ModuleLoader__.load({
|
|
|
11631
11937
|
"aria-label": diffDockText(fileTotal),
|
|
11632
11938
|
onClick: onOpenNextFile
|
|
11633
11939
|
}, diffDockText(fileTotal))));
|
|
11940
|
+
if (props.official) {
|
|
11941
|
+
const official = props.official;
|
|
11942
|
+
return react.default.createElement("div", {
|
|
11943
|
+
className: "edrv-diffbar edrv-diffbar-dock",
|
|
11944
|
+
"data-edrv-diffbox": "1",
|
|
11945
|
+
"data-edrv-diff-source": "official"
|
|
11946
|
+
}, react.default.createElement("div", { className: "edrv-diffbar-row" }, react.default.createElement("button", {
|
|
11947
|
+
className: "edrv-pill edrv-pill-ghost",
|
|
11948
|
+
onClick: onPrevFile,
|
|
11949
|
+
disabled: !fileTotal
|
|
11950
|
+
}, "←"), react.default.createElement("span", null, base + " · " + official.pending + " 条官方待审记录"), react.default.createElement("button", {
|
|
11951
|
+
className: "edrv-pill edrv-pill-ghost",
|
|
11952
|
+
onClick: onNextFile,
|
|
11953
|
+
disabled: !fileTotal
|
|
11954
|
+
}, "→"), react.default.createElement("button", {
|
|
11955
|
+
className: "edrv-pill edrv-pill-ghost",
|
|
11956
|
+
onClick: () => setExpanded(!expanded),
|
|
11957
|
+
"aria-expanded": expanded
|
|
11958
|
+
}, expanded ? "收起差异" : "查看差异 / 标记已审"), react.default.createElement("button", {
|
|
11959
|
+
className: "edrv-pill edrv-pill-ghost",
|
|
11960
|
+
disabled: true,
|
|
11961
|
+
title: "官方仅提供差异块,缺少可验证全文,不能安全回滚。"
|
|
11962
|
+
}, "Undo 不可用"), react.default.createElement("button", {
|
|
11963
|
+
className: "edrv-pill edrv-pill-ghost",
|
|
11964
|
+
onClick: () => onOpenLauncher("pending")
|
|
11965
|
+
}, "差异总览"), react.default.createElement("button", {
|
|
11966
|
+
className: "edrv-pill edrv-pill-ghost",
|
|
11967
|
+
onClick: () => onOpenLauncher("archive")
|
|
11968
|
+
}, "归档 / 历史")), props.reviewError ? react.default.createElement("div", { role: "alert" }, props.reviewError) : null, expanded ? react.default.createElement("div", { className: "edrv-diffbar-body" }, react.default.createElement(ReviewLedger, {
|
|
11969
|
+
key: official.sessionId + ":" + official.path,
|
|
11970
|
+
sessionId: official.sessionId,
|
|
11971
|
+
first: official.page,
|
|
11972
|
+
path: official.path
|
|
11973
|
+
})) : null);
|
|
11974
|
+
}
|
|
11634
11975
|
let bodyEl = null;
|
|
11635
11976
|
if (expanded) {
|
|
11636
11977
|
const rows = pendingRegions.map((r) => {
|
|
@@ -23015,6 +23356,18 @@ window.__ModuleLoader__.load({
|
|
|
23015
23356
|
const [monaco, setMonaco] = react.default.useState(null);
|
|
23016
23357
|
const [monacoErr, setMonacoErr] = react.default.useState(null);
|
|
23017
23358
|
const [records, setRecords] = react.default.useState({});
|
|
23359
|
+
const [review, setReview] = react.default.useState({
|
|
23360
|
+
page: null,
|
|
23361
|
+
error: ""
|
|
23362
|
+
});
|
|
23363
|
+
react.default.useEffect(() => {
|
|
23364
|
+
setReview({
|
|
23365
|
+
page: null,
|
|
23366
|
+
error: ""
|
|
23367
|
+
});
|
|
23368
|
+
if (!sessionId) return void 0;
|
|
23369
|
+
return subscribeReview(sessionId, setReview);
|
|
23370
|
+
}, [sessionId]);
|
|
23018
23371
|
const [tabs, setTabs] = react.default.useState([]);
|
|
23019
23372
|
const [active, setDiskActive] = react.default.useState(null);
|
|
23020
23373
|
const [planTabs, setPlanTabs] = react.default.useState([]);
|
|
@@ -23303,14 +23656,15 @@ window.__ModuleLoader__.load({
|
|
|
23303
23656
|
const currentRecords = react.default.useMemo(() => {
|
|
23304
23657
|
const list = [];
|
|
23305
23658
|
for (const rec of Object.values(records)) {
|
|
23306
|
-
if (rec.archived === true) continue;
|
|
23659
|
+
if (rec.archived === true || officialPath(rec.path, review.page)) continue;
|
|
23307
23660
|
if (sameFile(rec.path, active)) list.push(rec);
|
|
23308
23661
|
}
|
|
23309
23662
|
return list;
|
|
23310
23663
|
}, [
|
|
23311
23664
|
records,
|
|
23312
23665
|
active,
|
|
23313
|
-
cwd
|
|
23666
|
+
cwd,
|
|
23667
|
+
review.page
|
|
23314
23668
|
]);
|
|
23315
23669
|
const contentReady = content !== null && contentPath === active;
|
|
23316
23670
|
const isImageActive = !!active && isImagePath(active) && !svgTextRef.current.has(active);
|
|
@@ -23344,7 +23698,7 @@ window.__ModuleLoader__.load({
|
|
|
23344
23698
|
}
|
|
23345
23699
|
return map;
|
|
23346
23700
|
}, [pendingRegions]);
|
|
23347
|
-
const sum = react.default.useMemo(() =>
|
|
23701
|
+
const sum = react.default.useMemo(() => diffSummary(Object.values(records), review.page), [records, review.page]);
|
|
23348
23702
|
/** @private @author ddj 2026年10月08号 @param path Disk path. @param select Select tab. @param preview Optional explicit Markdown mode. */
|
|
23349
23703
|
function addTab(path, select, preview) {
|
|
23350
23704
|
setTabs((prev) => insertTab(prev, path));
|
|
@@ -26083,7 +26437,7 @@ window.__ModuleLoader__.load({
|
|
|
26083
26437
|
const allPending = react.default.useMemo(() => {
|
|
26084
26438
|
const out = [];
|
|
26085
26439
|
for (const rec of Object.values(records)) {
|
|
26086
|
-
if (rec.superseded === true) continue;
|
|
26440
|
+
if (rec.superseded === true || officialPath(rec.path, review.page)) continue;
|
|
26087
26441
|
const perHunk = Array.isArray(rec.decisions?.perHunk) ? rec.decisions.perHunk : [];
|
|
26088
26442
|
const hunks = Array.isArray(rec.hunks) ? rec.hunks : [];
|
|
26089
26443
|
for (let i = 0; i < hunks.length; i++) if ((perHunk.length ? perHunk[i] : rec.decisions.call) === "pending" && !noopHunk(rec, hunks[i])) out.push({
|
|
@@ -26094,7 +26448,7 @@ window.__ModuleLoader__.load({
|
|
|
26094
26448
|
});
|
|
26095
26449
|
}
|
|
26096
26450
|
return out;
|
|
26097
|
-
}, [records]);
|
|
26451
|
+
}, [records, review.page]);
|
|
26098
26452
|
/**
|
|
26099
26453
|
* 批量处理所有差异文件:采纳全部 / 不采纳全部。不采纳按 at 降序(新改先回滚)排序后一次 RPC。
|
|
26100
26454
|
* @author ddj 2026年08月20号
|
|
@@ -27576,8 +27930,16 @@ window.__ModuleLoader__.load({
|
|
|
27576
27930
|
* @author ddj 2026年09月29号
|
|
27577
27931
|
* @returns 含布局及操作回调的差异快照
|
|
27578
27932
|
*/
|
|
27933
|
+
const officialFile = review.page?.overview?.coveredPaths.find((path) => sameFile(path, active));
|
|
27579
27934
|
const buildDockSnapshot = () => ({
|
|
27580
27935
|
mode: editorDockMode(active),
|
|
27936
|
+
official: officialFile ? {
|
|
27937
|
+
sessionId,
|
|
27938
|
+
page: review.page,
|
|
27939
|
+
path: officialFile,
|
|
27940
|
+
pending: sum.files.find((file) => sameFile(file.path, active))?.pending ?? 0
|
|
27941
|
+
} : null,
|
|
27942
|
+
reviewError: review.error,
|
|
27581
27943
|
layout,
|
|
27582
27944
|
pendingRegions,
|
|
27583
27945
|
staleRegions,
|
|
@@ -28226,12 +28588,22 @@ window.__ModuleLoader__.load({
|
|
|
28226
28588
|
setSummary(null);
|
|
28227
28589
|
return;
|
|
28228
28590
|
}
|
|
28229
|
-
|
|
28230
|
-
|
|
28591
|
+
let records = [], page = null;
|
|
28592
|
+
const offRecords = subscribeRecords(sessionId, (next) => {
|
|
28593
|
+
records = next;
|
|
28594
|
+
setSummary(diffSummary(records, page));
|
|
28231
28595
|
});
|
|
28596
|
+
const offReview = subscribeReview(sessionId, (feed) => {
|
|
28597
|
+
page = feed.page;
|
|
28598
|
+
setSummary(diffSummary(records, page));
|
|
28599
|
+
});
|
|
28600
|
+
return () => {
|
|
28601
|
+
offRecords();
|
|
28602
|
+
offReview();
|
|
28603
|
+
};
|
|
28232
28604
|
}, [sessionId]);
|
|
28233
28605
|
const n = summary?.totalFiles ?? 0;
|
|
28234
|
-
if (!
|
|
28606
|
+
if (!sessionId) return null;
|
|
28235
28607
|
const click = () => {
|
|
28236
28608
|
openEditorView(summary?.pendingFiles?.[0]?.path ?? null);
|
|
28237
28609
|
setTimeout(() => {
|
|
@@ -28240,7 +28612,7 @@ window.__ModuleLoader__.load({
|
|
|
28240
28612
|
};
|
|
28241
28613
|
return react.default.createElement("button", {
|
|
28242
28614
|
onClick: click,
|
|
28243
|
-
title: n + "
|
|
28615
|
+
title: "差异审查:" + n + " 个待处理文件;点击查看总览与历史",
|
|
28244
28616
|
style: {
|
|
28245
28617
|
position: "relative",
|
|
28246
28618
|
display: "inline-flex",
|
|
@@ -28367,9 +28739,19 @@ window.__ModuleLoader__.load({
|
|
|
28367
28739
|
setSummary(null);
|
|
28368
28740
|
return;
|
|
28369
28741
|
}
|
|
28370
|
-
|
|
28371
|
-
|
|
28742
|
+
let records = [], page = null;
|
|
28743
|
+
const offRecords = subscribeRecords(sessionId, (next) => {
|
|
28744
|
+
records = next;
|
|
28745
|
+
setSummary(diffSummary(records, page));
|
|
28746
|
+
});
|
|
28747
|
+
const offReview = subscribeReview(sessionId, (feed) => {
|
|
28748
|
+
page = feed.page;
|
|
28749
|
+
setSummary(diffSummary(records, page));
|
|
28372
28750
|
});
|
|
28751
|
+
return () => {
|
|
28752
|
+
offRecords();
|
|
28753
|
+
offReview();
|
|
28754
|
+
};
|
|
28373
28755
|
}, [sessionId]);
|
|
28374
28756
|
react.default.useEffect(() => {
|
|
28375
28757
|
if (!sessionId) return void 0;
|
|
@@ -28399,12 +28781,12 @@ window.__ModuleLoader__.load({
|
|
|
28399
28781
|
nextIndexBySession.set(sessionId, next.index);
|
|
28400
28782
|
if (next.path) openDiffView(next.path);
|
|
28401
28783
|
};
|
|
28402
|
-
return react.default.createElement(react.default.Fragment, null, renderDock(react.default.createElement(DiffBox, {
|
|
28784
|
+
return react.default.createElement(react.default.Fragment, null, palette, renderDock(react.default.createElement(DiffBox, {
|
|
28403
28785
|
mode: "chat",
|
|
28404
28786
|
dock: true,
|
|
28405
28787
|
fileTotal: paths.length,
|
|
28406
28788
|
onOpenNextFile: click
|
|
28407
|
-
}), sessionId || "chat")
|
|
28789
|
+
}), sessionId || "chat"));
|
|
28408
28790
|
}
|
|
28409
28791
|
//#endregion
|
|
28410
28792
|
//#region src/client/mcpForm.ts
|
|
@@ -33948,6 +34330,8 @@ window.__ModuleLoader__.load({
|
|
|
33948
34330
|
]);
|
|
33949
34331
|
/** 外部文件变化重列去抖:一轮外部批量写入(如 agent 连写多文件)合并为一次重列。 */
|
|
33950
34332
|
const FILE_CHANGE_DEBOUNCE_MS = 400;
|
|
34333
|
+
/** 兼容 host 返回裸文案或附带路径的缺失目录错误。 */
|
|
34334
|
+
const MISSING_DIR_PREFIX = "目录不存在";
|
|
33951
34335
|
/**
|
|
33952
34336
|
* 目录图标元素(官方 IconFolderOpen16/Close16)。
|
|
33953
34337
|
* @author ddj 2026年09月10号
|
|
@@ -34043,6 +34427,22 @@ window.__ModuleLoader__.load({
|
|
|
34043
34427
|
const changedRelRef = react.default.useRef(/* @__PURE__ */ new Set());
|
|
34044
34428
|
/** 渲染取数:内存态 → 本地条目缓存 → null(显示加载态)。 */
|
|
34045
34429
|
const entriesOf = (rel) => dirsRef.current[rel] ?? entriesCacheGet(scope, rel) ?? null;
|
|
34430
|
+
/**
|
|
34431
|
+
* @private
|
|
34432
|
+
* @author ddj 2026年10月23号
|
|
34433
|
+
* @description 移除已删除目录的展开记录,经现有防抖持久化自愈;不改请求 token,保留 finally 的加载态清理。
|
|
34434
|
+
* @param rel 已确认缺失的非根相对目录
|
|
34435
|
+
*/
|
|
34436
|
+
function dropDeadDir(rel) {
|
|
34437
|
+
setExpanded(removeExpansion);
|
|
34438
|
+
/** @private @author ddj 2026年10月23号 @param prev 当前展开映射。 @returns 移除失效目录后的展开映射。 */
|
|
34439
|
+
function removeExpansion(prev) {
|
|
34440
|
+
if (prev[rel] !== true) return prev;
|
|
34441
|
+
const next = Object.assign({}, prev);
|
|
34442
|
+
delete next[rel];
|
|
34443
|
+
return next;
|
|
34444
|
+
}
|
|
34445
|
+
}
|
|
34046
34446
|
/** 预取子目录:≤4 个、排除重型目录、缓存新鲜跳过、已加载跳过、不级联。 */
|
|
34047
34447
|
const prefetchDirs = (rel, entries) => {
|
|
34048
34448
|
const want = [];
|
|
@@ -34086,7 +34486,14 @@ window.__ModuleLoader__.load({
|
|
|
34086
34486
|
if (res.root) setRoot((prev) => prev || res.root);
|
|
34087
34487
|
entriesCachePut(scope, rel, res.entries);
|
|
34088
34488
|
if (doPrefetch) prefetchDirs(rel, res.entries);
|
|
34089
|
-
} else if (!entriesOf(rel))
|
|
34489
|
+
} else if (!entriesOf(rel)) {
|
|
34490
|
+
const message = res?.error ? String(res.error) : "读取目录失败";
|
|
34491
|
+
if (rel && message.startsWith(MISSING_DIR_PREFIX)) {
|
|
34492
|
+
dropDeadDir(rel);
|
|
34493
|
+
return;
|
|
34494
|
+
}
|
|
34495
|
+
setErrors((prev) => Object.assign({}, prev, { [rel]: message }));
|
|
34496
|
+
}
|
|
34090
34497
|
}).catch((e) => {
|
|
34091
34498
|
if (scopeRef.current !== scope || tokensRef.current !== tokens || tokens[rel] !== token) return;
|
|
34092
34499
|
if (!entriesOf(rel)) setErrors((prev) => Object.assign({}, prev, { [rel]: "读取目录异常:" + String(e) }));
|
|
@@ -34450,7 +34857,12 @@ window.__ModuleLoader__.load({
|
|
|
34450
34857
|
}
|
|
34451
34858
|
const updateHint = !ctx?.svn?.svnCli ? "SVN CLI 不可用" : !ctx?.openSvnUpdate ? "SVN 更新窗口不可用" : "更新项目(打开 SVN 更新窗口)";
|
|
34452
34859
|
const errorList = Object.entries(errors);
|
|
34453
|
-
|
|
34860
|
+
let rawErrorText = null;
|
|
34861
|
+
if (errorList.length) {
|
|
34862
|
+
const [errorRel, message] = errorList[0];
|
|
34863
|
+
rawErrorText = message;
|
|
34864
|
+
if (errorRel && !message.endsWith(":" + errorRel)) rawErrorText += ":" + errorRel;
|
|
34865
|
+
}
|
|
34454
34866
|
const sessionMissing = isSessionMissingError(rawErrorText);
|
|
34455
34867
|
const errorText = rawErrorText === null ? null : sessionMissing ? sessionMissingTitle() + ":" + sessionMissingDetail() : rawErrorText;
|
|
34456
34868
|
return react.default.createElement("div", {
|