dsh-vscode-mode 0.21.0 → 0.21.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
@@ -477,18 +477,19 @@ DSH 每次启动监听随机端口,`http://127.0.0.1:<port>` 就是浏览器 o
477
477
 
478
478
  官方 `dsh plugin` 方式,三选一。下面以 `web` profile 为例;Desktop 或其他命名
479
479
  profile 请替换为实际目标,不要把示例目录当成当前运行 profile。升级前可阅读
480
- **v0.21.0** 新增 XLSX 基础编辑与浏览器导出副本,原件不自动写回,高级应用通过官方 Host 打开;官方差异定位统一 BOM 与 LF/CRLF/混合行尾口径,并区分内容缺失和多处命中。定位修复不扩展 CRLF patch Undo 能力,目标官方记录 ↑/↓ 的真实点击验收仍保留缺口。保留 **v0.20.5** 跨 DSH 实例只读互通——桌面端与 Web 端各自运行独立宿主时,官方审查列表/详情与工作区只读接口改按持久化会话头的 cwd 解析,另一实例创建的会话也能只读打开官方差异栏,写入类接口仍要求本端活跃会话。保留 **v0.20.4** 统一两套差异栏外观——非官方记录栏角标由内部术语「侧车记录」改为「历史改动」,悬停说明 Keep 只标记已看过、不改写磁盘,Undo 逐项校验后写回,两者都不影响官方审查;官方元数据不可用时给出可读原因与重试按钮,不再静默降级;「会话没在当前端激活」由红色告警改为中性说明。保留 v0.20.3 修复官方差异全部无法定位时差异栏无提示、上下箭头静默禁用(显示「无可跳转」与失效原因,Keep 说明只标记已采纳、不改写磁盘,会话缺失等宿主错误改为可读提示)、v0.20.2 的 MCP 状态告警收敛与原生确认弹窗,以及 v0.20.1 的文件内差异展示、块级/文件级安全审查和跨分页批量操作。升级后重启 DSH 并刷新页面;持久化与回退边界见 [0.20.0 发布说明](docs/release-0.20.0.md);从 0.14.2 或更旧版本升级还须阅读
480
+ **v0.21.2** 的新版 DSH 会话 Controller 与 MCP 公开工具目录适配:任务发送按目标会话持有生命周期,受理不明时不跨通道重发;文件范围引用的剪贴板与降级文本保留行区间;MCP 初始化失败增加脱敏持久化诊断。当前 Web Host 已完成真实 CodeGraph 调用,但首次连接偶发超时根因尚未证实,不宣称已修复,见 [0.21.2 发布说明](docs/release-0.21.2.md)。保留以下历史版本能力与验收边界:
481
+ **v0.21.1** 修复缺失历史文件:按完整快照显示整文件删除,Keep 保留删除、Undo 安全恢复快照,预览只读且拒绝覆盖重新出现的文件;真实 GUI 验收缺口已明确保留,见 [0.21.1 发布说明](docs/release-0.21.1.md)。保留 **v0.21.0** 新增 XLSX 基础编辑与浏览器导出副本,原件不自动写回,高级应用通过官方 Host 打开;官方差异定位统一 BOM 与 LF/CRLF/混合行尾口径,并区分内容缺失和多处命中。定位修复不扩展 CRLF patch Undo 能力,目标官方记录 ↑/↓ 的真实点击验收仍保留缺口。保留 **v0.20.5** 跨 DSH 实例只读互通——桌面端与 Web 端各自运行独立宿主时,官方审查列表/详情与工作区只读接口改按持久化会话头的 cwd 解析,另一实例创建的会话也能只读打开官方差异栏,写入类接口仍要求本端活跃会话。保留 **v0.20.4** 统一两套差异栏外观——非官方记录栏角标由内部术语「侧车记录」改为「历史改动」,悬停说明 Keep 只标记已看过、不改写磁盘,Undo 逐项校验后写回,两者都不影响官方审查;官方元数据不可用时给出可读原因与重试按钮,不再静默降级;「会话没在当前端激活」由红色告警改为中性说明。保留 v0.20.3 修复官方差异全部无法定位时差异栏无提示、上下箭头静默禁用(显示「无可跳转」与失效原因,Keep 说明只标记已采纳、不改写磁盘,会话缺失等宿主错误改为可读提示)、v0.20.2 的 MCP 状态告警收敛与原生确认弹窗,以及 v0.20.1 的文件内差异展示、块级/文件级安全审查和跨分页批量操作。升级后重启 DSH 并刷新页面;持久化与回退边界见 [0.20.0 发布说明](docs/release-0.20.0.md);从 0.14.2 或更旧版本升级还须阅读
481
482
  [0.14.3 数据迁移与回退说明](docs/release-0.14.3.md)。
482
483
 
483
484
  ```bash
484
485
  # ① Git 安装(clone + prepare 构建;推荐打固定 tag)
485
- dsh plugin --profile web add github:Lenonss/DSH_VsCodeMode#v0.21.0
486
+ dsh plugin --profile web add github:Lenonss/DSH_VsCodeMode#v0.21.2
486
487
 
487
488
  # ② npm 注册表(发布到 npm 后)
488
- dsh plugin --profile web add dsh-vscode-mode@0.21.0
489
+ dsh plugin --profile web add dsh-vscode-mode@0.21.2
489
490
 
490
491
  # ③ GitHub Release tgz 直装(Release asset 可用后)
491
- dsh plugin --profile web add https://github.com/Lenonss/DSH_VsCodeMode/releases/download/v0.21.0/dsh-vscode-mode-0.21.0.tgz
492
+ dsh plugin --profile web add https://github.com/Lenonss/DSH_VsCodeMode/releases/download/v0.21.2/dsh-vscode-mode-0.21.2.tgz
492
493
  ```
493
494
 
494
495
  > **DSH 版本政策(0.17.0 起)**:六项 DSH peer 仍统一声明为 `*`,另以
@@ -829,6 +830,8 @@ Keep All / Undo All 却是亮的。修复:单文件 Keep / Undo 覆盖冲突
829
830
  完整变更见 [GitHub Releases](https://github.com/Lenonss/DSH_VsCodeMode/releases)。
830
831
  近期关键版本:
831
832
 
833
+ - **v0.21.2**:适配新版 DSH Session Controller 与 MCP 公开工具目录;固定目标任务发送持有会话直到完成或卸载,受理不明时不跨通道重发。范围文件引用的模型文本、剪贴板与纯文本降级保持一致。MCP 初始化失败记录阶段、耗时和脱敏错误链;迁移官方 native/PTC 执行器集成测试并增加可选真实 CodeGraph stdio 验证。当前 Web Host 真实工具调用已通过,首次连接偶发超时根因仍未证实;不自动部署或重启。见 [发布说明](docs/release-0.21.2.md)。
834
+ - **v0.21.1**:缺失历史文件读取完整快照并整文件显示删除行;Keep 保留删除,Undo 安全恢复当前展示快照,不覆盖重新出现的文件。删除预览只读,单文件和批量操作统一处理,记录变化或证据不足时停止。真实 GUI 验收缺口已获用户接受;发布不自动部署,升级后重启 DSH 并刷新页面。见 [发布说明](docs/release-0.21.1.md)。
832
835
  - **v0.21.0**:新增 XLSX 基础编辑、多工作表与浏览器导出副本,原件不自动写回;不支持结构只读预览,高级应用经官方 Host 打开原件。编辑状态仅在当前页面内保留,不承诺刷新或崩溃恢复。官方差异定位统一 BOM 与 LF/CRLF/混合行尾口径,区分内容缺失与多处命中,保持原始字节及写回校验边界;不扩展 CRLF patch Undo 能力,目标官方记录 ↑/↓ 的真实点击仍未验收。升级后重启 DSH 并刷新页面;发布不自动部署当前实例。
833
836
  - **v0.20.5**:**跨实例官方审查数据只读互通**——桌面端与 Web 端各自运行独立 DSH 宿主时,会话只存在于创建它的宿主内存里,插件原先一律用本进程的活跃会话解析工作区,另一端打开官方差异栏会拿到「会话不存在」并退化成历史改动栏。现在官方审查列表/详情与工作区只读接口(文件读取、原始字节、分页文本、记录桶、归档列表、目录列表、界面状态镜像)改为按持久化会话头的 cwd 解析:只要会话在共享的 `~/.dsh/sessions` 下留有会话头,其他实例即可只读访问它的官方审查数据;写入类接口(保存、回滚、采纳/拒绝、批量决策)仍要求本端活跃会话,避免两个宿主并发写同一工作区的审查库。已知限制:会话内 `cd` 过的记录只能按创建时的 cwd 冷读;桌面端本版本未重新部署,反向仍不可用。升级后重启 DSH 并刷新页面。
834
837
  - **v0.20.4**:**两套差异栏不再互相冒充,内部术语退出界面**——插件本来并存官方审查栏与另一条历史记录栏(旧机制把每次改动留痕写进工作区 `.dsh-edit-review.json`),过去两者外观相同、无任何标识,容易被误认为插件未升级。现在非官方栏使用与官方栏同一套版式,但角标改为「历史改动」并在悬浮说明里讲清:这些改动没有进入官方审查列表,Keep 只标记看过、不改文件,Undo 会校验后写回,两者都不影响官方审查;设置页的「侧车摘要」一并对齐为日常说法。官方元数据通道失败(`edrv.review.list` 报错,如 `会话不存在`,或宿主未提供官方文件导航)时,在栏上方给出可读原因与重试按钮,不再静默降级;其中「会话没在当前端激活」由红色告警改为中性说明并存原始详情,真正的磁盘/回滚冲突错误仍保持红色。仅调整展示层,未改动数据库结构、决策语义与回退校验。
package/lib/client.js CHANGED
@@ -377810,6 +377810,44 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
377810
377810
  return regions;
377811
377811
  }
377812
377812
  //#endregion
377813
+ //#region src/client/missingReview.ts
377814
+ /** @public @author ddj 2026年10月28号 @param review Frozen complete snapshot. @returns One whole-file deletion, including empty files; no invented current lines. */
377815
+ function missingRegion(review) {
377816
+ return {
377817
+ callId: review.callIds[0],
377818
+ idx: 0,
377819
+ start: 1,
377820
+ end: 1,
377821
+ oldStart: 1,
377822
+ oldLines: splitLines(review.content.replace(/\r\n/g, "\n")),
377823
+ newLines: [],
377824
+ whole: true,
377825
+ status: "pending",
377826
+ create: false,
377827
+ superseded: false,
377828
+ sourceKind: "missing"
377829
+ };
377830
+ }
377831
+ /** @public @author ddj 2026年10月28号 @param paths Batch paths. @param api Guarded Host access. @returns Missing paths handled once; a partial failure stops before legacy hunk writes. @throws Missing evidence, changed lifecycle or Host decision failure. */
377832
+ async function missingBatch(paths, api) {
377833
+ const candidates = [];
377834
+ for (const path of new Set(paths)) {
377835
+ api.guard();
377836
+ const result = await api.query(path);
377837
+ if (!result.ok) throw new Error(result.error);
377838
+ if (result.review) candidates.push(result.review);
377839
+ }
377840
+ const completed = /* @__PURE__ */ new Set();
377841
+ for (const review of candidates) {
377842
+ api.guard();
377843
+ const result = await api.decide(review, api.operation(review));
377844
+ if (!result.ok) throw new Error(`已处理 ${completed.size} 个删除文件,其余未处理。${result.error}`);
377845
+ completed.add(review.path);
377846
+ api.committed(review, result.callIds);
377847
+ }
377848
+ return completed;
377849
+ }
377850
+ //#endregion
377813
377851
  //#region src/client/editorModelState.ts
377814
377852
  /** Monaco 编辑器实例选择器(限定在插件编辑器行内,排除官方预览等其它 Monaco)。 */
377815
377853
  const EDITOR_MODEL_SELECTOR = ".edrv-editor-row .monaco-editor";
@@ -382728,7 +382766,10 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
382728
382766
  onUndoAll: props.onUndoAllFiles,
382729
382767
  keepTitle: "把这批历史改动全部标记为已看过(只改记录,不动磁盘文件)",
382730
382768
  undoTitle: "逐项撤销这些历史改动;缺少证据或遇到冲突即停止"
382731
- }), busy ? /* @__PURE__ */ react$1.default.createElement("div", {
382769
+ }), props.missingDeletion ? /* @__PURE__ */ react$1.default.createElement("div", {
382770
+ className: "edrv-review-notice",
382771
+ role: "status"
382772
+ }, "文件已删除 · 展示最后记录快照。Keep 保留删除;Undo 恢复展示的完整快照。") : null, busy ? /* @__PURE__ */ react$1.default.createElement("div", {
382732
382773
  className: "edrv-review-notice",
382733
382774
  role: "status"
382734
382775
  }, "正在处理历史改动…") : null, down ? /* @__PURE__ */ react$1.default.createElement("div", {
@@ -382766,7 +382807,7 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
382766
382807
  icon: barIcon("undo"),
382767
382808
  onClick: props.onUndoFile,
382768
382809
  disabled: !canDecide,
382769
- title: SIDECAR_UNDO_TITLE
382810
+ title: props.missingDeletion ? "恢复展示的完整历史快照(不会覆盖重新出现的文件)" : SIDECAR_UNDO_TITLE
382770
382811
  }, "Undo"), /* @__PURE__ */ react$1.default.createElement(_deepseek_ai_dsh_client_ui_primitives.Button, {
382771
382812
  size: "sm",
382772
382813
  variant: "primary",
@@ -382774,7 +382815,7 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
382774
382815
  icon: barIcon("check"),
382775
382816
  onClick: props.onAcceptFile,
382776
382817
  disabled: !canDecide,
382777
- title: SIDECAR_KEEP_TITLE
382818
+ title: props.missingDeletion ? "保持文件删除并结清覆盖的待审历史记录,不写文件" : SIDECAR_KEEP_TITLE
382778
382819
  }, "Keep"), /* @__PURE__ */ react$1.default.createElement(_deepseek_ai_dsh_client_ui_primitives.Button, {
382779
382820
  size: "sm",
382780
382821
  variant: "ghost",
@@ -384367,25 +384408,57 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
384367
384408
  if (!range || range.endLine < range.startLine) return mention;
384368
384409
  return range.startLine === range.endLine ? `${mention} L${range.startLine}` : `${mention} L${range.startLine}-${range.endLine}`;
384369
384410
  }
384370
- /** 构造 DSH 文件引用插入载荷(source=reference,发送序列化为 mention(+range) 文本)。
384411
+ /** 构造 DSH 文件引用插入载荷,模型文本、剪贴板投影与降级文本共用完整行区间。
384412
+ * @public
384413
+ * @author ddj 2026年10月28号
384414
+ * @returns 引用载荷与含可选行区间的完整引用文本
384371
384415
  * @param path 目标路径(相对 cwd 或绝对)
384372
384416
  * @param cwd 会话工作区目录(可选)
384373
384417
  * @param range 行区间(可选)
384374
384418
  * @param appearance 引用外观:'file'(默认)| 'folder'
384375
384419
  */
384376
384420
  function buildFileRef(path, cwd, range, appearance = "file") {
384377
- const mention = mentionOf(path, cwd);
384421
+ const mention = refOf(mentionOf(path, cwd), range);
384378
384422
  return {
384379
384423
  mention,
384380
384424
  reference: {
384381
384425
  source: REF_SOURCE,
384382
- ref: refOf(mention, range),
384426
+ ref: mention,
384383
384427
  label: labelOf(path, range),
384384
384428
  appearance,
384385
384429
  clipboardText: mention
384386
384430
  }
384387
384431
  };
384388
384432
  }
384433
+ /** @private @author ddj 2026年10月28号 @param ctx Lifecycle owner. @param sessions Public Controller. @param id Fixed target. @param text Task body. @returns Acceptance; failures never retry another transport. @sideEffects Retains one generation until completion or unload. */
384434
+ async function sendOwned(ctx, sessions, id, text) {
384435
+ const controller = new AbortController();
384436
+ const reference = sessions.retain(id, {
384437
+ source: "controllerOperation",
384438
+ signal: controller.signal
384439
+ });
384440
+ let released = false;
384441
+ /** @private @author ddj 2026年10月28号 Cancel pending admission and release this exact generation once. */
384442
+ function release() {
384443
+ if (released) return;
384444
+ released = true;
384445
+ controller.abort();
384446
+ reference.release();
384447
+ }
384448
+ let remove;
384449
+ try {
384450
+ remove = ctx.effect?.(() => release, "vscode-mode: task session reference");
384451
+ const binding = await reference.ready;
384452
+ if (controller.signal.aborted) return false;
384453
+ return (await binding.session.prompt([{
384454
+ type: "text",
384455
+ text
384456
+ }], "queue", controller.signal)).ok === true;
384457
+ } finally {
384458
+ release();
384459
+ remove?.();
384460
+ }
384461
+ }
384389
384462
  /** apiproxy wire 端点(POST /api/<method>;浏览器同源 fetch 自带 cookie 鉴权;实证 401=未鉴权而非 404)。 */
384390
384463
  const SESSION_CREATE_PATH = "/api/session/create";
384391
384464
  /** apiproxy wire 端点:发送任务(session/prompt;点击即发送方案)。 */
@@ -384484,8 +384557,9 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
384484
384557
  }
384485
384558
  /**
384486
384559
  * 新建独立会话承载旁路任务(02-deep-session-prompt:深度分析不污染当前对话)。
384487
- * 主路径 = apiproxy wire 直调(不依赖 service 面形态猜测);回落 remote.session.create
384488
- * 与旧 sessions.create 两代 service 面(宽松解析)。任一步不符返回 ok:false,调用方降级。
384560
+ * 优先公开 sessions.create;旧 Host 按能力选择 remote 或 wire,调用后不换通道重试。
384561
+ * 受理状态不明时返回 ok:false,不重复创建会话。
384562
+ * @author ddj 2026年10月28号
384489
384563
  * @author ddj 2026年09月23号
384490
384564
  * @param ctx 客户端服务上下文(remote.session + sessions,仅回落用)
384491
384565
  * @param cwd 新会话工作区目录(与当前会话同工作区)
@@ -384502,32 +384576,25 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
384502
384576
  id
384503
384577
  };
384504
384578
  }
384505
- } catch {}
384579
+ if (typeof sessions?.create === "function") return { ok: false };
384580
+ } catch {
384581
+ return { ok: false };
384582
+ }
384506
384583
  try {
384507
384584
  const remote = ctx.get("remote.session");
384508
384585
  if (typeof remote?.create === "function") {
384509
384586
  const payload = cwd ? { cwd } : {};
384510
- for (const arg of [
384511
- slotArg(payload),
384512
- { request: {
384513
- rpcId: mintRpcId(),
384514
- payload
384515
- } },
384516
- {
384517
- rpcId: mintRpcId(),
384518
- payload
384519
- },
384520
- payload
384521
- ]) try {
384522
- const r = rpcOk(await remote.create(arg));
384523
- const id = r.value;
384524
- if (r.ok && typeof id?.sessionId === "string") return {
384525
- ok: true,
384526
- id: id.sessionId
384527
- };
384528
- } catch {}
384587
+ const receipt = rpcOk(await remote.create(slotArg(payload)));
384588
+ const created = receipt.value;
384589
+ if (receipt.ok && typeof created?.sessionId === "string") return {
384590
+ ok: true,
384591
+ id: created.sessionId
384592
+ };
384593
+ return { ok: false };
384529
384594
  }
384530
- } catch {}
384595
+ } catch {
384596
+ return { ok: false };
384597
+ }
384531
384598
  try {
384532
384599
  const id = await apiSessionCreate(cwd);
384533
384600
  if (id) return {
@@ -384549,7 +384616,7 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
384549
384616
  try {
384550
384617
  const sessions = ctx.get("sessions");
384551
384618
  const conversation = ctx.get("conversation");
384552
- const actx = sessions?.scope?.(sessionId);
384619
+ const actx = typeof sessions?.binding === "function" ? sessions.binding(sessionId)?.ctx : sessions?.scope?.(sessionId);
384553
384620
  const shell = actx && conversation?.input?.for?.(actx);
384554
384621
  return shell && typeof shell.insertReference === "function" ? shell : void 0;
384555
384622
  } catch {
@@ -384666,13 +384733,15 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
384666
384733
  /**
384667
384734
  * 新建独立会话(02-deep-session-prompt:旁路任务不污染当前对话)。
384668
384735
  * 闭包持有真服务 ctx(面板 SidebarCtx 无 .get,会话操作必须经此门面);
384669
- * 创建后尽力 `sessions.open(id)` 切换视图(缺失不致命,草稿仍可写入)。
384736
+ * 新版仅返回已登记身份,视图选择由宿主负责;旧版保留可用的 open/select。
384737
+ * @author ddj 2026年10月28号
384670
384738
  * @author ddj 2026年09月23号
384671
384739
  * @param cwd 新会话工作区目录(与当前会话同工作区)
384672
384740
  * @returns 新会话结果(失败供降级)
384673
384741
  */
384674
384742
  const startDraftSession = async (cwd) => {
384675
384743
  const result = await newDraftSession(ctx, cwd);
384744
+ if (typeof ctx.get("sessions")?.retain === "function") return result;
384676
384745
  if (result.ok && result.id) try {
384677
384746
  const sessions = ctx.get("sessions");
384678
384747
  sessions?.materializeScope?.(result.id);
@@ -384685,30 +384754,23 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
384685
384754
  };
384686
384755
  /**
384687
384756
  * 发送任务给目标会话 agent(点击即发送方案:任务落地会话即非 blank,列表可见)。
384688
- * 主路径 wire 直发 session/prompt;回落 remote.session.prompt service 面。
384689
- * @author ddj 2026年09月23号
384757
+ * 新版持有目标 Controller 到 prompt 结束;旧版按能力选择唯一通道,不跨通道重发。
384758
+ * @author ddj 2026年09月23号 / 2026年10月28号
384690
384759
  * @param sessionId 目标会话 id
384691
384760
  * @param text 任务文本
384692
384761
  * @returns 是否受理
384693
384762
  */
384694
384763
  const sendTask = async (sessionId, text) => {
384695
384764
  if (!sessionId) return false;
384696
- const composerOnce = async () => {
384765
+ try {
384766
+ const sessions = ctx.get("sessions");
384767
+ if (typeof sessions?.retain === "function") return await sendOwned(ctx, sessions, sessionId, text);
384697
384768
  const input = inputFor(ctx, sessionId);
384698
- if (input && typeof input.setDraft === "function" && typeof input.submit === "function") {
384769
+ if (typeof input?.setDraft === "function" && typeof input.submit === "function") {
384699
384770
  input.setDraft(text);
384700
384771
  input.submit();
384701
384772
  return true;
384702
384773
  }
384703
- return false;
384704
- };
384705
- try {
384706
- for (let attempt = 3; attempt > 0; attempt--) {
384707
- if (await composerOnce()) return true;
384708
- if (attempt > 1) await new Promise((resolve) => setTimeout(resolve, 500));
384709
- }
384710
- } catch {}
384711
- try {
384712
384774
  const remote = ctx.get("remote.session");
384713
384775
  if (typeof remote?.prompt === "function") {
384714
384776
  const payload = {
@@ -384719,26 +384781,12 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
384719
384781
  text
384720
384782
  }]
384721
384783
  };
384722
- for (const arg of [
384723
- slotArg(payload),
384724
- { request: {
384725
- rpcId: mintRpcId(),
384726
- payload
384727
- } },
384728
- {
384729
- rpcId: mintRpcId(),
384730
- payload
384731
- },
384732
- payload
384733
- ]) try {
384734
- if (rpcOk(await remote.prompt(arg)).ok) return true;
384735
- } catch {}
384784
+ return rpcOk(await remote.prompt(slotArg(payload))).ok;
384736
384785
  }
384737
- } catch {}
384738
- try {
384739
384786
  return await apiSessionPrompt(sessionId, text);
384740
- } catch {}
384741
- return false;
384787
+ } catch {
384788
+ return false;
384789
+ }
384742
384790
  };
384743
384791
  return {
384744
384792
  appendReference,
@@ -393389,6 +393437,9 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
393389
393437
  const [dirtyMap, setDirtyMap] = react$1.default.useState({});
393390
393438
  const [content, setContent] = react$1.default.useState(null);
393391
393439
  const [contentPath, setContentPath] = react$1.default.useState(null);
393440
+ const [missingPreview, setMissingPreview] = react$1.default.useState(null);
393441
+ const missingRef = react$1.default.useRef(/* @__PURE__ */ new Map());
393442
+ const missingNow = missingPreview?.path === active && missingPreview?.sessionId === sessionId ? missingPreview : null;
393392
393443
  const [receiptLoad, setReceiptLoad] = react$1.default.useState(null);
393393
393444
  const [imageSrc, setImageSrc] = react$1.default.useState(null);
393394
393445
  const [imgSize, setImgSize] = react$1.default.useState(null);
@@ -393751,7 +393802,10 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
393751
393802
  const regions = react$1.default.useMemo(() => {
393752
393803
  const trace = isDebug() && contentReady && currentRecords.length > 0;
393753
393804
  const start = trace ? performance.now() : 0;
393754
- const next = officialDetail ? reviewRegions(officialDetail.diff, officialDetail.item.id, officialDetail.blocks ?? [], contentReady ? content : null) : diffRegions(currentRecords, contentReady ? content : null).filter((r) => !r.superseded);
393805
+ let next;
393806
+ if (officialDetail) next = reviewRegions(officialDetail.diff, officialDetail.item.id, officialDetail.blocks ?? [], contentReady ? content : null);
393807
+ else if (missingNow && contentReady) next = missingNow.resolved ? [] : [missingRegion(missingNow)];
393808
+ else next = diffRegions(currentRecords, contentReady ? content : null).filter((r) => !r.superseded);
393755
393809
  if (trace) try {
393756
393810
  dbg(sessionId, `[DEBUG diffRegions] records=${currentRecords.length} bytes=${content.length} regions=${next.length} ms=${(performance.now() - start).toFixed(1)}`);
393757
393811
  } catch (e) {}
@@ -393761,7 +393815,16 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
393761
393815
  content,
393762
393816
  contentPath,
393763
393817
  active,
393764
- officialDetail
393818
+ officialDetail,
393819
+ missingNow
393820
+ ]);
393821
+ react$1.default.useEffect(() => {
393822
+ editorRef.current?.updateOptions({ readOnly: !!missingNow });
393823
+ }, [
393824
+ missingNow,
393825
+ active,
393826
+ content,
393827
+ monaco
393765
393828
  ]);
393766
393829
  const pendingRegions = react$1.default.useMemo(() => regions.filter((r) => r.status === ST.PENDING && !r.stale), [regions]);
393767
393830
  const staleRegions = react$1.default.useMemo(() => regions.filter((r) => r.status === ST.PENDING && r.stale), [regions]);
@@ -394236,7 +394299,13 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
394236
394299
  /** @author ddj 2026年09月28号 @param res Disk content for a still-clean cached model. */
394237
394300
  const accept = (res) => {
394238
394301
  if (!loadCurrent(path, sequence) || dirtyRef.current[path] || model.getValue() !== before) return;
394302
+ if (res?.code === "file-missing") {
394303
+ loadMissing(path, sid, sequence);
394304
+ return;
394305
+ }
394239
394306
  if (!res?.ok) return;
394307
+ missingRef.current.delete(path);
394308
+ setMissingPreview(null);
394240
394309
  mark(res.version);
394241
394310
  if (res.content === before) return;
394242
394311
  saveViewState(path);
@@ -394259,6 +394328,48 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
394259
394328
  * @param force 强制走 RPC(reloadFile 用,跳过 model 复用)
394260
394329
  * @param version 已知磁盘版本(缺省走 RPC 返回值)
394261
394330
  */
394331
+ /** @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. */
394332
+ async function loadMissing(path, sid, seq) {
394333
+ try {
394334
+ const result = await rpc("edrv.missingReview", {
394335
+ sessionId: sid,
394336
+ path
394337
+ });
394338
+ if (!loadCurrent(path, seq)) return;
394339
+ if (!result.ok) {
394340
+ failLoad(path, sid, seq, result.error);
394341
+ return;
394342
+ }
394343
+ if (!result.review) {
394344
+ loadContent(path, sid, true);
394345
+ return;
394346
+ }
394347
+ if (dirtyRef.current[path] || savingRef.current.has(path)) {
394348
+ failLoad(path, sid, seq, "文件已删除,但有未保存编辑;请先处理本地缓冲");
394349
+ return;
394350
+ }
394351
+ const preview = {
394352
+ ...result.review,
394353
+ sessionId: sid,
394354
+ keepId: crypto.randomUUID(),
394355
+ undoId: crypto.randomUUID()
394356
+ };
394357
+ missingRef.current.set(path, preview);
394358
+ setMissingPreview(preview);
394359
+ setContent("");
394360
+ setContentPath(path);
394361
+ setLoadError(null);
394362
+ setError(null);
394363
+ stampLoad(path, sid, seq);
394364
+ setLoadStage({
394365
+ progress: 100,
394366
+ message: "文件已删除 · 展示最后记录快照"
394367
+ });
394368
+ setStatus("文件已删除 · 展示最后记录快照(只读)");
394369
+ } catch (cause) {
394370
+ if (loadCurrent(path, seq)) failLoad(path, sid, seq, String(cause));
394371
+ }
394372
+ }
394262
394373
  const loadContent = (path, sid, force, version) => {
394263
394374
  const seq = ++loadSeqRef.current;
394264
394375
  setReceiptLoad({
@@ -394319,7 +394430,7 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
394319
394430
  loadPdf(path, sid, seq, force === true, version);
394320
394431
  return;
394321
394432
  }
394322
- const cachedModel = force ? null : modelsRef.current.get(path);
394433
+ const cachedModel = force || missingRef.current.has(path) ? null : modelsRef.current.get(path);
394323
394434
  if (cachedModel) {
394324
394435
  setContent(cachedModel.getValue());
394325
394436
  setContentPath(path);
@@ -394346,6 +394457,8 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
394346
394457
  } catch (e) {}
394347
394458
  if (!loadCurrent(path, seq)) return;
394348
394459
  if (res && res.ok) {
394460
+ missingRef.current.delete(path);
394461
+ setMissingPreview(null);
394349
394462
  mark(res.version, true);
394350
394463
  setContent(res.content);
394351
394464
  setContentPath(path);
@@ -394356,6 +394469,10 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
394356
394469
  }));
394357
394470
  setStatus("已加载");
394358
394471
  } else {
394472
+ if (res?.code === "file-missing") {
394473
+ loadMissing(path, sid, seq);
394474
+ return;
394475
+ }
394359
394476
  if (res?.code === "file-too-large") setLargeCandidate(JSON.stringify([
394360
394477
  scope,
394361
394478
  sid,
@@ -394606,6 +394723,7 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
394606
394723
  });
394607
394724
  return;
394608
394725
  }
394726
+ if (change.kind === "deleted" && change.path === activeRef.current && !dirtyRef.current[change.path] && !savingRef.current.has(change.path)) loadContent(change.path, sessionId, true);
394609
394727
  const kind = change.kind === "deleted" ? "deleted" : "conflict";
394610
394728
  markDiskFlag(readSync(scope, change.path) ?? {
394611
394729
  path: change.path,
@@ -395591,6 +395709,10 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
395591
395709
  /** @private @author ddj 2026年10月14号 @param path Canonical path. @param manual Explicit save. @returns Guarded save, never another pane's content. */
395592
395710
  function saveDocument(path, manual) {
395593
395711
  if (isExcelPath(path)) return Promise.resolve({ ok: false });
395712
+ if (missingRef.current.has(path)) {
395713
+ setStatus("删除预览只读;请使用 Keep / Undo");
395714
+ return Promise.resolve({ ok: false });
395715
+ }
395594
395716
  const model = modelsRef.current.get(path);
395595
395717
  if (!model) return Promise.resolve({ ok: false });
395596
395718
  const store = documents();
@@ -395662,7 +395784,7 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
395662
395784
  const ed = editorRef.current;
395663
395785
  const model = ed?.getModel();
395664
395786
  const path = [...modelsRef.current].find(([, item]) => item === model)?.[0];
395665
- if (!ed || !path) return;
395787
+ if (!ed || !path || missingRef.current.has(path)) return;
395666
395788
  setDirtyMap((d) => Object.assign({}, d, { [path]: true }));
395667
395789
  touchTab(path);
395668
395790
  setStatus("编辑中…");
@@ -395796,6 +395918,7 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
395796
395918
  const side = layoutRef.current === "side";
395797
395919
  const ed = m.editor.create(node, {
395798
395920
  value: "",
395921
+ readOnly: missingRef.current.has(activeRef.current),
395799
395922
  language: "plaintext",
395800
395923
  theme: themeNameOf(),
395801
395924
  fontFamily: "var(--ds-font-family-code, ui-monospace, monospace)",
@@ -396443,6 +396566,7 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
396443
396566
  * @returns Promise<boolean> 是否成功
396444
396567
  */
396445
396568
  const actHunk = (region, reject, silent) => {
396569
+ if (region.sourceKind === "missing") return decideMissing(reject);
396446
396570
  if (region.sourceKind === "official") return decideOfficial(reject, region.id);
396447
396571
  const method = reject ? "edrv.reject" : "edrv.accept";
396448
396572
  const trace = isDebug();
@@ -396612,7 +396736,57 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
396612
396736
  batchBusyRef.current = false;
396613
396737
  }
396614
396738
  }
396739
+ /** @private @author ddj 2026年10月28号 @param reject Restore the shown snapshot instead of keeping absence. @returns Safe whole-file decision, never a hunk reversal. */
396740
+ async function decideMissing(reject) {
396741
+ if (batchBusyRef.current || !missingNow || missingNow.resolved) return false;
396742
+ const preview = missingNow;
396743
+ const lifetime = reviewLifeRef.current;
396744
+ batchBusyRef.current = true;
396745
+ setReviewBatchBusy(true);
396746
+ try {
396747
+ guardReviewAll(reject, lifetime);
396748
+ const result = await rpc("edrv.missingDecide", {
396749
+ sessionId,
396750
+ token: preview.token,
396751
+ operationId: reject ? preview.undoId : preview.keepId,
396752
+ undo: reject
396753
+ });
396754
+ if (!lifetime?.live) return false;
396755
+ if (!result.ok) {
396756
+ setError(result.error);
396757
+ return false;
396758
+ }
396759
+ setRecords((previous) => Object.fromEntries(Object.entries(previous).filter(([id]) => !result.callIds.includes(id))));
396760
+ missingRef.current.set(preview.path, {
396761
+ ...preview,
396762
+ resolved: true
396763
+ });
396764
+ if (activeRef.current === preview.path) {
396765
+ if (reject) loadContent(preview.path, sessionId, true);
396766
+ else setMissingPreview({
396767
+ ...preview,
396768
+ resolved: true
396769
+ });
396770
+ }
396771
+ refreshRecords();
396772
+ emitRefresh({
396773
+ scope,
396774
+ paths: [preview.path],
396775
+ kind: "structure"
396776
+ });
396777
+ setStatus(reject ? "已恢复展示的完整历史快照" : "已保留文件删除");
396778
+ setError(null);
396779
+ return true;
396780
+ } catch (cause) {
396781
+ if (lifetime?.live) setError(String(cause));
396782
+ return false;
396783
+ } finally {
396784
+ batchBusyRef.current = false;
396785
+ if (lifetime?.live) setReviewBatchBusy(false);
396786
+ }
396787
+ }
396615
396788
  const acceptFile = () => {
396789
+ if (missingNow && !officialDetail) return decideMissing(false);
396616
396790
  if (officialDetail) return decideOfficial(false);
396617
396791
  if (batchBusyRef.current || !decideRegions.length) return;
396618
396792
  batchBusyRef.current = true;
@@ -396631,6 +396805,7 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
396631
396805
  });
396632
396806
  };
396633
396807
  const undoFile = () => {
396808
+ if (missingNow && !officialDetail) return decideMissing(true);
396634
396809
  if (officialDetail) return decideOfficial(true);
396635
396810
  if (batchBusyRef.current || !decideRegions.length) return;
396636
396811
  batchBusyRef.current = true;
@@ -396668,24 +396843,85 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
396668
396843
  * @author ddj 2026年08月20号
396669
396844
  * @param reject true=全部不采纳(回滚),false=全部采纳
396670
396845
  */
396671
- const actAllPending = (reject) => {
396846
+ const actAllPending = async (reject) => {
396672
396847
  if (batchBusyRef.current || !allPending.length) return;
396848
+ const lifetime = reviewLifeRef.current;
396673
396849
  batchBusyRef.current = true;
396674
396850
  const list = reject ? [...allPending].sort((a, b) => a.at < b.at ? 1 : a.at > b.at ? -1 : b.idx - a.idx) : allPending;
396675
396851
  const paths = [...new Set(list.map((r) => records[r.callId]?.path).filter(Boolean))];
396676
- return actMany(list.map((r) => itemOf(r, reject))).then((result) => {
396677
- batchBusyRef.current = false;
396678
- reloadFile(true);
396852
+ try {
396853
+ const handled = await missingBatch(paths, {
396854
+ query: async (path) => {
396855
+ const cached = missingRef.current.get(path);
396856
+ if (cached?.sessionId === sessionId && !cached.resolved) return {
396857
+ ok: true,
396858
+ review: cached
396859
+ };
396860
+ const result = await rpc("edrv.missingReview", {
396861
+ sessionId,
396862
+ path
396863
+ });
396864
+ if (result.ok && result.review) {
396865
+ const preview = {
396866
+ ...result.review,
396867
+ sessionId,
396868
+ keepId: crypto.randomUUID(),
396869
+ undoId: crypto.randomUUID()
396870
+ };
396871
+ missingRef.current.set(path, preview);
396872
+ return {
396873
+ ok: true,
396874
+ review: preview
396875
+ };
396876
+ }
396877
+ return result;
396878
+ },
396879
+ decide: (preview, operationId) => rpc("edrv.missingDecide", {
396880
+ sessionId,
396881
+ token: preview.token,
396882
+ operationId,
396883
+ undo: reject
396884
+ }),
396885
+ guard: () => guardReviewAll(reject, lifetime),
396886
+ operation: (preview) => reject ? preview.undoId : preview.keepId,
396887
+ committed: (preview, callIds) => {
396888
+ if (!lifetime?.live) return;
396889
+ missingRef.current.set(preview.path, {
396890
+ ...preview,
396891
+ resolved: true
396892
+ });
396893
+ setRecords((previous) => Object.fromEntries(Object.entries(previous).filter(([id]) => !callIds.includes(id))));
396894
+ if (!reject && sameFile(activeRef.current, preview.path)) setMissingPreview({
396895
+ ...preview,
396896
+ path: activeRef.current,
396897
+ sessionId,
396898
+ resolved: true
396899
+ });
396900
+ emitRefresh({
396901
+ scope,
396902
+ paths: [preview.path],
396903
+ kind: "structure"
396904
+ });
396905
+ }
396906
+ });
396907
+ guardReviewAll(reject, lifetime);
396908
+ const remaining = list.filter((item) => !handled.has(records[item.callId]?.path));
396909
+ const result = await actMany(remaining.map((item) => itemOf(item, reject)));
396910
+ if (!lifetime?.live) return;
396911
+ if (reject || !missingNow) reloadFile(true);
396679
396912
  emitRefresh({
396680
396913
  scope,
396681
- paths: paths.length ? paths : void 0,
396914
+ paths,
396682
396915
  kind: "structure"
396683
396916
  });
396684
- setStatus(decideStatus(reject ? "已不采纳 " : "已采纳 ", result));
396685
- if (result.fail) setError(result.fail + " 处差异处理失败(可能已被后续修改影响),可刷新后重试");
396686
- }).catch(() => {
396917
+ setStatus(decideStatus(reject ? "已不采纳 " : "已采纳 ", result) + `;删除文件 ${handled.size} 个`);
396918
+ if (result.fail) setError(result.fail + " 处差异处理失败,可刷新后重试");
396919
+ } catch (cause) {
396920
+ if (lifetime?.live) setError(String(cause));
396921
+ } finally {
396687
396922
  batchBusyRef.current = false;
396688
- });
396923
+ if (lifetime?.live) refreshRecords();
396924
+ }
396689
396925
  };
396690
396926
  /** @private @author ddj 2026年10月27号 @param reject Undo requires every local buffer to be saved. @param token Starting session lifetime. @returns Throws before the next mutation when unsafe. */
396691
396927
  function guardReviewAll(reject, token) {
@@ -398286,6 +398522,7 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
398286
398522
  const reviewErrorText = !rawReviewError ? void 0 : sessionDown ? SESSION_DOWN_NOTICE : sessionErrorText(rawReviewError);
398287
398523
  const buildDockSnapshot = () => ({
398288
398524
  mode: editorDockMode(active),
398525
+ missingDeletion: !!missingNow,
398289
398526
  official: officialFile ? {
398290
398527
  sessionId,
398291
398528
  page: review.page,