dsh-vscode-mode 0.23.1 → 0.23.3

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 CHANGED
@@ -521,6 +521,7 @@ DSH 每次启动监听随机端口,`http://127.0.0.1:<port>` 就是浏览器 o
521
521
 
522
522
  官方 `dsh plugin` 方式,三选一。下面以 `web` profile 为例;Desktop 或其他命名
523
523
  profile 请替换为实际目标,不要把示例目录当成当前运行 profile。升级前可阅读
524
+ **v0.23.2** 修复 Keep 在非活跃端(桌面端)点了没反应,共两处成因。①权限:`edrv.review.decide` 原先一律要求本端活跃会话,而同端详情接口的 `capability.keep` 只按审查库推导,于是会话在另一实例时按钮亮着、点击后抛错并被兜底成 HTTP 500「DSH 请求失败(HTTP 500)」。现按写入副作用区分——官方记录 Keep 只写共享审查库(`BEGIN IMMEDIATE` + operationId 幂等 + `id/seq/revision/current/decision` CAS,库为每工作区 SQLite 且 `WAL + busy_timeout=2000`),冷端放行;官方 Undo 需校验会话证据并写回源文件、待审投影条目写本机待审 JSON(无跨进程锁),两者仍限本端活跃会话,并返回可读的跨端原因而非 500。详情响应新增 `capability.live`,冷端差异栏禁用 Undo 并给出跨端说明、Keep 提示补一句「写入共享审查库,仍可执行」,批量预检对冷端待审条目给出准确原因。②选中目标:已结算(`oldText === newText`,累计净变化为零)的待审投影 `capability` 恒为不可 Keep,但单文件通道把它排在官方记录之前、客户端取 `items[0]`,同路径官方待审行又被历史过滤掉,于是差异栏「待审 0」、Keep 禁用、点击无反应。现 `pendingPageOf()` 在交给官方审查通道前剔除已结算投影,官方待审记录重新成为审查目标。另外详情未加载时点 Keep 不再静默返回,改为可读提示;打开文件时若某条官方记录的改动已全部不在当前文件中(无可定位差异且缓冲区干净),自动按 Keep 结案(只标记不写盘),差异栏不再显示「本记录 N 处改动均已不在当前文件中」这类失效叙事;官方元数据首次载入期间差异栏显示 `—` 而不是按历史记录报数,避免重启瞬间闪出几十条假待审。验证:typecheck 0 error、目标单测 57/57、`pnpm build` 通过,并用真实工作区审查库探针确认差异栏首条已回到官方记录且 `capability={keep:true,undo:true,live:true}`;桌面端 GUI 点按验收因 App 重启后恢复的是另一个会话而未完成,缺口保留,见 [0.23.2 发布说明](docs/release-0.23.2.md)。以下保留历史版本事实:
524
525
  **v0.23.1** 修复「Keep All / Undo All」在工作区待审超过一页时必定中止:批量按钮原先复用官方审查的分页接口逐页取待审条目,并把每页返回的展示用计数当成全量总数校验,因此官方待审超过 50 条(本仓库常态约 150 条)时会先报「待审记录在分页期间变化」或最终报「待审记录数量不完整」,实际一条都不会被决策(已完成 0 项)。现在批量操作改走一次性完整快照——`edrv.review.list` 新增 `all: true` 参数,服务端循环分页取全官方待审行,与工作区历史改动投影按路径去重后返回 `nextCursor: null` 与真实总数(`pending`),客户端单次取快照、校验条目唯一且仍为待审后才逐项决策;快照总量超过 2000 条时明确拒绝并提示分批处理,条目在决策途中变化则继续整批中止、不做部分写入。差异栏的日常浏览仍走原分页接口,展示行为不变。本版修复只经单测与真实审查库只读核对,未在重启后的 GUI 上点按批量按钮验证,缺口保留,见 [0.23.1 发布说明](docs/release-0.23.1.md)。以下保留历史版本事实:
525
526
  **v0.23.0** 的快捷菜单:编辑器内一个可拖动悬浮按钮,点开是自定义菜单树(组 / 按钮,最多 5 层),按钮可携带提示词并直接发送给 AI 或只填入输入框;配置存于 `<DSH_HOME>/dsh-vscode-mode/data/quick-menu.json`,带 `revision` 乐观锁与损坏降级;节点可限定生效工作区(默认全部),按钮可绑定技能与规则,发送时按当前会话可见性校验后只拼装通过的绑定。新增 `skills.list` 只读 RPC 枚举「全局技能 + 当前工作区技能」作为候选,配置界面的工作区、技能、规则全部改为下拉弹层/多选下拉,菜单设置重排为四段并统一按钮盒模型(修掉「导入 JSON」下移不居中)。本版**未做 GUI 真机验收**(本机 pnpm store 损坏、插件未部署),缺口已获用户接受,见 [0.23.0 发布说明](docs/release-0.23.0.md)。以下保留历史版本事实:
526
527
  **v0.22.0** 的累计基线待审(非 UI 能力对齐 dsh-diff-approval):同一文件的多次编辑折叠成一条待审、基线保留最早内容,A→B→C 显示 A→C、A→B→A 自然归零;Keep 只推进基线不改磁盘,Revert 走版本守卫写回并读回校验,撤销/重做用游标模型跨重启保留;新增 `str_replace_editor` 预像配对、`fs/observed` 终像、已入列路径的外部改动吸收与缺失/不可读生命周期、git/svn/Perforce 基线导入,以及旧 sidecar 写入口对已接管路径的服务端拒绝。差异仍在编辑器内行内展示、块级 Keep/Undo 用药丸,文件级在差异栏;详情面板与批注(含「让 AI 应答」)本版移除。验收边界:p4 仅有假 shell 单测(本机无 Perforce 客户端),「让 AI 应答」随批注一并下线故不再验证;本版未在宿主上重启验收 GUI 全流程,见 [0.22.0 发布说明](docs/release-0.22.0.md)。以下保留历史版本事实:
@@ -879,6 +880,8 @@ Keep All / Undo All 却是亮的。修复:单文件 Keep / Undo 覆盖冲突
879
880
  完整变更见 [GitHub Releases](https://github.com/Lenonss/DSH_VsCodeMode/releases)。
880
881
  近期关键版本:
881
882
 
883
+ - **v0.23.3**:修复 issue #13(已删除文件的差异条目把人卡死)与 #12(重启后成串「采集不完整」)。①文件在磁盘上被删掉时,客户端申请整文件删除预览被「已有官方审查记录」拒绝,而拒绝只是一条普通 `Error`,于是落到通用加载失败分支——「文件加载失败:…」+ 进度钉在 0%、重试同错、标签页关不掉;`edrv.review.detail` 还把 Undo 不可用解释成「缺少可验证的原始字节」,而真实原因是文件已不在。现缺失文件预览的可预期拒绝带稳定码(`official-covered` / `official-decided` / `no-records` / `no-evidence` / `too-large`),客户端据此显示「文件已在磁盘删除(· 有官方待审改动)」终态:标题 + 原因 + 重试 / 关闭标签页,不再画 0% 进度条;官方待审记录在磁盘已删除时给出「Keep 按删除结案、Undo 需要读写该文件」的准确原因(Keep 语义不变,仍只写共享审查库),记录都已结案时提示可直接关闭;`!review` 的磁盘状态抖动改为最多重读 2 次后给终态,消除缺失↔重现的无限递归。v0.22.0 起 `hasOfficial()` 已用 `typeof path !== 'string'` 守卫堵掉把不透明 FsTarget 绑进 SQL 导致的 `Unknown named parameter 'targetKey'`,本版不再复现。②官方 `workspace/changes` 的 summary 只存在宿主内存里,插件启动补采原先无条件把历史事件全部入队,重启后每一轮都必然抛「summary 已失效或尚不可用」并写成不可修复的 `gap`(面板成串「轮次 N 采集不完整」);现补采先 `summary()` 探测再入队,失效事件只记一条汇总日志。验证:typecheck 与基线同为 37 个既有错误(`fflate/browser` 依赖缺失)、零新增;定向单测 111/111(含新增 `tests/deletedFileReview.test.ts` 5 例与 `tests/officialBackfill.test.ts` 2 例)、EditorView 相关 82/82;`npm run build` 四个 bundle 通过。见 [发布说明](docs/release-0.23.3.md)。
884
+ - **v0.23.2**:修复 Keep 在非活跃端(桌面端)点了没反应,两处成因。①`edrv.review.decide` 原先一律要求本端活跃会话,而能力位只按审查库推导,会话在另一实例时按钮亮着、点击后抛错被兜底成 HTTP 500。现按写入副作用区分:官方记录 Keep 只写共享审查库(`BEGIN IMMEDIATE` + 幂等键 + 版本 CAS),冷端放行;官方 Undo(校验证据并写回源文件)与待审投影条目(本机 JSON 无跨进程锁)仍限活跃端,返回可读跨端原因而非 500;详情新增 `capability.live`,冷端禁用 Undo 并说明、Keep 提示补「写入共享审查库,仍可执行」。②已结算待审投影的 `capability.keep` 恒为假,却因排在 `items[0]`、同路径官方待审行被历史过滤,把官方记录挤出差异栏(表现为「待审 0」+Keep 禁用+点击无反应);现 `pendingPageOf()` 剔除已结算投影,官方记录重新成为审查目标。另详情未加载时点 Keep 不再静默失败;打开文件时若某条官方记录的改动已全部不在当前文件中(无可定位差异且缓冲区干净),自动按 Keep 结案(只标记不写盘),差异栏不再显示「本记录 N 处改动均已不在当前文件中」;官方元数据首次载入期间差异栏显示 `—` 而非按历史记录报数,重启瞬间不再闪出几十条假待审。验证:typecheck 0 error、目标单测 57/57、build 通过、真实审查库探针确认首条回到官方记录且 `capability={keep:true,undo:true,live:true}`;桌面端 GUI 点按验收因重启后恢复的是另一个会话未完成,缺口保留。发布不自动部署,升级后重启 DSH 并刷新页面。见 [发布说明](docs/release-0.23.2.md)。
882
885
  - **v0.23.1**:修复 Keep All / Undo All 在待审超过一页时必定中止——批量按钮原先复用分页接口并把每页的展示用计数当全量总数校验,官方待审 150 条时先报「待审记录在分页期间变化」或最终报「待审记录数量不完整」,实际 0 项被决策。现改为一次性完整快照(`edrv.review.list` 新增 `all: true`,服务端循环取全并与历史改动投影去重,`nextCursor: null`、`pending` 为真实总数),客户端单次取快照、校验唯一且仍待审后逐项决策;超过 2000 条明确拒绝并提示分批,途中变化仍整批中止不部分写入。差异栏浏览仍走原分页接口。本版未在重启后的 GUI 点按验证,缺口保留;发布不自动部署,升级后重启 DSH 并刷新页面。见 [发布说明](docs/release-0.23.1.md)。
883
886
  - **v0.23.0**:新增快捷菜单——可拖动悬浮按钮 + 自定义菜单树(组/按钮,最多 5 层),按钮携带提示词并「直接发送给 AI」或「只填入输入框」;配置存 `<DSH_HOME>/dsh-vscode-mode/data/quick-menu.json`,带 `revision` 乐观锁、冲突重拉不覆盖、损坏降级。节点可限定工作区(默认全部、子节点继承),按钮可绑定技能与规则,发送时按会话可见性校验后只拼装通过的绑定(规则支持引用/内联)。新增 `skills.list` 只读 RPC 枚举全局技能 + 当前工作区技能;配置界面工作区改下拉弹层、技能/规则改分组多选下拉并移除手输技能名,菜单设置重排四段、统一按钮盒模型并修掉「导入 JSON」下移不居中。本版 GUI 未真机验收(pnpm store 损坏、插件未部署),缺口已获用户接受;发布不自动部署,升级后重启 DSH 并刷新页面。见 [发布说明](docs/release-0.23.0.md)。
884
887
  - **v0.22.0**:新增累计基线待审——同文件多次编辑折叠成一条、基线保留最早内容(A→B→C 显示 A→C、A→B→A 归零);Keep 只推进基线不改磁盘,Revert 带版本守卫写回并读回,撤销/重做游标模型跨重启保留;补 `str_replace_editor` 预像配对、外部改动吸收与缺失生命周期、git/svn/Perforce 基线导入、旧入口服务端拒绝。差异仍在编辑器内行内展示,详情面板与批注(含「让 AI 应答」)移除。p4 仅假 shell 单测(本机无 Perforce 客户端),GUI 全流程未重启验收;发布不自动部署,升级后重启 DSH 并刷新页面。见 [发布说明](docs/release-0.22.0.md)。
package/lib/client.js CHANGED
@@ -3554,6 +3554,10 @@ window.__ModuleLoader__.load({
3554
3554
  }
3555
3555
  }
3556
3556
  //#endregion
3557
+ //#region src/shared/pendingReview.ts
3558
+ /** 待审条目在面板列表里的 id 前缀;客户端与 Host 共用这一份口径。 */
3559
+ const PENDING_PREFIX = "pending:";
3560
+ //#endregion
3557
3561
  //#region src/client/reviewBatch.ts
3558
3562
  /**
3559
3563
  * @public
@@ -3589,6 +3593,7 @@ window.__ModuleLoader__.load({
3589
3593
  for (const item of items) {
3590
3594
  port.guard();
3591
3595
  const detail = await port.detail(item.id);
3596
+ if (detail.capability.live === false && item.id.startsWith("pending:")) throw new Error(`${item.display}:待审条目需要在会话活跃的一端判定,请先在那一端处理或在本端打开该会话`);
3592
3597
  if (!detail.item.current || detail.item.seq !== item.seq || detail.item.revision !== item.revision || !detail.capability.keep) throw new Error(`${item.display}:审查版本变化,请刷新后重试`);
3593
3598
  if (reject && !detail.capability.undo) throw new Error(`${item.display}:${detail.capability.reason}`);
3594
3599
  details.push(detail);
@@ -3754,10 +3759,6 @@ window.__ModuleLoader__.load({
3754
3759
  return reviewBlocks(diff).map(placeRegion);
3755
3760
  }
3756
3761
  //#endregion
3757
- //#region src/shared/pendingReview.ts
3758
- /** 待审条目在面板列表里的 id 前缀;客户端与 Host 共用这一份口径。 */
3759
- const PENDING_PREFIX = "pending:";
3760
- //#endregion
3761
3762
  //#region src/shared/pathKey.ts
3762
3763
  /**
3763
3764
  * 路径比较口径:Host 与客户端共用同一套「远端 / 盘符 / 相对路径」判定,只在平台折叠上分工。
@@ -377891,6 +377892,25 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
377891
377892
  }
377892
377893
  //#endregion
377893
377894
  //#region src/client/missingReview.ts
377895
+ /**
377896
+ * @public
377897
+ * @author ddj 2026年10月31号
377898
+ * @description: 把 missingReview 的失败归并成「文件已删除」终态。只有确认删除的拒绝码才是终态;
377899
+ * 会话缺失、权限、文件重新出现等失败返回 null,调用方继续走通用加载失败(可重试、保留跨端提示)。
377900
+ * @param failure RPC 失败结果(error 必填,code 可选)
377901
+ * @returns 终态信息或 null
377902
+ */
377903
+ function goneStateOf(failure) {
377904
+ const code = failure?.code;
377905
+ const official = code === "official-covered";
377906
+ if (!official && code !== "official-decided" && code !== "no-records" && code !== "no-evidence" && code !== "too-large") return null;
377907
+ const message = String(failure?.error ?? "文件已在磁盘删除").replace(/^Error:\s*/, "");
377908
+ return {
377909
+ title: official ? "文件已在磁盘删除 · 有官方待审改动" : "文件已在磁盘删除",
377910
+ message,
377911
+ official
377912
+ };
377913
+ }
377894
377914
  /** @public @author ddj 2026年10月28号 @param review Frozen complete snapshot. @returns One whole-file deletion, including empty files; no invented current lines. */
377895
377915
  function missingRegion(review) {
377896
377916
  return {
@@ -382531,7 +382551,8 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
382531
382551
  * @returns 持久化的全工作区控制条
382532
382552
  */
382533
382553
  function ReviewAllRow(props) {
382534
- const total = props.total ?? 0;
382554
+ const known = typeof props.total === "number";
382555
+ const total = known ? props.total : 0;
382535
382556
  const current = total && props.current > 0 ? props.current : 0;
382536
382557
  const disabled = props.busy || !!props.unavailable || !props.pending;
382537
382558
  return /* @__PURE__ */ react$1.default.createElement("div", {
@@ -382544,7 +382565,7 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
382544
382565
  className: "edrv-review-total",
382545
382566
  onClick: props.onOpen,
382546
382567
  title: "打开当前工作区全部待审差异文件"
382547
- }, "差异文件 ", /* @__PURE__ */ react$1.default.createElement("strong", null, current, " / ", total)), /* @__PURE__ */ react$1.default.createElement("span", { className: "edrv-review-all-actions" }, /* @__PURE__ */ react$1.default.createElement(_deepseek_ai_dsh_client_ui_primitives.Button, {
382568
+ }, "差异文件 ", /* @__PURE__ */ react$1.default.createElement("strong", null, known ? `${current} / ${total}` : "—")), /* @__PURE__ */ react$1.default.createElement("span", { className: "edrv-review-all-actions" }, /* @__PURE__ */ react$1.default.createElement(_deepseek_ai_dsh_client_ui_primitives.Button, {
382548
382569
  size: "sm",
382549
382570
  variant: "ghost",
382550
382571
  onClick: props.onKeepAll,
@@ -382591,6 +382612,19 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
382591
382612
  "aria-hidden": "true"
382592
382613
  }, /* @__PURE__ */ react$1.default.createElement("path", { d: marks[mark] }));
382593
382614
  }
382615
+ /**
382616
+ * @private
382617
+ * @author ddj 2026年10月31号
382618
+ * @description: Keep 按钮提示。会话不在本端活跃时 Keep 仍可执行(只写共享审查库),
382619
+ * 提示里补一句说明,免得用户把它当成跨端不可用的按钮。
382620
+ * @param hint 差异栏给出的 Keep 说明
382621
+ * @param live 会话是否在本端活跃
382622
+ * @returns 按钮 title 文本
382623
+ */
382624
+ function keepTitle(hint, live) {
382625
+ if (live) return hint;
382626
+ return `${hint}(会话在另一端激活:Keep 写入共享审查库,仍可执行)`;
382627
+ }
382594
382628
  /** @public @author ddj 2026年10月27号 @param props Existing review callbacks and immutable record metadata. @returns Responsive review toolbar; no disk or decision side effects until an explicit action. */
382595
382629
  function OfficialDiffBar(props) {
382596
382630
  const { official, pendingRegions = [], staleRegions = [], diffIdx = 0 } = props;
@@ -382647,7 +382681,7 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
382647
382681
  role: "region",
382648
382682
  "aria-label": "文件差异审查"
382649
382683
  }, /* @__PURE__ */ react$1.default.createElement(ReviewAllRow, {
382650
- total: props.fileTotal ?? 0,
382684
+ total: props.fileTotal ?? null,
382651
382685
  current: (props.fileTotal ?? 0) && (props.fileIdx ?? -1) >= 0 ? (props.fileIdx ?? 0) + 1 : 0,
382652
382686
  busy: props.reviewBatchBusy,
382653
382687
  unavailable: props.batchUnavailable,
@@ -382692,7 +382726,7 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
382692
382726
  icon: barIcon("check"),
382693
382727
  onClick: props.onAcceptFile,
382694
382728
  disabled: props.reviewBatchBusy || !detail?.capability.keep,
382695
- title: detail?.capability.keep ? view.keepHint : detail?.capability.reason
382729
+ title: detail?.capability.keep ? keepTitle(view.keepHint, detail.capability.live !== false) : detail?.capability.reason
382696
382730
  }, "Keep"), /* @__PURE__ */ react$1.default.createElement(_deepseek_ai_dsh_client_ui_primitives.Menu, {
382697
382731
  open: menu === "more",
382698
382732
  anchor: moreTrigger,
@@ -382780,7 +382814,7 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
382780
382814
  role: "region",
382781
382815
  "aria-label": "文件差异审查"
382782
382816
  }, /* @__PURE__ */ react$1.default.createElement(ReviewAllRow, {
382783
- total: props.fileTotal ?? 0,
382817
+ total: props.fileTotal ?? null,
382784
382818
  current: (props.fileTotal ?? 0) && (props.fileIdx ?? -1) >= 0 ? (props.fileIdx ?? 0) + 1 : 0,
382785
382819
  busy,
382786
382820
  pending: props.allPendingCount,
@@ -393558,6 +393592,9 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
393558
393592
  const aiStatusTimer = react$1.default.useRef(null);
393559
393593
  const [error, setError] = react$1.default.useState(null);
393560
393594
  const [loadError, setLoadError] = react$1.default.useState(null);
393595
+ const [goneFile, setGoneFile] = react$1.default.useState(null);
393596
+ const missingReadRef = react$1.default.useRef(/* @__PURE__ */ new Map());
393597
+ const goneNow = goneFile && goneFile.path === active && goneFile.sessionId === sessionId ? goneFile : null;
393561
393598
  const [largeCandidate, setLargeCandidate] = react$1.default.useState(null);
393562
393599
  const [pagedFile, setPagedFile] = react$1.default.useState(null);
393563
393600
  const [pageChanged, setPageChanged] = react$1.default.useState(null);
@@ -393802,6 +393839,7 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
393802
393839
  review.page
393803
393840
  ]);
393804
393841
  const reviewOpsRef = react$1.default.useRef(/* @__PURE__ */ new Map());
393842
+ const settledRef = react$1.default.useRef(/* @__PURE__ */ new Set());
393805
393843
  const [officialDetail, setOfficialDetail] = react$1.default.useState(null);
393806
393844
  const [officialRecords, setOfficialRecords] = react$1.default.useState([]);
393807
393845
  const [selectedReview, setSelectedReview] = react$1.default.useState(null);
@@ -393816,7 +393854,9 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
393816
393854
  react$1.default.useEffect(() => {
393817
393855
  const controller = new AbortController();
393818
393856
  const pendingPath = (review.page?.items ?? []).filter((item) => String(item.id).startsWith(PENDING_PREFIX)).map((item) => item.currentPath || item.path).find((path) => sameFile(path, active));
393819
- const reviewPath = review.page?.overview?.coveredPaths.find((path) => sameFile(path, active)) ?? pendingPath;
393857
+ const coveredPath = review.page?.overview?.coveredPaths.find((path) => sameFile(path, active));
393858
+ const reviewPath = coveredPath ?? pendingPath;
393859
+ dbg(sessionId, `[DEBUG reviewPath] active=${active ?? "(null)"} covered=${coveredPath ?? "(none)"} pending=${pendingPath ?? "(none)"} coveredCount=${review.page?.overview?.coveredPaths.length ?? -1} items=${review.page?.items?.length ?? -1} overview=${review.page?.overview ? 1 : 0}`);
393820
393860
  if (!active || !reviewPath) {
393821
393861
  setOfficialDetail(null);
393822
393862
  setOfficialRecords([]);
@@ -393832,6 +393872,7 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
393832
393872
  if (controller.signal.aborted) return;
393833
393873
  if (!page.ok) throw new Error(page.error);
393834
393874
  const selected = page.page.items.find((item) => item.id === selectedReview) ?? page.page.items[0];
393875
+ dbg(sessionId, `[DEBUG reviewList] path=${reviewPath} items=${page.page.items.length} waiting=${page.page.pending} selected=${selected?.id ?? "(none)"}`);
393835
393876
  if (!selected) {
393836
393877
  setOfficialRecords([]);
393837
393878
  setOfficialDetail(null);
@@ -393842,6 +393883,7 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
393842
393883
  id: selected.id
393843
393884
  }, controller.signal);
393844
393885
  if (controller.signal.aborted) return;
393886
+ dbg(sessionId, `[DEBUG reviewDetail] id=${selected.id} ok=${result.ok ? 1 : 0} keep=${result.ok ? result.detail.capability?.keep === true ? 1 : 0 : "-"} live=${result.ok ? String(result.detail.capability?.live) : "-"} err=${result.ok ? "" : String(result.error).slice(0, 120)}`);
393845
393887
  if (!result.ok) throw new Error(result.error);
393846
393888
  setOfficialRecords((previous) => JSON.stringify(previous) === JSON.stringify(page.page.items) ? previous : page.page.items);
393847
393889
  setOfficialDetail((previous) => JSON.stringify(previous) === JSON.stringify(result.detail) ? previous : result.detail);
@@ -393929,6 +393971,20 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
393929
393971
  const staleAmbiguous = staleRegions.filter((region) => region.staleReason === "ambiguous").length;
393930
393972
  const noBlocks = !!officialDetail && (officialDetail.diff.kind !== "text" || officialDetail.diff.hunks.length === 0);
393931
393973
  const decideRegions = react$1.default.useMemo(() => [...pendingRegions, ...staleRegions], [pendingRegions, staleRegions]);
393974
+ react$1.default.useEffect(() => {
393975
+ if (!officialDetail || contentReady !== true || batchBusyRef.current) return;
393976
+ if (pendingRegions.length > 0 || staleRegions.length === 0) return;
393977
+ if (noBlocks || officialDetail.capability?.keep !== true) return;
393978
+ if (dirtyRef.current[active]) return;
393979
+ settleInvisibleRecord(officialDetail);
393980
+ }, [
393981
+ officialDetail,
393982
+ contentReady,
393983
+ pendingRegions,
393984
+ staleRegions,
393985
+ noBlocks,
393986
+ active
393987
+ ]);
393932
393988
  hoverRegionsRef.current = pendingRegions;
393933
393989
  lineRegionMapRef.current = react$1.default.useMemo(() => {
393934
393990
  const map = /* @__PURE__ */ new Map();
@@ -394425,7 +394481,38 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
394425
394481
  * @param force 强制走 RPC(reloadFile 用,跳过 model 复用)
394426
394482
  * @param version 已知磁盘版本(缺省走 RPC 返回值)
394427
394483
  */
394428
- /** @private @author ddj 2026年10月28号 @param path Missing text file. @param sid Owning session. @param seq Loader generation. @sideEffects Loads deletion evidence without creating or saving the file. */
394484
+ /**
394485
+ * 把「文件已在磁盘删除」记成终态面板:不占进度条、不反复重试,并允许直接关闭页签。
394486
+ * 只有确认删除的原因才走终态;会话/权限类失败仍回退通用加载失败,保留跨端提示。
394487
+ * @private @author ddj 2026年10月31号
394488
+ * @param path 目标文件
394489
+ * @param sid 会话 id
394490
+ * @param seq 加载序号(过期响应丢弃)
394491
+ * @param failure 宿主拒绝原因(可带稳定 code)
394492
+ */
394493
+ function settleGone(path, sid, seq, failure) {
394494
+ if (!loadCurrent(path, seq)) return;
394495
+ const error = failure && typeof failure === "object" ? failure : { error: failure };
394496
+ const gone = goneStateOf(error) ?? (error.deleted === true ? {
394497
+ title: "文件已在磁盘删除",
394498
+ official: false,
394499
+ message: String(error.error ?? "")
394500
+ } : null);
394501
+ if (!gone) {
394502
+ failLoad(path, sid, seq, String(error.error ?? error));
394503
+ return;
394504
+ }
394505
+ stampLoad(path, sid, seq, gone.message);
394506
+ setLoadError(null);
394507
+ setError(null);
394508
+ setGoneFile({
394509
+ ...gone,
394510
+ path,
394511
+ sessionId: sid
394512
+ });
394513
+ setStatus(gone.official ? "文件已删除 · 差异栏可 Keep 结案" : "文件已删除");
394514
+ }
394515
+ /** @private @author ddj 2026年10月28号 / 2026年10月31号 @param path Missing text file. @param sid Owning session. @param seq Loader generation. @sideEffects Loads deletion evidence without creating or saving the file. */
394429
394516
  async function loadMissing(path, sid, seq) {
394430
394517
  try {
394431
394518
  const result = await rpc("edrv.missingReview", {
@@ -394434,13 +394521,23 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
394434
394521
  });
394435
394522
  if (!loadCurrent(path, seq)) return;
394436
394523
  if (!result.ok) {
394437
- failLoad(path, sid, seq, result.error);
394524
+ settleGone(path, sid, seq, result);
394438
394525
  return;
394439
394526
  }
394440
394527
  if (!result.review) {
394441
- loadContent(path, sid, true);
394528
+ const attempts = (missingReadRef.current.get(path) ?? 0) + 1;
394529
+ missingReadRef.current.set(path, attempts);
394530
+ if (attempts <= 2) {
394531
+ loadContent(path, sid, true);
394532
+ return;
394533
+ }
394534
+ settleGone(path, sid, seq, {
394535
+ deleted: true,
394536
+ error: "文件状态在磁盘上反复变化,请稍后重试,或关闭该标签页"
394537
+ });
394442
394538
  return;
394443
394539
  }
394540
+ missingReadRef.current.delete(path);
394444
394541
  if (dirtyRef.current[path] || savingRef.current.has(path)) {
394445
394542
  failLoad(path, sid, seq, "文件已删除,但有未保存编辑;请先处理本地缓冲");
394446
394543
  return;
@@ -394453,6 +394550,7 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
394453
394550
  };
394454
394551
  missingRef.current.set(path, preview);
394455
394552
  setMissingPreview(preview);
394553
+ setGoneFile(null);
394456
394554
  setContent("");
394457
394555
  setContentPath(path);
394458
394556
  setLoadError(null);
@@ -394477,6 +394575,7 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
394477
394575
  status: "loading"
394478
394576
  });
394479
394577
  setLoadError(null);
394578
+ setGoneFile(null);
394480
394579
  setLargeCandidate(null);
394481
394580
  setPagedFile(null);
394482
394581
  setPageChanged(null);
@@ -396783,18 +396882,17 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
396783
396882
  hunkIndex: r.idx,
396784
396883
  decision: reject ? "rejected" : "accepted"
396785
396884
  });
396786
- /** @private @author ddj 2026年10月27号 @param reject Undo instead of Keep. @param blockId Optional contiguous block. @returns Persisted decision result; never reloads over a dirty buffer. */
396787
- async function decideOfficial(reject, blockId) {
396788
- if (!officialDetail || batchBusyRef.current) return false;
396789
- if (reject && (dirtyRef.current[active] || savingRef.current.has(active) || saveTimerRef.current.isPending())) {
396790
- setError("存在未保存或正在保存的修改,请等待完成后重试");
396791
- return false;
396792
- }
396793
- batchBusyRef.current = true;
396794
- const source = officialDetail.item;
396795
- const undoPath = active;
396796
- const undoModel = editorRef.current?.getModel?.();
396797
- const undoVersion = undoModel?.getVersionId?.();
396885
+ /**
396886
+ * @private
396887
+ * @author ddj 2026年10月31号
396888
+ * @description: 提交一条官方记录的判定;operationId 按决策键复用,网络重试保持幂等。
396889
+ * @param detail 目标记录(含冻结的 seq / revision)
396890
+ * @param reject true=Undo
396891
+ * @param blockId 可选的连续块;缺省为整文件
396892
+ * @returns 成功返回提交结果,失败抛出 host 给出的原因
396893
+ */
396894
+ async function submitOfficialDecision(detail, reject, blockId) {
396895
+ const source = detail.item;
396798
396896
  const opKey = JSON.stringify([
396799
396897
  sessionId,
396800
396898
  source.id,
@@ -396808,25 +396906,68 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
396808
396906
  operationId = crypto.randomUUID();
396809
396907
  reviewOpsRef.current.set(opKey, operationId);
396810
396908
  }
396909
+ const result = await rpc("edrv.review.decide", {
396910
+ sessionId,
396911
+ id: source.id,
396912
+ seq: source.seq,
396913
+ revision: source.revision,
396914
+ operationId,
396915
+ scope: blockId ? "hunk" : "file",
396916
+ blockId,
396917
+ decision: reject ? "rejected" : "accepted"
396918
+ });
396919
+ if (!result.ok) throw new Error(result.error);
396920
+ reviewOpsRef.current.delete(opKey);
396921
+ return result;
396922
+ }
396923
+ /**
396924
+ * @private
396925
+ * @author ddj 2026年10月31号
396926
+ * @description: 结案一条改动已不在当前文件中的官方记录:等价于 Keep(只标记已采纳,不改写磁盘)。
396927
+ * 这类记录没有可见差异可审,留在差异栏只会让用户去处理一个不存在的东西。
396928
+ * @param detail 待结案的记录
396929
+ */
396930
+ async function settleInvisibleRecord(detail) {
396931
+ if (batchBusyRef.current) return;
396932
+ const settled = settledRef.current;
396933
+ if (settled.has(detail.item.id)) return;
396934
+ settled.add(detail.item.id);
396935
+ batchBusyRef.current = true;
396811
396936
  try {
396812
- const result = await rpc("edrv.review.decide", {
396813
- sessionId,
396814
- id: source.id,
396815
- seq: source.seq,
396816
- revision: source.revision,
396817
- operationId,
396818
- scope: blockId ? "hunk" : "file",
396819
- blockId,
396820
- decision: reject ? "rejected" : "accepted"
396821
- });
396822
- if (!result.ok) throw new Error(result.error);
396823
- reviewOpsRef.current.delete(opKey);
396937
+ await submitOfficialDecision(detail, false);
396938
+ refreshReview(sessionId);
396939
+ } catch (cause) {
396940
+ settled.delete(detail.item.id);
396941
+ dbg(sessionId, `[DEBUG settleInvisible] id=${detail.item.id} err=${String(cause).slice(0, 160)}`);
396942
+ } finally {
396943
+ batchBusyRef.current = false;
396944
+ }
396945
+ }
396946
+ /** @private @author ddj 2026年10月27号 @param reject Undo instead of Keep. @param blockId Optional contiguous block. @returns Persisted decision result; never reloads over a dirty buffer. */
396947
+ async function decideOfficial(reject, blockId) {
396948
+ dbg(sessionId, `[DEBUG decideOfficial] reject=${reject ? 1 : 0} detail=${officialDetail ? 1 : 0} busy=${batchBusyRef.current ? 1 : 0} active=${active ?? "(null)"}`);
396949
+ if (!officialDetail || batchBusyRef.current) {
396950
+ if (!officialDetail && !batchBusyRef.current) setError("官方差异详情尚未加载完成,请稍后重试;若一直如此,请切换文件或刷新页面。");
396951
+ return false;
396952
+ }
396953
+ if (reject && (dirtyRef.current[active] || savingRef.current.has(active) || saveTimerRef.current.isPending())) {
396954
+ setError("存在未保存或正在保存的修改,请等待完成后重试");
396955
+ return false;
396956
+ }
396957
+ batchBusyRef.current = true;
396958
+ const detail = officialDetail;
396959
+ const undoPath = active;
396960
+ const undoModel = editorRef.current?.getModel?.();
396961
+ const undoVersion = undoModel?.getVersionId?.();
396962
+ try {
396963
+ await submitOfficialDecision(detail, reject, blockId);
396824
396964
  if (reject && activeRef.current === undoPath && !dirtyRef.current[undoPath] && !savingRef.current.has(undoPath) && undoModel?.getVersionId?.() === undoVersion) reloadFile(true);
396825
396965
  else if (reject) setError("Undo 已完成,编辑器在等待期间变化;未覆盖当前缓冲区,请核对磁盘后刷新");
396826
396966
  refreshReview(sessionId);
396827
396967
  setStatus(reject ? "已 Undo 选中差异" : "已 Keep 选中差异");
396828
396968
  return true;
396829
396969
  } catch (cause) {
396970
+ dbg(sessionId, `[DEBUG decideOfficialFail] id=${detail.item.id} reject=${reject ? 1 : 0} err=${String(cause).slice(0, 160)}`);
396830
396971
  setError(String(cause));
396831
396972
  return false;
396832
396973
  } finally {
@@ -397043,13 +397184,24 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
397043
397184
  }
397044
397185
  return operationId;
397045
397186
  }
397187
+ /**
397188
+ * @private
397189
+ * @author ddj 2026年10月31号
397190
+ * @description: 批量操作要求官方待审列表已就绪;切换会话后列表还在载入时,
397191
+ * 先触发一次刷新并给出可读提示,而不是直接报错中止(用户看到红色「尚未就绪」会以为功能坏了)。
397192
+ * @param action 面向用户的动作名
397193
+ * @returns 列表已就绪时为 true
397194
+ */
397195
+ function reviewReadyForBatch(action) {
397196
+ if (review.page?.overview) return true;
397197
+ refreshReview(sessionId);
397198
+ setStatus(`官方待审列表正在载入,载入完成后请再次点击${action}`);
397199
+ return false;
397200
+ }
397046
397201
  /** @private @author ddj 2026年10月27号 @param reject Undo or Keep all workspace pending records, including previous rounds. @returns Explicit partial results; legacy proceeds only when official stage fully succeeds. */
397047
397202
  async function actReviewAll(reject) {
397048
397203
  if (batchBusyRef.current) return;
397049
- if (!review.page?.overview) {
397050
- setError("官方待审列表尚未就绪,未执行批量操作");
397051
- return;
397052
- }
397204
+ if (!reviewReadyForBatch(reject ? "Undo All" : "Keep All")) return;
397053
397205
  if (!sum.pendingFiles.length) return;
397054
397206
  const token = reviewLifeRef.current;
397055
397207
  const undoPath = activeRef.current;
@@ -397116,10 +397268,7 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
397116
397268
  /** @private @author ddj 2026年10月27号 @param reject Whether to undo pending files. @description Open a DSH confirmation without starting any mutations. */
397117
397269
  function askReviewAll(reject) {
397118
397270
  if (batchBusyRef.current || !sum.pendingFiles.length) return;
397119
- if (!review.page?.overview) {
397120
- setError("官方待审列表尚未就绪,未执行批量操作");
397121
- return;
397122
- }
397271
+ if (!reviewReadyForBatch(reject ? "Undo All" : "Keep All")) return;
397123
397272
  setReviewAllPrompt({
397124
397273
  reject,
397125
397274
  count: sum.pendingFiles.length,
@@ -398458,6 +398607,37 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
398458
398607
  className: "edrv-pill edrv-pill-ghost",
398459
398608
  onClick: retry
398460
398609
  }, "重试") : null);
398610
+ /**
398611
+ * 渲染缺失文件终态面板:说明 + 重试 + 关闭页签(不显示进度条,避免卡在 0%)。
398612
+ * @private @author ddj 2026年10月31号
398613
+ * @param gone 终态信息
398614
+ * @param retry 重新读取回调
398615
+ * @param close 关闭该标签页回调
398616
+ * @returns 终态面板 React 元素
398617
+ */
398618
+ const goneBody = (gone, retry, close) => react$1.default.createElement("div", {
398619
+ className: "edrv-empty edrv-loading",
398620
+ "aria-live": "polite",
398621
+ role: "status"
398622
+ }, react$1.default.createElement("div", { className: "edrv-loading-title" }, gone.title), react$1.default.createElement("div", { style: {
398623
+ fontSize: 12,
398624
+ maxWidth: 460,
398625
+ lineHeight: 1.6
398626
+ } }, gone.message), gone.official ? react$1.default.createElement("div", { style: {
398627
+ fontSize: 12,
398628
+ maxWidth: 460,
398629
+ lineHeight: 1.6,
398630
+ opacity: .8
398631
+ } }, "待审改动在差异栏:Keep 保留删除结果并结案;Undo 需要该文件的原始内容,磁盘已删除时不可用。") : null, react$1.default.createElement("div", { style: {
398632
+ display: "flex",
398633
+ gap: 8
398634
+ } }, react$1.default.createElement("button", {
398635
+ className: "edrv-pill edrv-pill-ghost",
398636
+ onClick: retry
398637
+ }, "重试"), react$1.default.createElement("button", {
398638
+ className: "edrv-pill edrv-pill-ghost",
398639
+ onClick: close
398640
+ }, "关闭标签页")));
398461
398641
  let body;
398462
398642
  if (activePlan) {
398463
398643
  const labels = PLAN_MD_LABELS;
@@ -398498,6 +398678,12 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
398498
398678
  }
398499
398679
  }, "重试"));
398500
398680
  else if (!active) body = react$1.default.createElement("div", { className: "edrv-empty" }, react$1.default.createElement("div", null, "暂无打开的文件"), react$1.default.createElement("div", { style: { fontSize: 12 } }, "使用右上搜索框 (" + (chordOf("edrv.quickOpen") ?? "Ctrl+P") + ") 打开工作区文件;" + (chordOf("edrv.showCommands") ?? "Ctrl+Shift+P") + " 打开命令栏;agent 修改文件后顶部会出现差异角标"));
398681
+ else if (goneNow) body = goneBody(goneNow, () => {
398682
+ missingReadRef.current.delete(goneNow.path);
398683
+ loadContent(active, sessionId, true);
398684
+ }, () => {
398685
+ closeTab(goneNow.path);
398686
+ });
398501
398687
  else if (content === null && loadError && isSessionMissingError(loadError)) body = react$1.default.createElement("div", {
398502
398688
  className: "edrv-empty edrv-loading",
398503
398689
  "aria-live": "polite"
@@ -398617,6 +398803,7 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
398617
398803
  const sessionDown = isSessionMissingError(rawReviewError);
398618
398804
  const reviewErrorTone = sessionDown ? "info" : "error";
398619
398805
  const reviewErrorText = !rawReviewError ? void 0 : sessionDown ? SESSION_DOWN_NOTICE : sessionErrorText(rawReviewError);
398806
+ const reviewLoading = review.page === null && !review.error;
398620
398807
  const buildDockSnapshot = () => ({
398621
398808
  mode: editorDockMode(active),
398622
398809
  missingDeletion: !!missingNow,
@@ -398644,7 +398831,7 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
398644
398831
  onUndoFile: undoFile,
398645
398832
  onAcceptAllFiles: acceptAllFiles,
398646
398833
  onUndoAllFiles: undoAllFiles,
398647
- allPendingCount: allPending.length + (review.page?.pending ?? 0),
398834
+ allPendingCount: reviewLoading ? 0 : allPending.length + (review.page?.pending ?? 0),
398648
398835
  reviewBatchBusy,
398649
398836
  batchUnavailable: !review.page?.overview ? "官方待审列表尚未就绪" : void 0,
398650
398837
  onRollback: rollbackFile,
@@ -398661,7 +398848,7 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
398661
398848
  contentReady,
398662
398849
  diffTotal: displayDiffTotal(contentReady, pendingRegions.length, sum.files.find((file) => sameFile(file.path, active))?.pending ?? 0),
398663
398850
  fileIdx,
398664
- fileTotal: sum.pendingFiles.length,
398851
+ fileTotal: reviewLoading ? null : sum.pendingFiles.length,
398665
398852
  onPrevDiff: () => gotoDiff(-1),
398666
398853
  onNextDiff: () => gotoDiff(1),
398667
398854
  onPrevFile: () => gotoFile(-1),
package/lib/client.js.map CHANGED
@@ -1,4 +1,4 @@
1
1
  [diffend] Oversized file quarantined before diffing.
2
2
  name: package/lib/client.js.map
3
- size: 35662220 bytes
4
- sha256: 0204857e50fdc9778b1c9c5e5b128880ec0af54eb8d94edbd9d2b38cb2a63d3d
3
+ size: 35677585 bytes
4
+ sha256: df1afa8466a7852d100cfbc6a1f44e61beabd3b1808a262562e920ac06b96865