dsh-vscode-mode 0.1.17 → 0.1.20

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
@@ -1243,15 +1243,18 @@ async function appendArchiveEntries(ctx, cwd, entries, session) {
1243
1243
  console.error("edrv appendArchiveEntries failed", error);
1244
1244
  }
1245
1245
  }
1246
- /** 归档记录:标记 archived、按批次写入归档、落盘工作区桶。 */
1247
- async function archiveRecords(ctx, session, cwd, bucket, recs, reason) {
1246
+ /**
1247
+ * 归档记录:标记 archived、按批次写入归档、落盘工作区桶。
1248
+ * @param opts.deferSave 为 true 时只写归档、不落盘桶(由批量调用方统一 saveBucket 一次)
1249
+ */
1250
+ async function archiveRecords(ctx, session, cwd, bucket, recs, reason, opts) {
1248
1251
  if (!recs.length) return;
1249
1252
  const fresh = recs.filter((r) => !r.archived);
1250
1253
  for (const r of recs) r.archived = true;
1251
1254
  const entries = [];
1252
1255
  for (const list of groupByBatch(recs).values()) entries.push(archiveEntryFor(list, cwd, reason));
1253
1256
  await appendArchiveEntries(ctx, cwd, entries, session);
1254
- if (fresh.length) await saveBucket(ctx, cwd, bucket, session);
1257
+ if (fresh.length && !opts?.deferSave) await saveBucket(ctx, cwd, bucket, session);
1255
1258
  }
1256
1259
  //#endregion
1257
1260
  //#region src/registry.ts
@@ -2402,6 +2405,52 @@ async function projectRefresh(ctx, workspacePath, serverName) {
2402
2405
  return projectOf(ctx, workspacePath, workspace.title ?? "");
2403
2406
  }
2404
2407
  //#endregion
2408
+ //#region src/tree.ts
2409
+ /** 目录在文件前、名称按小写字母序的比较器(对齐 VSCode 资源管理器习惯)。 */
2410
+ function entryOrder(a, b) {
2411
+ const aDir = a.type === "directory" ? 0 : 1;
2412
+ const bDir = b.type === "directory" ? 0 : 1;
2413
+ if (aDir !== bDir) return aDir - bDir;
2414
+ const la = a.name.toLocaleLowerCase("en-US");
2415
+ const lb = b.name.toLocaleLowerCase("en-US");
2416
+ return la < lb ? -1 : la > lb ? 1 : 0;
2417
+ }
2418
+ /**
2419
+ * 归一化目录相对路径:去空白、反斜杠→斜杠、去首尾斜杠、`.`→''。
2420
+ * 任一路径段为 `..` 返回 null(拒绝越界)。
2421
+ * @author ddj 2026年08月26号
2422
+ * @param path 原始相对路径('' 表示根目录)
2423
+ * @returns 归一化相对路径('' = 根),非法返回 null
2424
+ */
2425
+ function normalizeRel(path) {
2426
+ const rel = String(path ?? "").trim().replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
2427
+ if (!rel) return "";
2428
+ const segs = rel.split("/").filter((seg) => seg !== ".");
2429
+ if (!segs.length) return "";
2430
+ for (const seg of segs) if (seg === "..") return null;
2431
+ return segs.join("/");
2432
+ }
2433
+ /**
2434
+ * fs.listDir 结果 → TreeEntry 数组:相对路径拼接 + 目录优先排序。
2435
+ * @author ddj 2026年08月26号
2436
+ * @param rel 父目录相对路径('' = 根)
2437
+ * @param children 目录子项
2438
+ * @returns 排序后的树条目
2439
+ */
2440
+ function toTreeEntries(rel, children) {
2441
+ const out = [];
2442
+ for (const child of children) {
2443
+ if (!child || typeof child.name !== "string" || !child.name) continue;
2444
+ out.push({
2445
+ name: child.name,
2446
+ path: rel ? rel + "/" + child.name : child.name,
2447
+ type: child.type === "directory" ? "directory" : child.type === "file" ? "file" : "other",
2448
+ size: typeof child.size === "number" ? child.size : void 0
2449
+ });
2450
+ }
2451
+ return out.sort(entryOrder);
2452
+ }
2453
+ //#endregion
2405
2454
  //#region src/rpc.ts
2406
2455
  /** cwd → Promise 链:串行化 debug 日志追加(fs read+write 非原子,避免并发丢行)。 */
2407
2456
  const debugWriteQueues = /* @__PURE__ */ new Map();
@@ -2437,6 +2486,53 @@ async function requireSession(ctx, sessionId) {
2437
2486
  cwd
2438
2487
  };
2439
2488
  }
2489
+ /**
2490
+ * 批量决策核心:一次会话/桶解析 + 逐项处理 + 统一落盘(accept/reject 单条与 decideBatch 共用)。
2491
+ * rejected 项先做回滚,失败记入该项 error 且不改决策;本批次新增"已解决"记录一次性归档,
2492
+ * 有任何成功项则整桶只写盘一次,避免逐条读写整个 sidecar。
2493
+ * @author ddj 2026年08月25号
2494
+ * @param items 决策项(按数组顺序处理,rejected 的先后即回滚顺序)
2495
+ * @returns 逐项结果(ok 项含更新后的记录视图)
2496
+ */
2497
+ async function applyDecisions(ctx, session, cwd, bucket, items) {
2498
+ const results = [];
2499
+ const resolved = [];
2500
+ let changed = false;
2501
+ for (const item of items) {
2502
+ const record = bucket.get(item.callId);
2503
+ if (!record) {
2504
+ results.push({
2505
+ callId: item.callId,
2506
+ ok: false,
2507
+ error: "记录不存在"
2508
+ });
2509
+ continue;
2510
+ }
2511
+ if (item.decision === "rejected") {
2512
+ const outcome = (item.scope ?? "call") === "hunk" ? await revertHunk(ctx, session, record, item.hunkIndex ?? -1) : await revertCall(ctx, session, record);
2513
+ if (!outcome.ok) {
2514
+ results.push({
2515
+ callId: item.callId,
2516
+ ok: false,
2517
+ error: outcome.error
2518
+ });
2519
+ continue;
2520
+ }
2521
+ }
2522
+ const wasResolved = recordResolved(record);
2523
+ markDecision(record, item.scope ?? "call", item.hunkIndex, item.decision);
2524
+ changed = true;
2525
+ if (!wasResolved && recordResolved(record)) resolved.push(record);
2526
+ results.push({
2527
+ callId: item.callId,
2528
+ ok: true,
2529
+ record: recView(record)
2530
+ });
2531
+ }
2532
+ if (resolved.length) await archiveRecords(ctx, session, cwd, bucket, resolved, items.some((item) => item.decision === "rejected") ? "已处理(回滚)" : "已处理", { deferSave: true });
2533
+ if (changed) await saveBucket(ctx, cwd, bucket, session);
2534
+ return results;
2535
+ }
2440
2536
  /** 各方法 handler 表(类型由 shared/rpc 的 RpcHandlerMap 约束)。 */
2441
2537
  function buildHandlers(ctx, registry, searcher = newSearcher(ctx)) {
2442
2538
  return {
@@ -2448,7 +2544,7 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx)) {
2448
2544
  };
2449
2545
  const bucket = await bucketOf(registry, ctx, sc.cwd);
2450
2546
  const want = Array.isArray(args.callIds) ? new Set(args.callIds) : null;
2451
- if (!want) await autoArchiveStale(ctx, sc.session, sc.cwd, bucket);
2547
+ if (!want && args.skipStale !== true) await autoArchiveStale(ctx, sc.session, sc.cwd, bucket);
2452
2548
  const out = [];
2453
2549
  for (const rec of bucket.values()) {
2454
2550
  if (!want && rec.archived) continue;
@@ -2468,17 +2564,19 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx)) {
2468
2564
  error: sc.err
2469
2565
  };
2470
2566
  const bucket = await bucketOf(registry, ctx, sc.cwd);
2471
- const record = bucket.get(args.callId);
2472
- if (!record) return {
2567
+ const item = (await applyDecisions(ctx, sc.session, sc.cwd, bucket, [{
2568
+ callId: args.callId,
2569
+ scope: args.scope,
2570
+ hunkIndex: args.hunkIndex,
2571
+ decision: "accepted"
2572
+ }]))[0];
2573
+ if (!item?.ok) return {
2473
2574
  ok: false,
2474
- error: "记录不存在"
2575
+ error: item?.error ?? "操作失败"
2475
2576
  };
2476
- markDecision(record, args.scope ?? "call", args.hunkIndex, "accepted");
2477
- if (recordResolved(record)) await archiveRecords(ctx, sc.session, sc.cwd, bucket, [record], "已处理");
2478
- else await saveBucket(ctx, sc.cwd, bucket, sc.session);
2479
2577
  return {
2480
2578
  ok: true,
2481
- record: recView(record)
2579
+ record: item.record
2482
2580
  };
2483
2581
  },
2484
2582
  "edrv.reject": async (args) => {
@@ -2488,24 +2586,31 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx)) {
2488
2586
  error: sc.err
2489
2587
  };
2490
2588
  const bucket = await bucketOf(registry, ctx, sc.cwd);
2491
- const record = bucket.get(args.callId);
2492
- if (!record) return {
2589
+ const item = (await applyDecisions(ctx, sc.session, sc.cwd, bucket, [{
2590
+ callId: args.callId,
2591
+ scope: args.scope,
2592
+ hunkIndex: args.hunkIndex,
2593
+ decision: "rejected"
2594
+ }]))[0];
2595
+ if (!item?.ok) return {
2493
2596
  ok: false,
2494
- error: "记录不存在"
2597
+ error: item?.error ?? "操作失败"
2495
2598
  };
2496
- const scope = args.scope ?? "call";
2497
- const outcome = scope === "hunk" ? await revertHunk(ctx, sc.session, record, args.hunkIndex ?? -1) : await revertCall(ctx, sc.session, record);
2498
- if (!outcome.ok) return {
2599
+ return {
2600
+ ok: true,
2601
+ record: item.record
2602
+ };
2603
+ },
2604
+ "edrv.decideBatch": async (args) => {
2605
+ const sc = await requireSession(ctx, args.sessionId);
2606
+ if ("err" in sc) return {
2499
2607
  ok: false,
2500
- error: outcome.error
2608
+ error: sc.err
2501
2609
  };
2502
- markDecision(record, scope, args.hunkIndex, "rejected");
2503
- record.at = (/* @__PURE__ */ new Date()).toISOString();
2504
- if (recordResolved(record)) await archiveRecords(ctx, sc.session, sc.cwd, bucket, [record], "已处理(回滚)");
2505
- else await saveBucket(ctx, sc.cwd, bucket, sc.session);
2610
+ const bucket = await bucketOf(registry, ctx, sc.cwd);
2506
2611
  return {
2507
2612
  ok: true,
2508
- record: recView(record)
2613
+ results: await applyDecisions(ctx, sc.session, sc.cwd, bucket, Array.isArray(args.items) ? args.items : [])
2509
2614
  };
2510
2615
  },
2511
2616
  "edrv.read": async (args) => {
@@ -2779,6 +2884,47 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx)) {
2779
2884
  })
2780
2885
  };
2781
2886
  },
2887
+ "edrv.listDir": async (args) => {
2888
+ const sc = await requireSession(ctx, args.sessionId);
2889
+ if ("err" in sc) return {
2890
+ ok: false,
2891
+ error: sc.err
2892
+ };
2893
+ const fs = ctx.get("fs");
2894
+ if (!fs) return {
2895
+ ok: false,
2896
+ error: "缺少 fs"
2897
+ };
2898
+ try {
2899
+ const rel = normalizeRel(args.path);
2900
+ if (rel === null) return {
2901
+ ok: false,
2902
+ error: "路径不合法"
2903
+ };
2904
+ const target = await resolveTarget(ctx, sc.session, rel || ".");
2905
+ const info = await fs.stat(target);
2906
+ if (!info) return {
2907
+ ok: false,
2908
+ error: "目录不存在"
2909
+ };
2910
+ if (info.type !== "directory") return {
2911
+ ok: false,
2912
+ error: "不是目录"
2913
+ };
2914
+ const children = await fs.listDir(target);
2915
+ return {
2916
+ ok: true,
2917
+ root: fs.processPath(await fs.resolve(".", { cwd: sc.cwd })),
2918
+ path: rel,
2919
+ entries: toTreeEntries(rel, children)
2920
+ };
2921
+ } catch (error) {
2922
+ return {
2923
+ ok: false,
2924
+ error: "读取目录失败:" + String(error)
2925
+ };
2926
+ }
2927
+ },
2782
2928
  "mcp.list": async () => ({
2783
2929
  ok: true,
2784
2930
  ...listMcp(ctx)