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/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
- }, [tab, sessionId]);
11264
+ }, [
11265
+ tab,
11266
+ sessionId,
11267
+ source
11268
+ ]);
10976
11269
  let body;
10977
- if (tab === "pending") body = sum.pendingFiles.length === 0 ? react.default.createElement("div", { className: "edrv-search-empty" }, "暂无待处理差异(已全部处理)") : sum.pendingFiles.map((f) => react.default.createElement("div", {
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("span", { style: { flex: 1 } }), error ? react.default.createElement("span", { style: {
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(() => summarize(Object.values(records)), [records]);
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
- return subscribeRecords(sessionId, (records) => {
28230
- setSummary(summarize(records));
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 (!n) return null;
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
- return subscribeRecords(sessionId, (records) => {
28371
- setSummary(summarize(records));
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"), palette);
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)) setErrors((prev) => Object.assign({}, prev, { [rel]: res?.error ? String(res.error) : "读取目录失败" }));
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
- const rawErrorText = errorList.length ? errorList[0][1] : null;
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", {