dsh-vscode-mode 0.13.1 → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -2349,6 +2349,29 @@ function pluginLogRoot(home = dshHome()) {
2349
2349
  function treeCacheFile(cwd, home = dshHome(), schema = 1) {
2350
2350
  return join(workspaceCacheDir(hashOf(cwd), home), "tree.v" + schema + ".json");
2351
2351
  }
2352
+ /**
2353
+ * 每工作区 UI 状态镜像文件(工作区级:workspace/<id>/ui.v<schema>.json)。
2354
+ * 存编辑器页签/侧栏/预览态等界面状态的 localStorage 原值副本,供换 origin 后回填。
2355
+ * @author ddj 2026年09月28号
2356
+ * @param cwd 工作区绝对路径
2357
+ * @param home DSH home(测试可注入)
2358
+ * @param schema schema 版本(缺省 UI_STATE_SCHEMA)
2359
+ * @returns UI 状态镜像文件绝对路径
2360
+ */
2361
+ function uiStateFile(cwd, home = dshHome(), schema = 1) {
2362
+ return join(workspaceCacheDir(hashOf(cwd), home), "ui.v" + schema + ".json");
2363
+ }
2364
+ /**
2365
+ * 用户级 UI 状态镜像文件(跨工作区共享:user/ui-globals.v<schema>.json)。
2366
+ * 存与工作区无关的全局键(侧栏提示已忽略、工作区折叠态等)。
2367
+ * @author ddj 2026年09月28号
2368
+ * @param home DSH home(测试可注入)
2369
+ * @param schema schema 版本(缺省 UI_STATE_SCHEMA)
2370
+ * @returns 全局 UI 状态镜像文件绝对路径
2371
+ */
2372
+ function uiGlobalsFile(home = dshHome(), schema = 1) {
2373
+ return join(userCacheDir(home), "ui-globals.v" + schema + ".json");
2374
+ }
2352
2375
  /** 每工作区 debug 日志文件(用户级日志根下,按 cwd hash 隔离)。 */
2353
2376
  function debugLogFile(cwd, home = dshHome()) {
2354
2377
  return join(pluginLogRoot(home), "debug." + hashOf(cwd) + ".log");
@@ -2377,10 +2400,36 @@ function imageDirOf(config, moduleUrl) {
2377
2400
  if (cfg && typeof cfg.imageDir === "string" && cfg.imageDir) return cfg.imageDir.replace(/\\/g, "/");
2378
2401
  return assetsDirOf(moduleUrl);
2379
2402
  }
2380
- /** 树缓存文件名 → schema 版本(不匹配返回 null)。 */
2381
- function treeSchemaOf(filename) {
2382
- const m = /^tree\.v(\d+)\./.exec(filename);
2383
- return m ? Number(m[1]) : null;
2403
+ /**
2404
+ * 已知缓存文件种类 → 当前 schema 版本(sweep 判旧依据)。
2405
+ * 新增缓存文件必须登记在此,否则被当作未知残留清理。
2406
+ */
2407
+ const CACHE_SCHEMAS = {
2408
+ tree: 1,
2409
+ "lsp-specs": 4,
2410
+ ui: 1,
2411
+ "ui-globals": 1
2412
+ };
2413
+ /** 缓存文件名 → 种类与 schema 版本(`<kind>.v<schema>.json`;未登记种类/命名不合规 → null)。 */
2414
+ function parseCacheName(filename) {
2415
+ const m = /^([a-z-]+)\.v(\d+)\.json$/.exec(filename);
2416
+ if (!m) return null;
2417
+ const kind = m[1];
2418
+ if (CACHE_SCHEMAS[kind] === void 0) return null;
2419
+ return {
2420
+ kind,
2421
+ schema: Number(m[2])
2422
+ };
2423
+ }
2424
+ /**
2425
+ * 该缓存文件名是否为已登记种类的当前 schema 版本(sweep 判旧/判残留依据)。
2426
+ * @author ddj 2026年09月28号
2427
+ * @param filename 缓存文件名(不含目录)
2428
+ * @returns 已登记种类且版本为当前版本
2429
+ */
2430
+ function isCurrentCacheFile(filename) {
2431
+ const parsed = parseCacheName(filename);
2432
+ return parsed !== null && parsed.schema === CACHE_SCHEMAS[parsed.kind];
2384
2433
  }
2385
2434
  /** 按「schema 版本 + 保留期」清扫某目录内文件(目录级,返回相对路径清单)。 */
2386
2435
  async function sweepDirFiles(dir, removed, now, dropAll) {
@@ -2389,9 +2438,7 @@ async function sweepDirFiles(dir, removed, now, dropAll) {
2389
2438
  const full = join(dir, name);
2390
2439
  let drop = dropAll;
2391
2440
  if (!drop) {
2392
- const schema = treeSchemaOf(name);
2393
- if (schema === null) drop = true;
2394
- else if (schema !== 1) drop = true;
2441
+ if (!isCurrentCacheFile(name)) drop = true;
2395
2442
  else try {
2396
2443
  if (now - (await stat(full)).mtimeMs > 2592e6) drop = true;
2397
2444
  } catch (error) {
@@ -2886,14 +2933,19 @@ function inDshRange(version, range) {
2886
2933
  }
2887
2934
  /**
2888
2935
  * 版本线标签:报告与文档展示版本归属;不可解析返回 '未知'。
2889
- * 逐线自新到旧匹配,先命中先返回(区间无上界,故顺序即优先级)。
2890
- * @author ddj 2026年09月02号 / 2026年09月18号 / 2026年09月22号
2936
+ * 逐线自新到旧匹配,先命中先返回;仅 0.2.0-rc.1 精确标记为已审计版本。
2937
+ * @author ddj 2026年09月02号 / 2026年09月18号 / 2026年09月22号 / 2026年09月29号
2891
2938
  * @param input 版本串(如 '0.1.6-alpha.2')
2892
2939
  * @returns 版本线标签
2893
2940
  */
2894
2941
  function familyLabel(input) {
2895
2942
  const version = parseDshVersion(input);
2896
2943
  if (!version) return "未知";
2944
+ if (inDshRange(version, {
2945
+ from: "0.2.0-rc.1",
2946
+ to: "0.2.0-rc.1"
2947
+ })) return "0.2.0-rc.1(设置=profile Config;按能力探测)";
2948
+ if (inDshRange(version, { from: "0.2.0-0" })) return "0.2.0 及更新(除 rc.1 外未经逐版适配;按能力探测)";
2897
2949
  if (inDshRange(version, { from: "0.1.7-alpha.1" })) return "0.1.7-alpha.1 及更新(设置=profile Config + configForms 客户端面 + 会话 V4)";
2898
2950
  if (inDshRange(version, { from: "0.1.6-alpha.2" })) return "0.1.6-alpha.2 及更新(会话多实例共存 + 回合改动卡片 + Office 侧栏预览 + 侧栏浏览器)";
2899
2951
  if (inDshRange(version, { from: "0.1.6-alpha.1" })) return "0.1.6-alpha 及更新(MCP SDK v2 + Web 侧边栏终端 + 文件链接默认侧栏预览)";
@@ -9214,6 +9266,125 @@ async function revealInExplorer(absPath, isDir) {
9214
9266
  };
9215
9267
  }
9216
9268
  }
9269
+ /**
9270
+ * 合并增量补丁到键值表(不改入参)。
9271
+ * @author ddj 2026年09月28号
9272
+ * @param base 已有键值表
9273
+ * @param patch 增量补丁(null/undefined 值删除该键;空键跳过)
9274
+ * @returns 合并后的新键值表
9275
+ */
9276
+ function mergeUiStateKeys(base, patch) {
9277
+ const merged = { ...base };
9278
+ for (const [key, value] of Object.entries(patch)) {
9279
+ if (!key) continue;
9280
+ if (value === null || value === void 0) delete merged[key];
9281
+ else merged[key] = String(value);
9282
+ }
9283
+ return merged;
9284
+ }
9285
+ /**
9286
+ * 解析镜像文件文本;损坏/版本不符 → null(调用方视为空)。
9287
+ * @author ddj 2026年09月28号
9288
+ * @param text 文件文本(可为空)
9289
+ * @returns 解析后的文档;不可用时 null
9290
+ */
9291
+ function parseUiState(text) {
9292
+ if (!text) return null;
9293
+ let raw;
9294
+ try {
9295
+ raw = JSON.parse(text);
9296
+ } catch (error) {
9297
+ return null;
9298
+ }
9299
+ if (!raw || typeof raw !== "object") return null;
9300
+ const doc = raw;
9301
+ if (doc.version !== 1) return null;
9302
+ if (!doc.keys || typeof doc.keys !== "object") return null;
9303
+ const keys = {};
9304
+ for (const [key, value] of Object.entries(doc.keys)) {
9305
+ if (!key || typeof value !== "string") continue;
9306
+ keys[key] = value;
9307
+ }
9308
+ return {
9309
+ version: 1,
9310
+ updatedAt: typeof doc.updatedAt === "string" ? doc.updatedAt : "",
9311
+ keys
9312
+ };
9313
+ }
9314
+ /**
9315
+ * 序列化为镜像文件文本。
9316
+ * @author ddj 2026年09月28号
9317
+ * @param keys 键值表
9318
+ * @param updatedAt 更新时间(缺省当前时间)
9319
+ * @returns 文件文本
9320
+ */
9321
+ function serializeUiState(keys, updatedAt = (/* @__PURE__ */ new Date()).toISOString()) {
9322
+ return JSON.stringify({
9323
+ version: 1,
9324
+ updatedAt,
9325
+ keys
9326
+ });
9327
+ }
9328
+ /** 同文件写队列尾(串行化并发写入;队列清空即移除条目)。 */
9329
+ const writeChains = /* @__PURE__ */ new Map();
9330
+ /**
9331
+ * 把写任务挂到该文件的队尾串行执行(前序失败不影响后续)。
9332
+ * @author ddj 2026年09月28号
9333
+ * @param file 目标文件绝对路径
9334
+ * @param run 实际写操作
9335
+ * @returns 本次写操作结果
9336
+ */
9337
+ function chainWrite(file, run) {
9338
+ const next = (writeChains.get(file) ?? Promise.resolve()).then(run, run);
9339
+ const tail = next.then(() => void 0, () => void 0);
9340
+ writeChains.set(file, tail);
9341
+ tail.then(() => {
9342
+ if (writeChains.get(file) === tail) writeChains.delete(file);
9343
+ });
9344
+ return next;
9345
+ }
9346
+ /**
9347
+ * 读取镜像文件的键值表(文件缺失/损坏/不可读 → 空表,静默)。
9348
+ * @author ddj 2026年09月28号
9349
+ * @param file 镜像文件绝对路径
9350
+ * @returns 键值表(localStorage 键 → 字符串原值)
9351
+ */
9352
+ async function readUiState(file) {
9353
+ try {
9354
+ const doc = parseUiState(await readFile(file, "utf8"));
9355
+ return doc ? doc.keys : {};
9356
+ } catch (error) {
9357
+ return {};
9358
+ }
9359
+ }
9360
+ /**
9361
+ * 合并写入镜像文件(读旧 → 合并补丁 → 容量校验 → 原子替换),同文件写操作串行。
9362
+ * @author ddj 2026年09月28号
9363
+ * @param file 镜像文件绝对路径
9364
+ * @param patch 增量补丁(null/undefined 值删除该键)
9365
+ * @returns ok;超限或写入失败时 ok:false + 中文错误文案
9366
+ */
9367
+ async function writeUiState(file, patch) {
9368
+ return chainWrite(file, async () => {
9369
+ try {
9370
+ const json = serializeUiState(mergeUiStateKeys(await readUiState(file), patch));
9371
+ if (Buffer$1.byteLength(json, "utf8") > 524288) return {
9372
+ ok: false,
9373
+ error: "界面状态镜像超出容量上限(524288 字节)"
9374
+ };
9375
+ await mkdir(dirname(file), { recursive: true });
9376
+ const tmp = file + ".tmp";
9377
+ await writeFile(tmp, json, "utf8");
9378
+ await rename(tmp, file);
9379
+ return { ok: true };
9380
+ } catch (error) {
9381
+ return {
9382
+ ok: false,
9383
+ error: "界面状态写入失败: " + (error instanceof Error ? error.message : String(error))
9384
+ };
9385
+ }
9386
+ });
9387
+ }
9217
9388
  //#endregion
9218
9389
  //#region src/debugLog.ts
9219
9390
  /**
@@ -9931,6 +10102,12 @@ function settingsDocPath(configuredPath, requestedPath, platform = process.platf
9931
10102
  const staleCheckedAt = /* @__PURE__ */ new Map();
9932
10103
  /** stale 自动清理最小间隔。 */
9933
10104
  const STALE_CHECK_MIN_MS = 1e4;
10105
+ /**
10106
+ * 无会话期诊断的伪 cwd:冷启动早期本插件客户端就会上报界面状态镜像事件
10107
+ * (此时 `sessions` 可能尚未就绪,`requireSession` 会失败),退回本固定值落盘
10108
+ * `logs/debug.<hashOf(该值)>.log`,保证「换 origin 后没恢复」这类问题的诊断不丢。
10109
+ */
10110
+ const UI_AUDIT_CWD = "ui-state";
9934
10111
  /** 记录 → 客户端视图(不含 before 全文,仅长度);远程工作区附远端展示路径(issue #7)。 */
9935
10112
  function recView(record, ws = null) {
9936
10113
  return {
@@ -10296,6 +10473,49 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
10296
10473
  records: out
10297
10474
  };
10298
10475
  },
10476
+ /**
10477
+ * 界面状态镜像读取:回填换 origin(Web GUI 重启换端口)后丢失的 localStorage 界面状态。
10478
+ * 文件缺失/损坏 → 空表(客户端自然不动作)。
10479
+ * @author ddj 2026年09月28号
10480
+ */
10481
+ "edrv.uiState.get": async (args) => {
10482
+ const sc = await requireSession(ctx, args.sessionId);
10483
+ if ("err" in sc) return {
10484
+ ok: false,
10485
+ error: sc.err
10486
+ };
10487
+ return {
10488
+ ok: true,
10489
+ keys: await readUiState(uiStateFile(sc.cwd)),
10490
+ globals: await readUiState(uiGlobalsFile())
10491
+ };
10492
+ },
10493
+ /**
10494
+ * 界面状态镜像写入:把客户端改动过的 `edrv.*` 键增量同步到 host 文件(空补丁不落盘)。
10495
+ * 失败时写一条诊断日志(默认静默的客户端 dbg 之外的可查证据),并如实回错误由客户端逐键回退。
10496
+ * @author ddj 2026年09月28号 / 2026年09月29号
10497
+ */
10498
+ "edrv.uiState.set": async (args) => {
10499
+ const sc = await requireSession(ctx, args.sessionId);
10500
+ if ("err" in sc) return {
10501
+ ok: false,
10502
+ error: sc.err
10503
+ };
10504
+ const tasks = [];
10505
+ if (args.keys && Object.keys(args.keys).length) tasks.push(writeUiState(uiStateFile(sc.cwd), args.keys));
10506
+ if (args.globals && Object.keys(args.globals).length) tasks.push(writeUiState(uiGlobalsFile(), args.globals));
10507
+ const failed = (await Promise.all(tasks)).find((item) => !item.ok);
10508
+ if (failed && !failed.ok) {
10509
+ const keyCount = Object.keys(args.keys ?? {}).length;
10510
+ const globalCount = Object.keys(args.globals ?? {}).length;
10511
+ debugRecord(ctx, sc.cwd, "界面状态镜像写入失败: " + failed.error + "(keys=" + keyCount + " 键, globals=" + globalCount + " 键)", "warn");
10512
+ return {
10513
+ ok: false,
10514
+ error: failed.error
10515
+ };
10516
+ }
10517
+ return { ok: true };
10518
+ },
10299
10519
  "edrv.accept": async (args) => {
10300
10520
  const sc = await requireSession(ctx, args.sessionId);
10301
10521
  if ("err" in sc) return {
@@ -10784,12 +11004,13 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
10784
11004
  };
10785
11005
  },
10786
11006
  "edrv.debug": async (args) => {
11007
+ const text = String(args.text ?? "");
10787
11008
  const sc = await requireSession(ctx, args.sessionId);
10788
- if ("err" in sc) return {
10789
- ok: false,
10790
- error: sc.err
10791
- };
10792
- debugRecord(ctx, sc.cwd, String(args.text ?? ""), args.level);
11009
+ if ("err" in sc) {
11010
+ debugRecord(ctx, UI_AUDIT_CWD, text, args.level);
11011
+ return { ok: true };
11012
+ }
11013
+ debugRecord(ctx, sc.cwd, text, args.level);
10793
11014
  return { ok: true };
10794
11015
  },
10795
11016
  "edrv.dlog.list": async (args) => {