dsh-vscode-mode 0.23.1 → 0.23.2

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,7 @@ Keep All / Undo All 却是亮的。修复:单文件 Keep / Undo 覆盖冲突
879
880
  完整变更见 [GitHub Releases](https://github.com/Lenonss/DSH_VsCodeMode/releases)。
880
881
  近期关键版本:
881
882
 
883
+ - **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
884
  - **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
885
  - **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
886
  - **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 与客户端共用同一套「远端 / 盘符 / 相对路径」判定,只在平台折叠上分工。
@@ -382531,7 +382532,8 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
382531
382532
  * @returns 持久化的全工作区控制条
382532
382533
  */
382533
382534
  function ReviewAllRow(props) {
382534
- const total = props.total ?? 0;
382535
+ const known = typeof props.total === "number";
382536
+ const total = known ? props.total : 0;
382535
382537
  const current = total && props.current > 0 ? props.current : 0;
382536
382538
  const disabled = props.busy || !!props.unavailable || !props.pending;
382537
382539
  return /* @__PURE__ */ react$1.default.createElement("div", {
@@ -382544,7 +382546,7 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
382544
382546
  className: "edrv-review-total",
382545
382547
  onClick: props.onOpen,
382546
382548
  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, {
382549
+ }, "差异文件 ", /* @__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
382550
  size: "sm",
382549
382551
  variant: "ghost",
382550
382552
  onClick: props.onKeepAll,
@@ -382591,6 +382593,19 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
382591
382593
  "aria-hidden": "true"
382592
382594
  }, /* @__PURE__ */ react$1.default.createElement("path", { d: marks[mark] }));
382593
382595
  }
382596
+ /**
382597
+ * @private
382598
+ * @author ddj 2026年10月31号
382599
+ * @description: Keep 按钮提示。会话不在本端活跃时 Keep 仍可执行(只写共享审查库),
382600
+ * 提示里补一句说明,免得用户把它当成跨端不可用的按钮。
382601
+ * @param hint 差异栏给出的 Keep 说明
382602
+ * @param live 会话是否在本端活跃
382603
+ * @returns 按钮 title 文本
382604
+ */
382605
+ function keepTitle(hint, live) {
382606
+ if (live) return hint;
382607
+ return `${hint}(会话在另一端激活:Keep 写入共享审查库,仍可执行)`;
382608
+ }
382594
382609
  /** @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
382610
  function OfficialDiffBar(props) {
382596
382611
  const { official, pendingRegions = [], staleRegions = [], diffIdx = 0 } = props;
@@ -382647,7 +382662,7 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
382647
382662
  role: "region",
382648
382663
  "aria-label": "文件差异审查"
382649
382664
  }, /* @__PURE__ */ react$1.default.createElement(ReviewAllRow, {
382650
- total: props.fileTotal ?? 0,
382665
+ total: props.fileTotal ?? null,
382651
382666
  current: (props.fileTotal ?? 0) && (props.fileIdx ?? -1) >= 0 ? (props.fileIdx ?? 0) + 1 : 0,
382652
382667
  busy: props.reviewBatchBusy,
382653
382668
  unavailable: props.batchUnavailable,
@@ -382692,7 +382707,7 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
382692
382707
  icon: barIcon("check"),
382693
382708
  onClick: props.onAcceptFile,
382694
382709
  disabled: props.reviewBatchBusy || !detail?.capability.keep,
382695
- title: detail?.capability.keep ? view.keepHint : detail?.capability.reason
382710
+ title: detail?.capability.keep ? keepTitle(view.keepHint, detail.capability.live !== false) : detail?.capability.reason
382696
382711
  }, "Keep"), /* @__PURE__ */ react$1.default.createElement(_deepseek_ai_dsh_client_ui_primitives.Menu, {
382697
382712
  open: menu === "more",
382698
382713
  anchor: moreTrigger,
@@ -382780,7 +382795,7 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
382780
382795
  role: "region",
382781
382796
  "aria-label": "文件差异审查"
382782
382797
  }, /* @__PURE__ */ react$1.default.createElement(ReviewAllRow, {
382783
- total: props.fileTotal ?? 0,
382798
+ total: props.fileTotal ?? null,
382784
382799
  current: (props.fileTotal ?? 0) && (props.fileIdx ?? -1) >= 0 ? (props.fileIdx ?? 0) + 1 : 0,
382785
382800
  busy,
382786
382801
  pending: props.allPendingCount,
@@ -393802,6 +393817,7 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
393802
393817
  review.page
393803
393818
  ]);
393804
393819
  const reviewOpsRef = react$1.default.useRef(/* @__PURE__ */ new Map());
393820
+ const settledRef = react$1.default.useRef(/* @__PURE__ */ new Set());
393805
393821
  const [officialDetail, setOfficialDetail] = react$1.default.useState(null);
393806
393822
  const [officialRecords, setOfficialRecords] = react$1.default.useState([]);
393807
393823
  const [selectedReview, setSelectedReview] = react$1.default.useState(null);
@@ -393816,7 +393832,9 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
393816
393832
  react$1.default.useEffect(() => {
393817
393833
  const controller = new AbortController();
393818
393834
  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;
393835
+ const coveredPath = review.page?.overview?.coveredPaths.find((path) => sameFile(path, active));
393836
+ const reviewPath = coveredPath ?? pendingPath;
393837
+ 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
393838
  if (!active || !reviewPath) {
393821
393839
  setOfficialDetail(null);
393822
393840
  setOfficialRecords([]);
@@ -393832,6 +393850,7 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
393832
393850
  if (controller.signal.aborted) return;
393833
393851
  if (!page.ok) throw new Error(page.error);
393834
393852
  const selected = page.page.items.find((item) => item.id === selectedReview) ?? page.page.items[0];
393853
+ dbg(sessionId, `[DEBUG reviewList] path=${reviewPath} items=${page.page.items.length} waiting=${page.page.pending} selected=${selected?.id ?? "(none)"}`);
393835
393854
  if (!selected) {
393836
393855
  setOfficialRecords([]);
393837
393856
  setOfficialDetail(null);
@@ -393842,6 +393861,7 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
393842
393861
  id: selected.id
393843
393862
  }, controller.signal);
393844
393863
  if (controller.signal.aborted) return;
393864
+ 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
393865
  if (!result.ok) throw new Error(result.error);
393846
393866
  setOfficialRecords((previous) => JSON.stringify(previous) === JSON.stringify(page.page.items) ? previous : page.page.items);
393847
393867
  setOfficialDetail((previous) => JSON.stringify(previous) === JSON.stringify(result.detail) ? previous : result.detail);
@@ -393929,6 +393949,20 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
393929
393949
  const staleAmbiguous = staleRegions.filter((region) => region.staleReason === "ambiguous").length;
393930
393950
  const noBlocks = !!officialDetail && (officialDetail.diff.kind !== "text" || officialDetail.diff.hunks.length === 0);
393931
393951
  const decideRegions = react$1.default.useMemo(() => [...pendingRegions, ...staleRegions], [pendingRegions, staleRegions]);
393952
+ react$1.default.useEffect(() => {
393953
+ if (!officialDetail || contentReady !== true || batchBusyRef.current) return;
393954
+ if (pendingRegions.length > 0 || staleRegions.length === 0) return;
393955
+ if (noBlocks || officialDetail.capability?.keep !== true) return;
393956
+ if (dirtyRef.current[active]) return;
393957
+ settleInvisibleRecord(officialDetail);
393958
+ }, [
393959
+ officialDetail,
393960
+ contentReady,
393961
+ pendingRegions,
393962
+ staleRegions,
393963
+ noBlocks,
393964
+ active
393965
+ ]);
393932
393966
  hoverRegionsRef.current = pendingRegions;
393933
393967
  lineRegionMapRef.current = react$1.default.useMemo(() => {
393934
393968
  const map = /* @__PURE__ */ new Map();
@@ -396783,18 +396817,17 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
396783
396817
  hunkIndex: r.idx,
396784
396818
  decision: reject ? "rejected" : "accepted"
396785
396819
  });
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?.();
396820
+ /**
396821
+ * @private
396822
+ * @author ddj 2026年10月31号
396823
+ * @description: 提交一条官方记录的判定;operationId 按决策键复用,网络重试保持幂等。
396824
+ * @param detail 目标记录(含冻结的 seq / revision)
396825
+ * @param reject true=Undo
396826
+ * @param blockId 可选的连续块;缺省为整文件
396827
+ * @returns 成功返回提交结果,失败抛出 host 给出的原因
396828
+ */
396829
+ async function submitOfficialDecision(detail, reject, blockId) {
396830
+ const source = detail.item;
396798
396831
  const opKey = JSON.stringify([
396799
396832
  sessionId,
396800
396833
  source.id,
@@ -396808,25 +396841,68 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
396808
396841
  operationId = crypto.randomUUID();
396809
396842
  reviewOpsRef.current.set(opKey, operationId);
396810
396843
  }
396844
+ const result = await rpc("edrv.review.decide", {
396845
+ sessionId,
396846
+ id: source.id,
396847
+ seq: source.seq,
396848
+ revision: source.revision,
396849
+ operationId,
396850
+ scope: blockId ? "hunk" : "file",
396851
+ blockId,
396852
+ decision: reject ? "rejected" : "accepted"
396853
+ });
396854
+ if (!result.ok) throw new Error(result.error);
396855
+ reviewOpsRef.current.delete(opKey);
396856
+ return result;
396857
+ }
396858
+ /**
396859
+ * @private
396860
+ * @author ddj 2026年10月31号
396861
+ * @description: 结案一条改动已不在当前文件中的官方记录:等价于 Keep(只标记已采纳,不改写磁盘)。
396862
+ * 这类记录没有可见差异可审,留在差异栏只会让用户去处理一个不存在的东西。
396863
+ * @param detail 待结案的记录
396864
+ */
396865
+ async function settleInvisibleRecord(detail) {
396866
+ if (batchBusyRef.current) return;
396867
+ const settled = settledRef.current;
396868
+ if (settled.has(detail.item.id)) return;
396869
+ settled.add(detail.item.id);
396870
+ batchBusyRef.current = true;
396871
+ try {
396872
+ await submitOfficialDecision(detail, false);
396873
+ refreshReview(sessionId);
396874
+ } catch (cause) {
396875
+ settled.delete(detail.item.id);
396876
+ dbg(sessionId, `[DEBUG settleInvisible] id=${detail.item.id} err=${String(cause).slice(0, 160)}`);
396877
+ } finally {
396878
+ batchBusyRef.current = false;
396879
+ }
396880
+ }
396881
+ /** @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. */
396882
+ async function decideOfficial(reject, blockId) {
396883
+ dbg(sessionId, `[DEBUG decideOfficial] reject=${reject ? 1 : 0} detail=${officialDetail ? 1 : 0} busy=${batchBusyRef.current ? 1 : 0} active=${active ?? "(null)"}`);
396884
+ if (!officialDetail || batchBusyRef.current) {
396885
+ if (!officialDetail && !batchBusyRef.current) setError("官方差异详情尚未加载完成,请稍后重试;若一直如此,请切换文件或刷新页面。");
396886
+ return false;
396887
+ }
396888
+ if (reject && (dirtyRef.current[active] || savingRef.current.has(active) || saveTimerRef.current.isPending())) {
396889
+ setError("存在未保存或正在保存的修改,请等待完成后重试");
396890
+ return false;
396891
+ }
396892
+ batchBusyRef.current = true;
396893
+ const detail = officialDetail;
396894
+ const undoPath = active;
396895
+ const undoModel = editorRef.current?.getModel?.();
396896
+ const undoVersion = undoModel?.getVersionId?.();
396811
396897
  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);
396898
+ await submitOfficialDecision(detail, reject, blockId);
396824
396899
  if (reject && activeRef.current === undoPath && !dirtyRef.current[undoPath] && !savingRef.current.has(undoPath) && undoModel?.getVersionId?.() === undoVersion) reloadFile(true);
396825
396900
  else if (reject) setError("Undo 已完成,编辑器在等待期间变化;未覆盖当前缓冲区,请核对磁盘后刷新");
396826
396901
  refreshReview(sessionId);
396827
396902
  setStatus(reject ? "已 Undo 选中差异" : "已 Keep 选中差异");
396828
396903
  return true;
396829
396904
  } catch (cause) {
396905
+ dbg(sessionId, `[DEBUG decideOfficialFail] id=${detail.item.id} reject=${reject ? 1 : 0} err=${String(cause).slice(0, 160)}`);
396830
396906
  setError(String(cause));
396831
396907
  return false;
396832
396908
  } finally {
@@ -397043,13 +397119,24 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
397043
397119
  }
397044
397120
  return operationId;
397045
397121
  }
397122
+ /**
397123
+ * @private
397124
+ * @author ddj 2026年10月31号
397125
+ * @description: 批量操作要求官方待审列表已就绪;切换会话后列表还在载入时,
397126
+ * 先触发一次刷新并给出可读提示,而不是直接报错中止(用户看到红色「尚未就绪」会以为功能坏了)。
397127
+ * @param action 面向用户的动作名
397128
+ * @returns 列表已就绪时为 true
397129
+ */
397130
+ function reviewReadyForBatch(action) {
397131
+ if (review.page?.overview) return true;
397132
+ refreshReview(sessionId);
397133
+ setStatus(`官方待审列表正在载入,载入完成后请再次点击${action}`);
397134
+ return false;
397135
+ }
397046
397136
  /** @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
397137
  async function actReviewAll(reject) {
397048
397138
  if (batchBusyRef.current) return;
397049
- if (!review.page?.overview) {
397050
- setError("官方待审列表尚未就绪,未执行批量操作");
397051
- return;
397052
- }
397139
+ if (!reviewReadyForBatch(reject ? "Undo All" : "Keep All")) return;
397053
397140
  if (!sum.pendingFiles.length) return;
397054
397141
  const token = reviewLifeRef.current;
397055
397142
  const undoPath = activeRef.current;
@@ -397116,10 +397203,7 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
397116
397203
  /** @private @author ddj 2026年10月27号 @param reject Whether to undo pending files. @description Open a DSH confirmation without starting any mutations. */
397117
397204
  function askReviewAll(reject) {
397118
397205
  if (batchBusyRef.current || !sum.pendingFiles.length) return;
397119
- if (!review.page?.overview) {
397120
- setError("官方待审列表尚未就绪,未执行批量操作");
397121
- return;
397122
- }
397206
+ if (!reviewReadyForBatch(reject ? "Undo All" : "Keep All")) return;
397123
397207
  setReviewAllPrompt({
397124
397208
  reject,
397125
397209
  count: sum.pendingFiles.length,
@@ -398617,6 +398701,7 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
398617
398701
  const sessionDown = isSessionMissingError(rawReviewError);
398618
398702
  const reviewErrorTone = sessionDown ? "info" : "error";
398619
398703
  const reviewErrorText = !rawReviewError ? void 0 : sessionDown ? SESSION_DOWN_NOTICE : sessionErrorText(rawReviewError);
398704
+ const reviewLoading = review.page === null && !review.error;
398620
398705
  const buildDockSnapshot = () => ({
398621
398706
  mode: editorDockMode(active),
398622
398707
  missingDeletion: !!missingNow,
@@ -398644,7 +398729,7 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
398644
398729
  onUndoFile: undoFile,
398645
398730
  onAcceptAllFiles: acceptAllFiles,
398646
398731
  onUndoAllFiles: undoAllFiles,
398647
- allPendingCount: allPending.length + (review.page?.pending ?? 0),
398732
+ allPendingCount: reviewLoading ? 0 : allPending.length + (review.page?.pending ?? 0),
398648
398733
  reviewBatchBusy,
398649
398734
  batchUnavailable: !review.page?.overview ? "官方待审列表尚未就绪" : void 0,
398650
398735
  onRollback: rollbackFile,
@@ -398661,7 +398746,7 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
398661
398746
  contentReady,
398662
398747
  diffTotal: displayDiffTotal(contentReady, pendingRegions.length, sum.files.find((file) => sameFile(file.path, active))?.pending ?? 0),
398663
398748
  fileIdx,
398664
- fileTotal: sum.pendingFiles.length,
398749
+ fileTotal: reviewLoading ? null : sum.pendingFiles.length,
398665
398750
  onPrevDiff: () => gotoDiff(-1),
398666
398751
  onNextDiff: () => gotoDiff(1),
398667
398752
  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: 35670356 bytes
4
+ sha256: 4a298d4c08a69a99f2aa5a4626c9aa23ff928609a2af4f4cb9328d3ba4b25114
package/lib/index.js CHANGED
@@ -15971,6 +15971,10 @@ function finishUndo(cwd, request) {
15971
15971
  //#region src/reviewRpc.ts
15972
15972
  /** 批量写入快照的硬上限:待审积压超过它说明状态异常,宁可拒绝也不做无界写循环。 */
15973
15973
  const BATCH_LIMIT = 2e3;
15974
+ /** 冷端 Undo 说明:Undo 会校验证据并写回源文件,只能在持有该会话的那一端执行。 */
15975
+ const UNDO_NEEDS_LIVE = "Undo 需要在本端活跃的会话上执行:该会话当前在另一端(Web / 桌面)激活。Keep 不受影响,可在任一端执行;或在本端打开该会话后重试。";
15976
+ /** 冷端待审判定说明:待审库是按工作区的本地存储,判定需要与正在编辑的那一端保持一致。 */
15977
+ const PENDING_NEEDS_LIVE = "待审条目的判定需要在本端活跃的会话上执行:该会话当前在另一端激活,请在那一端处理,或在本端打开该会话后重试。";
15974
15978
  /**
15975
15979
  * @private
15976
15980
  * @author ddj 2026年10月31号
@@ -16020,19 +16024,61 @@ function batchSnapshot(cwd, pending) {
16020
16024
  * @returns 审查相关处理器表
16021
16025
  */
16022
16026
  function reviewHandlers(ctx, registry) {
16023
- /** @private @author ddj 2026年10月28号 @param sessionId 请求作用域 @returns 已迁移工作区(允许跨实例冷读) */
16024
- async function workspace(sessionId) {
16027
+ /**
16028
+ * @private
16029
+ * @author ddj 2026年10月31号
16030
+ * @description: 解析会话作用域,并标明本端是否持有该会话的活跃实例。
16031
+ * 工作区一律由 host 解析,绝不采信调用方 cwd;冷读作用域只带 cwd,不带会话实例。
16032
+ * @param sessionId 请求作用域
16033
+ * @returns 已迁移工作区与活跃标记
16034
+ * @throws 会话不存在、会话无工作区或活跃库迁移未完成
16035
+ */
16036
+ async function scopeOf(sessionId) {
16025
16037
  const scope = await resolveScope(ctx, sessionId);
16026
16038
  if ("err" in scope) throw new Error("会话不存在或没有工作区");
16027
16039
  await bucketOf(registry, ctx, scope.cwd);
16028
- return scope.cwd;
16040
+ return {
16041
+ cwd: scope.cwd,
16042
+ live: !!scope.session
16043
+ };
16029
16044
  }
16030
- /** @private @author ddj 2026年10月28号 @param sessionId 请求作用域 @returns 本端 live 会话的工作区;冷读作用域一律拒绝写入 */
16031
- async function liveWorkspace(sessionId) {
16032
- const scope = await resolveScope(ctx, sessionId);
16033
- if ("err" in scope || !scope.session) throw new Error("会话不存在或没有工作区");
16034
- await bucketOf(registry, ctx, scope.cwd);
16035
- return scope.cwd;
16045
+ /**
16046
+ * @private
16047
+ * @author ddj 2026年10月31号
16048
+ * @description: 本端是否持有该会话的活跃实例(纯诊断用,不解析工作区)。
16049
+ * @param sessionId 请求作用域
16050
+ * @returns 本端加载了该会话时为 true
16051
+ */
16052
+ function liveOf(sessionId) {
16053
+ return !!sessionOf(ctx, sessionId);
16054
+ }
16055
+ /**
16056
+ * @private
16057
+ * @author ddj 2026年10月31号
16058
+ * @description: 按本端是否活跃覆写面板能力位。活跃端保持数据库结论;
16059
+ * 冷端关闭依赖会话的操作(Undo、待审判定)并给出跨端原因,Keep 仍按数据库结论放行。
16060
+ * @param capability 数据库推导的能力位
16061
+ * @param live 本端是否持有活跃会话
16062
+ * @param pending 是否为待审投影条目
16063
+ * @returns 面板可用的能力位
16064
+ */
16065
+ function liveCapabilityOf(capability, live, pending) {
16066
+ if (live) return {
16067
+ ...capability,
16068
+ live: true
16069
+ };
16070
+ if (pending) return {
16071
+ keep: false,
16072
+ undo: false,
16073
+ reason: PENDING_NEEDS_LIVE,
16074
+ live: false
16075
+ };
16076
+ return {
16077
+ ...capability,
16078
+ undo: false,
16079
+ live: false,
16080
+ reason: capability.undo ? UNDO_NEEDS_LIVE : capability.reason
16081
+ };
16036
16082
  }
16037
16083
  /**
16038
16084
  * @private
@@ -16045,7 +16091,11 @@ function reviewHandlers(ctx, registry) {
16045
16091
  */
16046
16092
  function pendingPageOf(cwd, sessionId) {
16047
16093
  try {
16048
- return loadPage(cwd, sessionId);
16094
+ const page = loadPage(cwd, sessionId);
16095
+ return page.items.some((item) => item.settled === true) ? {
16096
+ ...page,
16097
+ items: page.items.filter((item) => item.settled !== true)
16098
+ } : page;
16049
16099
  } catch {
16050
16100
  return {
16051
16101
  items: [],
@@ -16057,7 +16107,7 @@ function reviewHandlers(ctx, registry) {
16057
16107
  }
16058
16108
  return {
16059
16109
  "edrv.review.list": async (args) => {
16060
- const cwd = await workspace(args.sessionId);
16110
+ const { cwd } = await scopeOf(args.sessionId);
16061
16111
  if (args.all && !args.history && args.path === void 0) return batchSnapshot(cwd, pendingPageOf(cwd, args.sessionId));
16062
16112
  const official = reviewPage(cwd, args.cursor, args.history, args.path);
16063
16113
  const pending = pendingPageOf(cwd, args.sessionId);
@@ -16077,63 +16127,97 @@ function reviewHandlers(ctx, registry) {
16077
16127
  }
16078
16128
  };
16079
16129
  }
16130
+ const merged = mergedPage(official, pending, cwd);
16131
+ log.debug(`[review] list live=${liveOf(args.sessionId) ? 1 : 0} path=${args.path ?? "(all)"} items=${merged.items.length} pending=${merged.pending} covered=${merged.overview?.coveredPaths.length ?? -1}`);
16080
16132
  return {
16081
16133
  ok: true,
16082
- page: mergedPage(official, pending, cwd)
16134
+ page: merged
16083
16135
  };
16084
16136
  },
16085
16137
  "edrv.review.detail": async (args) => {
16086
- const cwd = await workspace(args.sessionId);
16138
+ const scope = await scopeOf(args.sessionId);
16139
+ const cwd = scope.cwd;
16087
16140
  if (args.id.startsWith("pending:")) {
16088
16141
  const detail = loadDetail(cwd, args.id.slice(8));
16089
16142
  if (!detail) return {
16090
16143
  ok: false,
16091
16144
  error: "待审条目不存在"
16092
16145
  };
16146
+ const mapped = pendingDetailOf(detail, new Date(detail.item.updatedAt).toISOString(), cwd);
16093
16147
  return {
16094
16148
  ok: true,
16095
- detail: pendingDetailOf(detail, new Date(detail.item.updatedAt).toISOString(), cwd)
16149
+ detail: {
16150
+ ...mapped,
16151
+ capability: liveCapabilityOf(mapped.capability, scope.live, true)
16152
+ }
16096
16153
  };
16097
16154
  }
16098
16155
  const detail = reviewDetail(cwd, args.id);
16099
- return detail ? {
16156
+ if (!detail) {
16157
+ log.debug(`[review] detail live=${scope.live ? 1 : 0} id=${args.id} found=0`);
16158
+ return {
16159
+ ok: false,
16160
+ error: "官方审查记录不存在"
16161
+ };
16162
+ }
16163
+ const capability = liveCapabilityOf(detail.capability, scope.live, false);
16164
+ log.debug(`[review] detail live=${scope.live ? 1 : 0} id=${args.id} found=1 keep=${capability.keep ? 1 : 0} undo=${capability.undo ? 1 : 0}`);
16165
+ return {
16100
16166
  ok: true,
16101
- detail
16102
- } : {
16103
- ok: false,
16104
- error: "官方审查记录不存在"
16167
+ detail: {
16168
+ ...detail,
16169
+ capability
16170
+ }
16105
16171
  };
16106
16172
  },
16107
16173
  "edrv.review.decide": async (args) => {
16108
- const cwd = await liveWorkspace(args.sessionId);
16109
- if (args.id.startsWith("pending:")) {
16110
- const result = await applyDecide(ctx.get("fs"), cwd, {
16111
- sessionId: args.sessionId,
16112
- id: args.id.slice(8),
16113
- revision: args.revision,
16114
- operationId: args.operationId,
16115
- decision: args.decision,
16116
- scope: args.scope === "hunk" ? "block" : "file",
16117
- ...args.blockId === void 0 ? {} : { blockId: args.blockId }
16118
- });
16119
- if (!result.ok) return {
16120
- ok: false,
16121
- error: result.reason
16122
- };
16123
- return {
16124
- ok: true,
16125
- revision: result.flags.revision
16126
- };
16127
- }
16128
16174
  try {
16175
+ const scope = await scopeOf(args.sessionId);
16176
+ if (args.id.startsWith("pending:")) {
16177
+ if (!scope.live) return {
16178
+ ok: false,
16179
+ error: PENDING_NEEDS_LIVE
16180
+ };
16181
+ const result = await applyDecide(ctx.get("fs"), scope.cwd, {
16182
+ sessionId: args.sessionId,
16183
+ id: args.id.slice(8),
16184
+ revision: args.revision,
16185
+ operationId: args.operationId,
16186
+ decision: args.decision,
16187
+ scope: args.scope === "hunk" ? "block" : "file",
16188
+ ...args.blockId === void 0 ? {} : { blockId: args.blockId }
16189
+ });
16190
+ if (!result.ok) return {
16191
+ ok: false,
16192
+ error: result.reason
16193
+ };
16194
+ return {
16195
+ ok: true,
16196
+ revision: result.flags.revision
16197
+ };
16198
+ }
16199
+ if (args.decision === "rejected") {
16200
+ if (!scope.live) return {
16201
+ ok: false,
16202
+ error: UNDO_NEEDS_LIVE
16203
+ };
16204
+ return {
16205
+ ok: true,
16206
+ ...await undoReview(ctx, sessionOf(ctx, args.sessionId), scope.cwd, args)
16207
+ };
16208
+ }
16209
+ const result = decideReview(scope.cwd, args);
16210
+ if (!scope.live) log.info(`[review] 冷端 Keep 官方记录 ${args.id}`);
16129
16211
  return {
16130
16212
  ok: true,
16131
- ...args.decision === "rejected" ? await undoReview(ctx, sessionOf(ctx, args.sessionId), cwd, args) : decideReview(cwd, args)
16213
+ ...result
16132
16214
  };
16133
16215
  } catch (error) {
16216
+ const message = error instanceof Error ? error.message : String(error);
16217
+ log.warn(`[review] decide 失败 id=${args.id} decision=${args.decision}: ${message}`);
16134
16218
  return {
16135
16219
  ok: false,
16136
- error: error instanceof Error ? error.message : String(error)
16220
+ error: message
16137
16221
  };
16138
16222
  }
16139
16223
  }