dsh-vscode-mode 0.23.3 → 0.23.4

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
@@ -533,13 +533,13 @@ profile 请替换为实际目标,不要把示例目录当成当前运行 profi
533
533
 
534
534
  ```bash
535
535
  # ① Git 安装(clone + prepare 构建;推荐打固定 tag)
536
- dsh plugin --profile web add github:Lenonss/DSH_VsCodeMode#v0.23.1
536
+ dsh plugin --profile web add github:Lenonss/DSH_VsCodeMode#v0.23.4
537
537
 
538
538
  # ② npm 注册表(发布到 npm 后)
539
- dsh plugin --profile web add dsh-vscode-mode@0.23.1
539
+ dsh plugin --profile web add dsh-vscode-mode@0.23.4
540
540
 
541
541
  # ③ GitHub Release tgz 直装(Release asset 可用后)
542
- dsh plugin --profile web add https://github.com/Lenonss/DSH_VsCodeMode/releases/download/v0.23.1/dsh-vscode-mode-0.23.1.tgz
542
+ dsh plugin --profile web add https://github.com/Lenonss/DSH_VsCodeMode/releases/download/v0.23.4/dsh-vscode-mode-0.23.4.tgz
543
543
  ```
544
544
 
545
545
  > **DSH 版本政策(0.17.0 起)**:六项 DSH peer 仍统一声明为 `*`,另以
@@ -880,6 +880,7 @@ Keep All / Undo All 却是亮的。修复:单文件 Keep / Undo 覆盖冲突
880
880
  完整变更见 [GitHub Releases](https://github.com/Lenonss/DSH_VsCodeMode/releases)。
881
881
  近期关键版本:
882
882
 
883
+ - **v0.23.4**:修复桌面端点计划卡「查看全文」会先闪开一页官方计划预览、随即自动关闭。根因:官方卡片走 `ctx.sidebarRight.openResource(planAddress(callId))`,而本插件以 extension 优先级认领了 `dsh-resource://plan/**` 与 `dsh-resource://plan-review/**`,于是侧栏先按本插件类型建出一个页签(正文「正在打开计划预览…」),挂载后才把计划转进「文件编辑」、再把这个页签关掉——用户看到的就是这次建而即关。现由新增 `src/client/planDirect.ts` 在装配期包装 `sidebarRight.openResource`:只接管「2 段计划地址 + 会话为在屏会话」,正文依次取回顾卡片的 `params.planReview`、插件侧地址→正文缓存、可选 `resources.source()` 资源流(上限 1.5s),拿到正文后经 `requestOpen(..., { preview: true })` 直通本插件编辑器的只读计划页签,不再创建侧栏页签;非计划地址、跨会话与 subagent 5 段地址、正文不可得、编辑器 5s 未回执、以及服务对象只读导致包装赋值失败,一律回落原生 `openResource`,原「认领 + 挂载后转移」链路保留为后备。`openReceipt` 新增可选 `timeoutMs`,`planState` 新增有界(64 条)地址→正文缓存并由 live `exit_plan_mode` 写入。验证:typecheck 0 error;`tests/planDirect.test.ts`(新增 9 例)、`tests/planBridge.test.ts`(13)、`tests/planState.test.ts`(10)、`tests/openReceipt.test.ts`(9)四套 41/41;`npm run build` 双 bundle 通过(含客户端产物完整性校验);浏览器确认新 bundle 已加载且 Console 无「计划直通装配失败」;GUI 点按由用户在本机 Web 端手动复验后发布。见 [发布说明](docs/release-0.23.4.md)。
883
884
  - **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
885
  - **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)。
885
886
  - **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)。
package/lib/client.js CHANGED
@@ -2613,7 +2613,7 @@ window.__ModuleLoader__.load({
2613
2613
  * @param line Optional target line.
2614
2614
  * @param column Optional target column.
2615
2615
  * @param sessionId Owning session, required for routing.
2616
- * @param options Explicit preview, read-only plan document and optional cancellation signal.
2616
+ * @param options Explicit preview, read-only plan document, cancellation signal and receipt deadline.
2617
2617
  * @returns Actual completion; timeout and disposal are false.
2618
2618
  */
2619
2619
  function requestOpen(path, line, column, sessionId = "", options = {}) {
@@ -2647,7 +2647,7 @@ window.__ModuleLoader__.load({
2647
2647
  request,
2648
2648
  finish
2649
2649
  });
2650
- timer = setTimeout(() => finish(false), 2e4);
2650
+ timer = setTimeout(() => finish(false), options.timeoutMs ?? 2e4);
2651
2651
  openEditorView(null);
2652
2652
  window.dispatchEvent(new CustomEvent("edrv:open-request", { detail: request }));
2653
2653
  });
@@ -376030,6 +376030,8 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
376030
376030
  //#endregion
376031
376031
  //#region src/client/planState.ts
376032
376032
  const associated = /* @__PURE__ */ new Map();
376033
+ /** 地址 → 已解析计划正文(只留内存,供后续点击省掉一次资源解析)。 */
376034
+ const documents = /* @__PURE__ */ new Map();
376033
376035
  /** @private @author ddj 2026年10月08号 @param value JSON candidate. @returns Object or empty record. */
376034
376036
  function record(value) {
376035
376037
  return value !== null && typeof value === "object" ? value : {};
@@ -376096,9 +376098,25 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
376096
376098
  associated.set(sessionId, paths);
376097
376099
  if (associated.size > 32) associated.delete(associated.keys().next().value);
376098
376100
  }
376101
+ /** @public @author ddj 2026年10月31号
376102
+ * 记住一份已解析计划,供「查看全文」时免去资源解析。
376103
+ * @param plan 已解析计划。
376104
+ * @returns 无;只缓存合法地址与正文,超过上限按插入序淘汰。
376105
+ */
376106
+ function rememberPlan(plan) {
376107
+ const valid = planDocument(plan.address, plan);
376108
+ if (valid === void 0) return;
376109
+ documents.set(valid.address, valid);
376110
+ if (documents.size > 64) documents.delete(documents.keys().next().value);
376111
+ }
376112
+ /** @public @author ddj 2026年10月31号 @param address 官方资源地址。 @returns 已缓存的计划正文。 */
376113
+ function cachedPlan(address) {
376114
+ return documents.get(address);
376115
+ }
376099
376116
  /** @public @author ddj 2026年10月08号 Clear associations when the plugin unloads. */
376100
376117
  function clearPlanState() {
376101
376118
  associated.clear();
376119
+ documents.clear();
376102
376120
  }
376103
376121
  /** @public @author ddj 2026年10月08号
376104
376122
  * Reduce only live append deltas; hydration, pagination, reconnect and duplicate sequences never open plans.
@@ -376134,6 +376152,7 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
376134
376152
  if (event.type === "tool/result") return void 0;
376135
376153
  const plan = planDocument("dsh-resource://plan/" + encodeURIComponent(sessionId) + "/" + encodeURIComponent(id), { markdown: call.args.plan });
376136
376154
  if (plan) {
376155
+ rememberPlan(plan);
376137
376156
  remember(id);
376138
376157
  return { plan };
376139
376158
  }
@@ -412357,19 +412376,269 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
412357
412376
  return directTranslate();
412358
412377
  }
412359
412378
  //#endregion
412360
- //#region src/client/planBridge.ts
412361
- /** Optional DSH plan integration; no provider or approval UI replacement. @author ddj 2026年10月08号 */
412362
- const CLAIM_ID = "dsh-vscode-mode/plan";
412363
- const CLAIM_KIND = "edrvPlan";
412364
- const EMPTY_RESOURCE = { status: "none" };
412365
- /** @private @author ddj 2026年10月08号 @param ctx Optional context. @param name Service name. @returns Service or undefined without gating plugin activation. */
412366
- function optional(ctx, name) {
412379
+ //#region src/client/planDirect.ts
412380
+ /**
412381
+ * dsh-vscode-mode client — 计划地址直通。
412382
+ *
412383
+ * 背景:官方计划卡片「查看全文」调 `ctx.sidebarRight.openResource(planAddress)`,
412384
+ * 侧栏会先创建计划页签、再由本插件认领并立刻关闭,桌面端表现为「快速打开又迅速关闭」。
412385
+ * 本模块在装配时包装该服务方法:能直接拿到计划正文时改为把只读计划送进「文件编辑」,
412386
+ * 不再创建侧栏页签;地址不属于在屏会话、正文暂不可得、编辑器未回执或包装未生效时
412387
+ * 一律回落原方法,官方页签与原认领转交链路继续作为后备。
412388
+ * 作者 ddj 2026年10月31号
412389
+ */
412390
+ /** 正文解析上限:超过即回落官方页签,避免点击后长时间无反馈。 */
412391
+ const RESOLVE_MS = 1500;
412392
+ /** 编辑器回执上限:超过即回落官方页签。 */
412393
+ const ROUTE_MS = 5e3;
412394
+ /** @public @author ddj 2026年10月31号
412395
+ * 可选探测客户端服务,缺失或抛错时返回 undefined。
412396
+ * @param ctx 客户端上下文(只需 get)。
412397
+ * @param name 服务名。
412398
+ * @returns 服务实例,或 undefined(不阻断装配)。
412399
+ */
412400
+ function optionalService(ctx, name) {
412367
412401
  try {
412368
412402
  return ctx.get(name);
412369
412403
  } catch {
412370
412404
  return;
412371
412405
  }
412372
412406
  }
412407
+ /** @public @author ddj 2026年10月31号
412408
+ * 包装官方 openResource,把本会话的计划地址改为直通插件编辑器。
412409
+ * @param ctx 客户端上下文(可选服务一律 ctx.get 探测)。
412410
+ * @param service 官方 sidebarRight 服务;仅在赋值生效时留下包装。
412411
+ * @returns 供路由回退使用的原方法,以及卸载时中止在途并还原的清理函数。
412412
+ */
412413
+ function installPlanDirect(ctx, service) {
412414
+ const original = service.openResource;
412415
+ const pending = /* @__PURE__ */ new Map();
412416
+ let closed = false;
412417
+ let installed = false;
412418
+ /** @private @author ddj 2026年10月31号
412419
+ * 解码地址段(畸形编码一律视为不可接管)。
412420
+ * @param address 官方资源地址。
412421
+ * @returns 去掉协议前缀后的解码段。
412422
+ */
412423
+ function decode(address) {
412424
+ try {
412425
+ return address.split("/").slice(3).map(decodeURIComponent);
412426
+ } catch {
412427
+ return;
412428
+ }
412429
+ }
412430
+ /** @private @author ddj 2026年10月31号
412431
+ * 判断该地址是否由本插件接管:只认两段式计划地址,且归属当前在屏会话。
412432
+ * @param address 官方资源地址。
412433
+ * @returns 可接管的会话 id,其他情况 undefined(回落原方法)。
412434
+ */
412435
+ function take(address) {
412436
+ if (closed || !isPlanAddress(address)) return void 0;
412437
+ const segments = decode(address);
412438
+ if (segments?.length !== 2) return void 0;
412439
+ const current = readSessionScope(ctx).sessionId ?? "";
412440
+ return current !== "" && segments[0] === current ? current : void 0;
412441
+ }
412442
+ /** @private @author ddj 2026年10月31号
412443
+ * 读官方参数里随页签携带的计划正文(临时回顾页签走这里)。
412444
+ * @param address 官方资源地址。
412445
+ * @param options 官方 openResource 参数。
412446
+ * @returns 计划正文,或不带正文时 undefined。
412447
+ */
412448
+ function inline(address, options) {
412449
+ const params = options?.params;
412450
+ if (params === null || typeof params !== "object") return void 0;
412451
+ return planDocument(address, params.planReview);
412452
+ }
412453
+ /** @private @author ddj 2026年10月31号
412454
+ * 打开计划资源流。
412455
+ * @param services resources 服务。
412456
+ * @param address 官方资源地址。
412457
+ * @returns 资源流;服务缺失或抛错时 undefined。
412458
+ */
412459
+ function streamOf(services, address) {
412460
+ try {
412461
+ return services?.source?.(address);
412462
+ } catch {
412463
+ return;
412464
+ }
412465
+ }
412466
+ /** @private @author ddj 2026年10月31号
412467
+ * 从资源快照读已就绪的计划正文。
412468
+ * @param address 官方资源地址。
412469
+ * @param stream 计划资源流。
412470
+ * @returns 计划正文;未就绪、失败或内容不完整时 undefined。
412471
+ */
412472
+ function planOf(address, stream) {
412473
+ let snapshot;
412474
+ try {
412475
+ snapshot = stream.getSnapshot?.();
412476
+ } catch {
412477
+ return;
412478
+ }
412479
+ const state = snapshot !== null && typeof snapshot === "object" ? snapshot : {};
412480
+ return state.status === "live" ? planDocument(address, state.value) : void 0;
412481
+ }
412482
+ /** @private @author ddj 2026年10月31号
412483
+ * 订阅资源快照变化。
412484
+ * @param stream 计划资源流。
412485
+ * @param listener 变更监听。
412486
+ * @returns 退订函数;订阅面缺失或抛错时为 no-op。
412487
+ */
412488
+ function watchStream(stream, listener) {
412489
+ try {
412490
+ const off = stream.subscribe?.(listener);
412491
+ return typeof off === "function" ? off : () => void 0;
412492
+ } catch {
412493
+ return () => void 0;
412494
+ }
412495
+ }
412496
+ /** @private @author ddj 2026年10月31号
412497
+ * 回落官方方法(包装外的原入口)。
412498
+ * @param address 官方资源地址。
412499
+ * @param options 官方参数。
412500
+ * @returns 无;失败只记日志,不影响点击链路。
412501
+ */
412502
+ function native(address, options) {
412503
+ if (typeof original !== "function") return;
412504
+ try {
412505
+ original.call(service, address, options);
412506
+ } catch (error) {
412507
+ log$2.warn("官方计划页签回退失败:" + String(error));
412508
+ }
412509
+ }
412510
+ /** @private @author ddj 2026年10月31号
412511
+ * 把已解析的计划送进插件编辑器;地址级在途合并,重复点击不叠开。
412512
+ * @param plan 计划正文。
412513
+ * @param session 归属会话。
412514
+ * @param address 官方资源地址。
412515
+ * @param options 官方参数(回落时原样透传)。
412516
+ * @returns 无;未就绪或失败时回落官方页签,但不劫持已切换的会话。
412517
+ */
412518
+ function direct(plan, session, address, options) {
412519
+ if (closed || pending.has(address)) return;
412520
+ const controller = new AbortController();
412521
+ pending.set(address, () => {
412522
+ pending.delete(address);
412523
+ controller.abort();
412524
+ });
412525
+ rememberPlan(plan);
412526
+ /** @private @author ddj 2026年10月31号 @param ok 编辑器是否就绪。 @returns 无;失败时回落官方页签。 */
412527
+ function settle(ok) {
412528
+ pending.delete(address);
412529
+ if (ok || closed || readSessionScope(ctx).sessionId !== session) return;
412530
+ native(address, options);
412531
+ }
412532
+ requestOpen(null, void 0, void 0, session, {
412533
+ preview: true,
412534
+ plan,
412535
+ signal: controller.signal,
412536
+ timeoutMs: ROUTE_MS
412537
+ }).then(settle, (error) => {
412538
+ log$2.warn("计划直通失败:" + String(error));
412539
+ settle(false);
412540
+ });
412541
+ }
412542
+ /** @private @author ddj 2026年10月31号
412543
+ * 从计划资源异步解析正文,就绪后直通;超时或无法解析回落官方页签。
412544
+ * @param address 官方资源地址。
412545
+ * @param session 归属会话。
412546
+ * @param options 官方参数。
412547
+ * @returns 无。
412548
+ */
412549
+ function lookup(address, session, options) {
412550
+ const stream = streamOf(optionalService(ctx, "resources"), address);
412551
+ if (stream === void 0) {
412552
+ native(address, options);
412553
+ return;
412554
+ }
412555
+ const early = planOf(address, stream);
412556
+ if (early !== void 0) {
412557
+ direct(early, session, address, options);
412558
+ return;
412559
+ }
412560
+ if (closed || pending.has(address)) return;
412561
+ let timer;
412562
+ let stop;
412563
+ let done = false;
412564
+ /** @private @author ddj 2026年10月31号 @param ok 是否已拿到正文。 @returns 无;幂等收尾,清定时器、退订、释放地址。 */
412565
+ function finish(ok) {
412566
+ if (done) return;
412567
+ done = true;
412568
+ if (timer !== void 0) clearTimeout(timer);
412569
+ stop?.();
412570
+ pending.delete(address);
412571
+ if (!ok && !closed) native(address, options);
412572
+ }
412573
+ pending.set(address, () => finish(false));
412574
+ timer = setTimeout(() => finish(false), RESOLVE_MS);
412575
+ stop = watchStream(stream, () => {
412576
+ const plan = planOf(address, stream);
412577
+ if (plan === void 0) return;
412578
+ finish(true);
412579
+ direct(plan, session, address, options);
412580
+ });
412581
+ }
412582
+ /** @private @author ddj 2026年10月31号 @param address 官方资源地址。 @param options 官方参数。 @returns 无;包装后的 openResource 入口。 */
412583
+ function open(address, options) {
412584
+ const session = take(address);
412585
+ if (session === void 0) {
412586
+ native(address, options);
412587
+ return;
412588
+ }
412589
+ const known = inline(address, options) ?? cachedPlan(address);
412590
+ if (known !== void 0) {
412591
+ direct(known, session, address, options);
412592
+ return;
412593
+ }
412594
+ lookup(address, session, options);
412595
+ }
412596
+ /** @private @author ddj 2026年10月31号 @returns 无;赋值后校验,未生效即降级为原认领转交链路。 */
412597
+ function mount() {
412598
+ if (typeof original !== "function") return;
412599
+ try {
412600
+ service.openResource = open;
412601
+ } catch (error) {
412602
+ log$2.warn("计划直通装配失败:" + String(error));
412603
+ }
412604
+ installed = service.openResource === open;
412605
+ if (installed) return;
412606
+ try {
412607
+ service.openResource = original;
412608
+ } catch {}
412609
+ }
412610
+ /** @private @author ddj 2026年10月31号 @returns 无;仅在包装仍是自己时还原(HMR 安全)。 */
412611
+ function unmount() {
412612
+ if (!installed || service.openResource !== open) return;
412613
+ try {
412614
+ service.openResource = original;
412615
+ } catch (error) {
412616
+ log$2.warn("计划直通还原失败:" + String(error));
412617
+ }
412618
+ installed = false;
412619
+ }
412620
+ /** @private @author ddj 2026年10月31号 @returns 无;卸载时先标记关闭,再中止在途并还原官方方法。 */
412621
+ function dispose() {
412622
+ if (closed) return;
412623
+ closed = true;
412624
+ for (const cancel of [...pending.values()]) try {
412625
+ cancel();
412626
+ } catch {}
412627
+ pending.clear();
412628
+ unmount();
412629
+ }
412630
+ mount();
412631
+ return {
412632
+ dispose,
412633
+ native
412634
+ };
412635
+ }
412636
+ //#endregion
412637
+ //#region src/client/planBridge.ts
412638
+ /** Optional DSH plan integration; no provider or approval UI replacement. @author ddj 2026年10月08号 */
412639
+ const CLAIM_ID = "dsh-vscode-mode/plan";
412640
+ const CLAIM_KIND = "edrvPlan";
412641
+ const EMPTY_RESOURCE = { status: "none" };
412373
412642
  /** @private @author ddj 2026年10月08号 @param tab Current tab. @returns Native plan fallback options, preserving transient review parameters. */
412374
412643
  function nativeOptions(tab) {
412375
412644
  return {
@@ -412377,8 +412646,8 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
412377
412646
  params: tab?.navigation?.params
412378
412647
  };
412379
412648
  }
412380
- /** @private @author ddj 2026年10月08号 @param ctx Optional context. @param official Sidebar services. @returns Transfer component with native fallback and cancellable receipts. */
412381
- function planRouter(ctx, official) {
412649
+ /** @private @author ddj 2026年10月08号 @param ctx Optional context. @param official Sidebar services. @param native Official openResource bypass. @returns Transfer component with native fallback and cancellable receipts. */
412650
+ function planRouter(ctx, official, native) {
412382
412651
  const transfers = /* @__PURE__ */ new Map();
412383
412652
  /** @private @author ddj 2026年10月08号 @param all Plugin disposal. Cancel scope-stale transfers, not transfers merely hidden by the editor mount. */
412384
412653
  function cancel(all = false) {
@@ -412408,7 +412677,7 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
412408
412677
  function openNative() {
412409
412678
  if (typeof official.service.openResource !== "function") return;
412410
412679
  try {
412411
- official.service.openResource(address, nativeOptions(tab));
412680
+ native(address, nativeOptions(tab));
412412
412681
  tab?.actions?.close?.();
412413
412682
  } catch (error) {
412414
412683
  log$2.warn("计划官方查看器回退失败:" + String(error));
@@ -412475,18 +412744,21 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
412475
412744
  canOpen: isPlanAddress,
412476
412745
  title: () => "计划预览"
412477
412746
  });
412478
- const router = planRouter(ctx, official);
412747
+ const direct = installPlanDirect(ctx, official.service);
412748
+ const router = planRouter(ctx, official, direct.native);
412479
412749
  try {
412480
412750
  const offBody = ctx.slots.inject(OFFICIAL_SLOT_NAME, () => ctx.slots.register({
412481
412751
  name: OFFICIAL_SLOT_NAME,
412482
412752
  key: CLAIM_ID
412483
412753
  }, router.body));
412484
412754
  return () => {
412755
+ direct.dispose();
412485
412756
  router.dispose();
412486
412757
  if (typeof offBody === "function") offBody();
412487
412758
  offType();
412488
412759
  };
412489
412760
  } catch (error) {
412761
+ direct.dispose();
412490
412762
  router.dispose();
412491
412763
  offType();
412492
412764
  throw error;
@@ -412502,7 +412774,7 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
412502
412774
  /** @private @author ddj 2026年10月08号 @param id Owning session. @returns Valid borrowed feed, or undefined on older/incompatible services. */
412503
412775
  function sourceOf(id) {
412504
412776
  try {
412505
- const sessions = optional(ctx, "sessions");
412777
+ const sessions = optionalService(ctx, "sessions");
412506
412778
  if (!id || typeof sessions?.binding !== "function") return void 0;
412507
412779
  const feed = sessions.binding(id)?.eventSource;
412508
412780
  return typeof feed?.getSnapshot === "function" && typeof feed.subscribe === "function" ? feed : void 0;
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: 35677585 bytes
4
- sha256: df1afa8466a7852d100cfbc6a1f44e61beabd3b1808a262562e920ac06b96865
3
+ size: 35694036 bytes
4
+ sha256: 5a0053a6aa5733ebef51b077a624f0fc5bcf1a9c555232c357762a7eb8bdf2ef
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-vscode-mode",
3
- "version": "0.23.3",
3
+ "version": "0.23.4",
4
4
  "description": "DSH 上的类 VSCode 编码体验:Monaco 编辑器(文件页签/QuickOpen/状态栏,可驻 DSH 0.1.5+ 官方右侧 Sidebar 与对话同屏)+ 命令栏(Ctrl+Shift+P)与可扩展指令系统 + VS Code 兼容代码片段(.code-snippets 全局/项目,IntelliSense 展开)+ Agent 编辑差异审查(整文件差异/采纳/拒绝/归档/回滚)+ SVN 集成(侧栏变更面板/日志弹窗/并排差异)+ 语言服务器(LSP)智能(跳转定义/引用/hover/大纲 + VSIX 扩展市场安装),状态持久化到工作区旁车,界面状态(页签/光标/侧栏/编辑 Tab/导航历史)镜像到 DSH home 跨重启恢复",
5
5
  "icon": "assets/icon.svg",
6
6
  "keywords": [
@@ -84,7 +84,7 @@
84
84
  "vitest": "^3.2.4"
85
85
  },
86
86
  "scripts": {
87
- "build": "tsdown",
87
+ "build": "tsdown && node scripts/check-client-artifacts.mjs lib",
88
88
  "build:client": "tsdown",
89
89
  "typecheck": "tsc -p tsconfig.json --noEmit",
90
90
  "test": "vitest run && node --test tests/nativeOpen.test.mjs",
@@ -4,7 +4,7 @@ import { createRequestId } from './requestId.js'
4
4
  import type { PlanDocument } from './planState.js'
5
5
 
6
6
  export interface EditorRequest { requestId: string; sessionId: string; path: string | null; line?: number; column?: number; preview?: boolean; plan?: PlanDocument }
7
- export interface OpenOptions { preview?: boolean; plan?: PlanDocument; signal?: AbortSignal }
7
+ export interface OpenOptions { preview?: boolean; plan?: PlanDocument; signal?: AbortSignal; timeoutMs?: number }
8
8
  interface Waiter { request: EditorRequest; finish: (success: boolean) => void; claimed?: boolean }
9
9
  const waiters = new Map<string, Waiter>()
10
10
 
@@ -37,7 +37,7 @@ export function finishOpen(requestId: string, success: boolean, error?: string):
37
37
  * @param line Optional target line.
38
38
  * @param column Optional target column.
39
39
  * @param sessionId Owning session, required for routing.
40
- * @param options Explicit preview, read-only plan document and optional cancellation signal.
40
+ * @param options Explicit preview, read-only plan document, cancellation signal and receipt deadline.
41
41
  * @returns Actual completion; timeout and disposal are false.
42
42
  */
43
43
  export function requestOpen(path: string | null, line?: number, column?: number, sessionId = '', options: OpenOptions = {}): Promise<boolean> {
@@ -60,7 +60,7 @@ export function requestOpen(path: string | null, line?: number, column?: number,
60
60
  function abort(): void { finish(false) }
61
61
  options.signal?.addEventListener('abort', abort, { once: true })
62
62
  waiters.set(request.requestId, { request, finish })
63
- timer = setTimeout(() => finish(false), 20_000)
63
+ timer = setTimeout(() => finish(false), options.timeoutMs ?? 20_000)
64
64
  openEditorView(null)
65
65
  window.dispatchEvent(new CustomEvent('edrv:open-request', { detail: request }))
66
66
  })
@@ -2,6 +2,7 @@
2
2
  import React from 'react'
3
3
  import { requestOpen } from './openReceipt.js'
4
4
  import { createPlanFeed, clearPlanState, isPlanAddress, planDocument, type PlanWindow, type PlanOpen } from './planState.js'
5
+ import { installPlanDirect, optionalService, type PlanOpenOptions } from './planDirect.js'
5
6
  import { detectOfficial, OFFICIAL_SLOT_NAME, type OfficialSidebar } from './officialSidebar.js'
6
7
  import { readSessionScope, subscribeScope } from './sessionScope.js'
7
8
  import { log } from './log.js'
@@ -22,16 +23,12 @@ const CLAIM_ID = 'dsh-vscode-mode/plan'
22
23
  const CLAIM_KIND = 'edrvPlan'
23
24
  const EMPTY_RESOURCE = { status: 'none' }
24
25
 
25
- /** @private @author ddj 2026年10月08号 @param ctx Optional context. @param name Service name. @returns Service or undefined without gating plugin activation. */
26
- function optional(ctx: PlanContext, name: string): unknown {
27
- try { return ctx.get(name) } catch { return undefined }
28
- }
29
26
  /** @private @author ddj 2026年10月08号 @param tab Current tab. @returns Native plan fallback options, preserving transient review parameters. */
30
27
  function nativeOptions(tab: PlanTab | undefined): { kind: string; params?: unknown } {
31
28
  return { kind: 'plan', params: tab?.navigation?.params }
32
29
  }
33
- /** @private @author ddj 2026年10月08号 @param ctx Optional context. @param official Sidebar services. @returns Transfer component with native fallback and cancellable receipts. */
34
- function planRouter(ctx: PlanContext, official: OfficialSidebar) {
30
+ /** @private @author ddj 2026年10月08号 @param ctx Optional context. @param official Sidebar services. @param native Official openResource bypass. @returns Transfer component with native fallback and cancellable receipts. */
31
+ function planRouter(ctx: PlanContext, official: OfficialSidebar, native: (address: string, options?: PlanOpenOptions) => void) {
35
32
  const transfers = new Map<string, { controller: AbortController; sessionId: string }>()
36
33
  /** @private @author ddj 2026年10月08号 @param all Plugin disposal. Cancel scope-stale transfers, not transfers merely hidden by the editor mount. */
37
34
  function cancel(all = false): void {
@@ -60,7 +57,7 @@ function planRouter(ctx: PlanContext, official: OfficialSidebar) {
60
57
  function openNative(): void {
61
58
  if (typeof official.service.openResource !== 'function') return
62
59
  try {
63
- official.service.openResource(address, nativeOptions(tab))
60
+ native(address, nativeOptions(tab))
64
61
  tab?.actions?.close?.()
65
62
  } catch (error) { log.warn('计划官方查看器回退失败:' + String(error)) }
66
63
  }
@@ -99,11 +96,12 @@ function registerPlan(ctx: PlanContext, official: OfficialSidebar): () => void {
99
96
  patterns: ['dsh-resource://plan/**', 'dsh-resource://plan-review/**'],
100
97
  canOpen: isPlanAddress, title: () => '计划预览',
101
98
  })
102
- const router = planRouter(ctx, official)
99
+ const direct = installPlanDirect(ctx, official.service)
100
+ const router = planRouter(ctx, official, direct.native)
103
101
  try {
104
102
  const offBody = ctx.slots.inject(OFFICIAL_SLOT_NAME, () => ctx.slots.register({ name: OFFICIAL_SLOT_NAME, key: CLAIM_ID }, router.body))
105
- return () => { router.dispose(); if (typeof offBody === 'function') offBody(); offType() }
106
- } catch (error) { router.dispose(); offType(); throw error }
103
+ return () => { direct.dispose(); router.dispose(); if (typeof offBody === 'function') offBody(); offType() }
104
+ } catch (error) { direct.dispose(); router.dispose(); offType(); throw error }
107
105
  }
108
106
  /** @private @author ddj 2026年10月08号 @param ctx Context. @returns Current-session feed subscription, cancelling pending opens on scope change. */
109
107
  function watchPlans(ctx: PlanContext): () => void {
@@ -116,7 +114,7 @@ function watchPlans(ctx: PlanContext): () => void {
116
114
  /** @private @author ddj 2026年10月08号 @param id Owning session. @returns Valid borrowed feed, or undefined on older/incompatible services. */
117
115
  function sourceOf(id: string): EventSource | undefined {
118
116
  try {
119
- const sessions = optional(ctx, 'sessions') as Sessions | undefined
117
+ const sessions = optionalService(ctx, 'sessions') as Sessions | undefined
120
118
  if (!id || typeof sessions?.binding !== 'function') return undefined
121
119
  const feed = sessions.binding(id)?.eventSource
122
120
  return typeof feed?.getSnapshot === 'function' && typeof feed.subscribe === 'function' ? feed : undefined
@@ -0,0 +1,266 @@
1
+ /**
2
+ * dsh-vscode-mode client — 计划地址直通。
3
+ *
4
+ * 背景:官方计划卡片「查看全文」调 `ctx.sidebarRight.openResource(planAddress)`,
5
+ * 侧栏会先创建计划页签、再由本插件认领并立刻关闭,桌面端表现为「快速打开又迅速关闭」。
6
+ * 本模块在装配时包装该服务方法:能直接拿到计划正文时改为把只读计划送进「文件编辑」,
7
+ * 不再创建侧栏页签;地址不属于在屏会话、正文暂不可得、编辑器未回执或包装未生效时
8
+ * 一律回落原方法,官方页签与原认领转交链路继续作为后备。
9
+ * 作者 ddj 2026年10月31号
10
+ */
11
+
12
+ import { log } from './log.js'
13
+ import { requestOpen } from './openReceipt.js'
14
+ import { cachedPlan, isPlanAddress, planDocument, rememberPlan, type PlanDocument } from './planState.js'
15
+ import { readSessionScope } from './sessionScope.js'
16
+
17
+ //region 对外结构面
18
+
19
+ /** 官方 openResource 的可选参数(临时回顾页签的正文在 params.planReview 里)。 */
20
+ export interface PlanOpenOptions { kind?: string; params?: unknown }
21
+
22
+ /** sidebarRight 的最小结构面:只接管 openResource 的调用。 */
23
+ export interface PlanServiceLike { openResource?: (address: string, options?: PlanOpenOptions) => void }
24
+
25
+ /** 直通装配结果:native 绕过包装回落原方法,dispose 中止在途并条件还原。 */
26
+ export interface PlanDirect {
27
+ dispose: () => void
28
+ native: (address: string, options?: PlanOpenOptions) => void
29
+ }
30
+
31
+ /** 资源流最小结构面(@deepseek-ai/dsh-client-resources 为可选依赖)。 */
32
+ interface ResourceStream { getSnapshot?: () => unknown; subscribe?: (listener: () => void) => unknown }
33
+
34
+ /** resources 服务最小结构面。 */
35
+ interface ResourcesLike { source?: (address: string) => ResourceStream | undefined }
36
+
37
+ //endregion
38
+
39
+ /** 正文解析上限:超过即回落官方页签,避免点击后长时间无反馈。 */
40
+ const RESOLVE_MS = 1_500
41
+ /** 编辑器回执上限:超过即回落官方页签。 */
42
+ const ROUTE_MS = 5_000
43
+
44
+ /** @public @author ddj 2026年10月31号
45
+ * 可选探测客户端服务,缺失或抛错时返回 undefined。
46
+ * @param ctx 客户端上下文(只需 get)。
47
+ * @param name 服务名。
48
+ * @returns 服务实例,或 undefined(不阻断装配)。
49
+ */
50
+ export function optionalService(ctx: { get: (name: string) => unknown }, name: string): unknown {
51
+ try { return ctx.get(name) } catch { return undefined }
52
+ }
53
+
54
+ /** @public @author ddj 2026年10月31号
55
+ * 包装官方 openResource,把本会话的计划地址改为直通插件编辑器。
56
+ * @param ctx 客户端上下文(可选服务一律 ctx.get 探测)。
57
+ * @param service 官方 sidebarRight 服务;仅在赋值生效时留下包装。
58
+ * @returns 供路由回退使用的原方法,以及卸载时中止在途并还原的清理函数。
59
+ */
60
+ export function installPlanDirect(ctx: { get: (name: string) => unknown }, service: PlanServiceLike): PlanDirect {
61
+ const original = service.openResource
62
+ const pending = new Map<string, () => void>()
63
+ let closed = false
64
+ let installed = false
65
+
66
+ //region 判定与参数
67
+
68
+ /** @private @author ddj 2026年10月31号
69
+ * 解码地址段(畸形编码一律视为不可接管)。
70
+ * @param address 官方资源地址。
71
+ * @returns 去掉协议前缀后的解码段。
72
+ */
73
+ function decode(address: string): string[] | undefined {
74
+ try { return address.split('/').slice(3).map(decodeURIComponent) } catch { return undefined }
75
+ }
76
+
77
+ /** @private @author ddj 2026年10月31号
78
+ * 判断该地址是否由本插件接管:只认两段式计划地址,且归属当前在屏会话。
79
+ * @param address 官方资源地址。
80
+ * @returns 可接管的会话 id,其他情况 undefined(回落原方法)。
81
+ */
82
+ function take(address: string): string | undefined {
83
+ if (closed || !isPlanAddress(address)) return undefined
84
+
85
+ const segments = decode(address)
86
+ if (segments?.length !== 2) return undefined
87
+
88
+ const current = readSessionScope(ctx).sessionId ?? ''
89
+ return current !== '' && segments[0] === current ? current : undefined
90
+ }
91
+
92
+ /** @private @author ddj 2026年10月31号
93
+ * 读官方参数里随页签携带的计划正文(临时回顾页签走这里)。
94
+ * @param address 官方资源地址。
95
+ * @param options 官方 openResource 参数。
96
+ * @returns 计划正文,或不带正文时 undefined。
97
+ */
98
+ function inline(address: string, options?: PlanOpenOptions): PlanDocument | undefined {
99
+ const params = options?.params
100
+ if (params === null || typeof params !== 'object') return undefined
101
+ return planDocument(address, (params as { planReview?: unknown }).planReview)
102
+ }
103
+
104
+ //endregion
105
+
106
+ //region 资源解析
107
+
108
+ /** @private @author ddj 2026年10月31号
109
+ * 打开计划资源流。
110
+ * @param services resources 服务。
111
+ * @param address 官方资源地址。
112
+ * @returns 资源流;服务缺失或抛错时 undefined。
113
+ */
114
+ function streamOf(services: ResourcesLike | undefined, address: string): ResourceStream | undefined {
115
+ try { return services?.source?.(address) } catch { return undefined }
116
+ }
117
+
118
+ /** @private @author ddj 2026年10月31号
119
+ * 从资源快照读已就绪的计划正文。
120
+ * @param address 官方资源地址。
121
+ * @param stream 计划资源流。
122
+ * @returns 计划正文;未就绪、失败或内容不完整时 undefined。
123
+ */
124
+ function planOf(address: string, stream: ResourceStream): PlanDocument | undefined {
125
+ let snapshot: unknown
126
+ try { snapshot = stream.getSnapshot?.() } catch { return undefined }
127
+
128
+ const state = snapshot !== null && typeof snapshot === 'object' ? snapshot as { status?: unknown; value?: unknown } : {}
129
+ return state.status === 'live' ? planDocument(address, state.value) : undefined
130
+ }
131
+
132
+ /** @private @author ddj 2026年10月31号
133
+ * 订阅资源快照变化。
134
+ * @param stream 计划资源流。
135
+ * @param listener 变更监听。
136
+ * @returns 退订函数;订阅面缺失或抛错时为 no-op。
137
+ */
138
+ function watchStream(stream: ResourceStream, listener: () => void): () => void {
139
+ try {
140
+ const off = stream.subscribe?.(listener)
141
+ return typeof off === 'function' ? off as () => void : () => undefined
142
+ } catch { return () => undefined }
143
+ }
144
+
145
+ //endregion
146
+
147
+ //region 直通与回落
148
+
149
+ /** @private @author ddj 2026年10月31号
150
+ * 回落官方方法(包装外的原入口)。
151
+ * @param address 官方资源地址。
152
+ * @param options 官方参数。
153
+ * @returns 无;失败只记日志,不影响点击链路。
154
+ */
155
+ function native(address: string, options?: PlanOpenOptions): void {
156
+ if (typeof original !== 'function') return
157
+ try { original.call(service, address, options) } catch (error) { log.warn('官方计划页签回退失败:' + String(error)) }
158
+ }
159
+
160
+ /** @private @author ddj 2026年10月31号
161
+ * 把已解析的计划送进插件编辑器;地址级在途合并,重复点击不叠开。
162
+ * @param plan 计划正文。
163
+ * @param session 归属会话。
164
+ * @param address 官方资源地址。
165
+ * @param options 官方参数(回落时原样透传)。
166
+ * @returns 无;未就绪或失败时回落官方页签,但不劫持已切换的会话。
167
+ */
168
+ function direct(plan: PlanDocument, session: string, address: string, options?: PlanOpenOptions): void {
169
+ if (closed || pending.has(address)) return
170
+
171
+ const controller = new AbortController()
172
+ pending.set(address, () => { pending.delete(address); controller.abort() })
173
+ rememberPlan(plan)
174
+
175
+ /** @private @author ddj 2026年10月31号 @param ok 编辑器是否就绪。 @returns 无;失败时回落官方页签。 */
176
+ function settle(ok: boolean): void {
177
+ pending.delete(address)
178
+ if (ok || closed || readSessionScope(ctx).sessionId !== session) return
179
+ native(address, options)
180
+ }
181
+ requestOpen(null, undefined, undefined, session, { preview: true, plan, signal: controller.signal, timeoutMs: ROUTE_MS })
182
+ .then(settle, (error: unknown) => { log.warn('计划直通失败:' + String(error)); settle(false) })
183
+ }
184
+
185
+ /** @private @author ddj 2026年10月31号
186
+ * 从计划资源异步解析正文,就绪后直通;超时或无法解析回落官方页签。
187
+ * @param address 官方资源地址。
188
+ * @param session 归属会话。
189
+ * @param options 官方参数。
190
+ * @returns 无。
191
+ */
192
+ function lookup(address: string, session: string, options?: PlanOpenOptions): void {
193
+ const stream = streamOf(optionalService(ctx, 'resources') as ResourcesLike | undefined, address)
194
+ if (stream === undefined) { native(address, options); return }
195
+
196
+ const early = planOf(address, stream)
197
+ if (early !== undefined) { direct(early, session, address, options); return }
198
+ if (closed || pending.has(address)) return
199
+
200
+ let timer: ReturnType<typeof setTimeout> | undefined
201
+ let stop: (() => void) | undefined
202
+ let done = false
203
+
204
+ /** @private @author ddj 2026年10月31号 @param ok 是否已拿到正文。 @returns 无;幂等收尾,清定时器、退订、释放地址。 */
205
+ function finish(ok: boolean): void {
206
+ if (done) return
207
+ done = true
208
+ if (timer !== undefined) clearTimeout(timer)
209
+ stop?.()
210
+ pending.delete(address)
211
+ if (!ok && !closed) native(address, options)
212
+ }
213
+ pending.set(address, () => finish(false))
214
+ timer = setTimeout(() => finish(false), RESOLVE_MS)
215
+ stop = watchStream(stream, () => {
216
+ const plan = planOf(address, stream)
217
+ if (plan === undefined) return
218
+ finish(true)
219
+ direct(plan, session, address, options)
220
+ })
221
+ }
222
+
223
+ //endregion
224
+
225
+ //region 装配
226
+
227
+ /** @private @author ddj 2026年10月31号 @param address 官方资源地址。 @param options 官方参数。 @returns 无;包装后的 openResource 入口。 */
228
+ function open(address: string, options?: PlanOpenOptions): void {
229
+ const session = take(address)
230
+ if (session === undefined) { native(address, options); return }
231
+
232
+ const known = inline(address, options) ?? cachedPlan(address)
233
+ if (known !== undefined) { direct(known, session, address, options); return }
234
+ lookup(address, session, options)
235
+ }
236
+
237
+ /** @private @author ddj 2026年10月31号 @returns 无;赋值后校验,未生效即降级为原认领转交链路。 */
238
+ function mount(): void {
239
+ if (typeof original !== 'function') return
240
+ try { service.openResource = open } catch (error) { log.warn('计划直通装配失败:' + String(error)) }
241
+ installed = service.openResource === open
242
+ if (installed) return
243
+ try { service.openResource = original } catch { /* 只读服务:保持原状 */ }
244
+ }
245
+
246
+ /** @private @author ddj 2026年10月31号 @returns 无;仅在包装仍是自己时还原(HMR 安全)。 */
247
+ function unmount(): void {
248
+ if (!installed || service.openResource !== open) return
249
+ try { service.openResource = original } catch (error) { log.warn('计划直通还原失败:' + String(error)) }
250
+ installed = false
251
+ }
252
+
253
+ /** @private @author ddj 2026年10月31号 @returns 无;卸载时先标记关闭,再中止在途并还原官方方法。 */
254
+ function dispose(): void {
255
+ if (closed) return
256
+ closed = true
257
+ for (const cancel of [...pending.values()]) { try { cancel() } catch { /* 收尾失败不影响卸载 */ } }
258
+ pending.clear()
259
+ unmount()
260
+ }
261
+
262
+ mount()
263
+ return { dispose, native }
264
+
265
+ //endregion
266
+ }
@@ -4,6 +4,8 @@ export interface PlanOpen { path?: string; plan?: PlanDocument }
4
4
  export interface PlanEntry { type?: string; event: { type: string; seq?: number; data: unknown } }
5
5
  export interface PlanWindow { revision: number; entries: readonly PlanEntry[]; change: { kind: string; entries?: readonly PlanEntry[] } }
6
6
  const associated = new Map<string, Set<string>>()
7
+ /** 地址 → 已解析计划正文(只留内存,供后续点击省掉一次资源解析)。 */
8
+ const documents = new Map<string, PlanDocument>()
7
9
 
8
10
  /** @private @author ddj 2026年10月08号 @param value JSON candidate. @returns Object or empty record. */
9
11
  function record(value: unknown): Record<string, unknown> {
@@ -59,8 +61,24 @@ function associate(sessionId: string, path: string, cwd: string): void {
59
61
  associated.set(sessionId, paths)
60
62
  if (associated.size > 32) associated.delete(associated.keys().next().value!)
61
63
  }
64
+ /** @public @author ddj 2026年10月31号
65
+ * 记住一份已解析计划,供「查看全文」时免去资源解析。
66
+ * @param plan 已解析计划。
67
+ * @returns 无;只缓存合法地址与正文,超过上限按插入序淘汰。
68
+ */
69
+ export function rememberPlan(plan: PlanDocument): void {
70
+ const valid = planDocument(plan.address, plan)
71
+ if (valid === undefined) return
72
+
73
+ documents.set(valid.address, valid)
74
+ if (documents.size > 64) documents.delete(documents.keys().next().value!)
75
+ }
76
+
77
+ /** @public @author ddj 2026年10月31号 @param address 官方资源地址。 @returns 已缓存的计划正文。 */
78
+ export function cachedPlan(address: string): PlanDocument | undefined { return documents.get(address) }
79
+
62
80
  /** @public @author ddj 2026年10月08号 Clear associations when the plugin unloads. */
63
- export function clearPlanState(): void { associated.clear() }
81
+ export function clearPlanState(): void { associated.clear(); documents.clear() }
64
82
 
65
83
  /** @public @author ddj 2026年10月08号
66
84
  * Reduce only live append deltas; hydration, pagination, reconnect and duplicate sequences never open plans.
@@ -91,7 +109,7 @@ export function createPlanFeed(sessionId: string, cwd = '') {
91
109
  if (event.type === 'tool/result') return undefined
92
110
  const address = 'dsh-resource://plan/' + encodeURIComponent(sessionId) + '/' + encodeURIComponent(id)
93
111
  const plan = planDocument(address, { markdown: call.args.plan })
94
- if (plan) { remember(id); return { plan } }
112
+ if (plan) { rememberPlan(plan); remember(id); return { plan } }
95
113
  return undefined
96
114
  }
97
115
  if (!settled || !['write', 'edit'].includes(call.name) || typeof call.args.file_path !== 'string') return undefined