dsh-vscode-mode 0.1.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.
Files changed (163) hide show
  1. package/README.md +95 -0
  2. package/assets/compare_idle.png +0 -0
  3. package/assets/compare_select.png +0 -0
  4. package/assets/vendor/monaco/vs/abap-D-t0cyap.js +1 -0
  5. package/assets/vendor/monaco/vs/apex-CcIm7xu6.js +1 -0
  6. package/assets/vendor/monaco/vs/assets/css.worker-URu8fCFR.js +97 -0
  7. package/assets/vendor/monaco/vs/assets/editor.worker-lj3bdIIn.js +30 -0
  8. package/assets/vendor/monaco/vs/assets/editorWebWorkerMain-CA_vMoUU.js +30 -0
  9. package/assets/vendor/monaco/vs/assets/html.worker-D1SL3iM8.js +474 -0
  10. package/assets/vendor/monaco/vs/assets/json.worker-CoJx_OPf.js +62 -0
  11. package/assets/vendor/monaco/vs/assets/ts.worker-BWKtMYOk.js +67722 -0
  12. package/assets/vendor/monaco/vs/azcli-BA0tQDCg.js +1 -0
  13. package/assets/vendor/monaco/vs/basic-languages/monaco.contribution.js +1 -0
  14. package/assets/vendor/monaco/vs/bat-C397hTD6.js +1 -0
  15. package/assets/vendor/monaco/vs/bicep-DF5aW17k.js +2 -0
  16. package/assets/vendor/monaco/vs/cameligo-plsz8qhj.js +1 -0
  17. package/assets/vendor/monaco/vs/clojure-Y2auQMzK.js +1 -0
  18. package/assets/vendor/monaco/vs/coffee-Bu45yuWE.js +1 -0
  19. package/assets/vendor/monaco/vs/cpp-CkKPQIni.js +1 -0
  20. package/assets/vendor/monaco/vs/csharp-CX28MZyh.js +1 -0
  21. package/assets/vendor/monaco/vs/csp-D8uWnyxW.js +1 -0
  22. package/assets/vendor/monaco/vs/css-CaeNmE3S.js +3 -0
  23. package/assets/vendor/monaco/vs/css.worker-CyhWkhHo.js +1 -0
  24. package/assets/vendor/monaco/vs/cssMode-CV6Ay48H.js +1 -0
  25. package/assets/vendor/monaco/vs/cypher-DVThT8BS.js +1 -0
  26. package/assets/vendor/monaco/vs/dart-CmGfCvrO.js +1 -0
  27. package/assets/vendor/monaco/vs/dockerfile-CZqqYdch.js +1 -0
  28. package/assets/vendor/monaco/vs/ecl-30fUercY.js +1 -0
  29. package/assets/vendor/monaco/vs/editor/editor.main.css +1 -0
  30. package/assets/vendor/monaco/vs/editor/editor.main.js +1 -0
  31. package/assets/vendor/monaco/vs/editor/editor.worker.js +1 -0
  32. package/assets/vendor/monaco/vs/editor-KLE6jdfb.js +831 -0
  33. package/assets/vendor/monaco/vs/editor.js +1 -0
  34. package/assets/vendor/monaco/vs/editorWorkerHost-fVE1cjcC.js +35 -0
  35. package/assets/vendor/monaco/vs/elixir-xjPaIfzF.js +1 -0
  36. package/assets/vendor/monaco/vs/flow9-DqtmStfK.js +1 -0
  37. package/assets/vendor/monaco/vs/freemarker2-FWreY7v7.js +3 -0
  38. package/assets/vendor/monaco/vs/fsharp-BOMdg4U1.js +1 -0
  39. package/assets/vendor/monaco/vs/go-D_hbi-Jt.js +1 -0
  40. package/assets/vendor/monaco/vs/graphql-CKUU4kLG.js +1 -0
  41. package/assets/vendor/monaco/vs/handlebars-CZi4pHCH.js +1 -0
  42. package/assets/vendor/monaco/vs/hcl-DTaboeZW.js +1 -0
  43. package/assets/vendor/monaco/vs/html-j_7ZTNRU.js +1 -0
  44. package/assets/vendor/monaco/vs/html.worker-CA3iAimZ.js +1 -0
  45. package/assets/vendor/monaco/vs/htmlMode-27by_KM4.js +1 -0
  46. package/assets/vendor/monaco/vs/index-049xrXrp.js +1 -0
  47. package/assets/vendor/monaco/vs/index-CBVt3dzv.js +5 -0
  48. package/assets/vendor/monaco/vs/index.js +1 -0
  49. package/assets/vendor/monaco/vs/ini-CsNwO04R.js +1 -0
  50. package/assets/vendor/monaco/vs/initialize-DL0l1TGY.js +1 -0
  51. package/assets/vendor/monaco/vs/java-CI4ZMsH9.js +1 -0
  52. package/assets/vendor/monaco/vs/javascript-DTJl4Jn1.js +1 -0
  53. package/assets/vendor/monaco/vs/json.worker-BizpAl9O.js +1 -0
  54. package/assets/vendor/monaco/vs/jsonMode-DLUiMUrI.js +1 -0
  55. package/assets/vendor/monaco/vs/julia-BwzEvaQw.js +1 -0
  56. package/assets/vendor/monaco/vs/kotlin-IUYPiTV8.js +1 -0
  57. package/assets/vendor/monaco/vs/language/css/css.worker.js +66 -0
  58. package/assets/vendor/monaco/vs/language/css/monaco.contribution.js +1 -0
  59. package/assets/vendor/monaco/vs/language/html/html.worker.js +443 -0
  60. package/assets/vendor/monaco/vs/language/html/monaco.contribution.js +1 -0
  61. package/assets/vendor/monaco/vs/language/json/json.worker.js +17 -0
  62. package/assets/vendor/monaco/vs/language/json/monaco.contribution.js +1 -0
  63. package/assets/vendor/monaco/vs/language/typescript/monaco.contribution.js +1 -0
  64. package/assets/vendor/monaco/vs/language/typescript/ts.worker.js +67693 -0
  65. package/assets/vendor/monaco/vs/less-C0eDYdqa.js +2 -0
  66. package/assets/vendor/monaco/vs/lexon-iON-Kj97.js +1 -0
  67. package/assets/vendor/monaco/vs/liquid-CLJYelW6.js +1 -0
  68. package/assets/vendor/monaco/vs/loader.js +1368 -0
  69. package/assets/vendor/monaco/vs/lspLanguageFeatures-BIkJOWLw.js +2 -0
  70. package/assets/vendor/monaco/vs/lua-DtygF91M.js +1 -0
  71. package/assets/vendor/monaco/vs/m3-CsR4AuFi.js +1 -0
  72. package/assets/vendor/monaco/vs/main-BEx-Fmlo.js +15 -0
  73. package/assets/vendor/monaco/vs/main-DsK8pnKg.js +3 -0
  74. package/assets/vendor/monaco/vs/markdown-C_rD0bIw.js +1 -0
  75. package/assets/vendor/monaco/vs/mdx-CpzZIPHF.js +1 -0
  76. package/assets/vendor/monaco/vs/mips-CiYP61RB.js +1 -0
  77. package/assets/vendor/monaco/vs/monaco.contribution-9cKT3C7t.js +1 -0
  78. package/assets/vendor/monaco/vs/monaco.contribution-BE88ZNGY.js +1 -0
  79. package/assets/vendor/monaco/vs/monaco.contribution-BPhsneLd.js +1 -0
  80. package/assets/vendor/monaco/vs/monaco.contribution-BgRy6xDf.js +1 -0
  81. package/assets/vendor/monaco/vs/msdax-C38-sJlp.js +1 -0
  82. package/assets/vendor/monaco/vs/mysql-CdtbpvbG.js +1 -0
  83. package/assets/vendor/monaco/vs/nls/lang/zh-cn.d.ts +1 -0
  84. package/assets/vendor/monaco/vs/nls/lang/zh-cn.js +9 -0
  85. package/assets/vendor/monaco/vs/nls.messages-loader.js +1 -0
  86. package/assets/vendor/monaco/vs/objective-c-CntZFaHX.js +1 -0
  87. package/assets/vendor/monaco/vs/pascal-r6kuqfl_.js +1 -0
  88. package/assets/vendor/monaco/vs/pascaligo-BiXoTmXh.js +1 -0
  89. package/assets/vendor/monaco/vs/perl-DABw_TcH.js +1 -0
  90. package/assets/vendor/monaco/vs/pgsql-me_jFXeX.js +1 -0
  91. package/assets/vendor/monaco/vs/php-D_kh-9LK.js +1 -0
  92. package/assets/vendor/monaco/vs/pla-VfZjczW0.js +1 -0
  93. package/assets/vendor/monaco/vs/postiats-BBSzz8Pk.js +1 -0
  94. package/assets/vendor/monaco/vs/powerquery-Dt-g_2cc.js +1 -0
  95. package/assets/vendor/monaco/vs/powershell-B-7ap1zc.js +1 -0
  96. package/assets/vendor/monaco/vs/protobuf-BmtuEB1A.js +2 -0
  97. package/assets/vendor/monaco/vs/pug-BRpRNeEb.js +1 -0
  98. package/assets/vendor/monaco/vs/python-CqWUUgfu.js +1 -0
  99. package/assets/vendor/monaco/vs/qsharp-BzsFaUU9.js +1 -0
  100. package/assets/vendor/monaco/vs/r-f8dDdrp4.js +1 -0
  101. package/assets/vendor/monaco/vs/razor-hok5y9Km.js +1 -0
  102. package/assets/vendor/monaco/vs/redis-fvZQY4PI.js +1 -0
  103. package/assets/vendor/monaco/vs/redshift-45Et0LQi.js +1 -0
  104. package/assets/vendor/monaco/vs/restructuredtext-C7UUFKFD.js +1 -0
  105. package/assets/vendor/monaco/vs/ruby-CZO8zYTz.js +1 -0
  106. package/assets/vendor/monaco/vs/rust-Bfetafyc.js +1 -0
  107. package/assets/vendor/monaco/vs/sb-3GYllVck.js +1 -0
  108. package/assets/vendor/monaco/vs/scala-foMgrKo1.js +1 -0
  109. package/assets/vendor/monaco/vs/scheme-CHdMtr7p.js +1 -0
  110. package/assets/vendor/monaco/vs/scss-C1cmLt9V.js +3 -0
  111. package/assets/vendor/monaco/vs/shell-ClXCKCEW.js +1 -0
  112. package/assets/vendor/monaco/vs/solidity-MZ6ExpPy.js +1 -0
  113. package/assets/vendor/monaco/vs/sophia-DWkuSsPQ.js +1 -0
  114. package/assets/vendor/monaco/vs/sparql-AUGFYSyk.js +1 -0
  115. package/assets/vendor/monaco/vs/sql-32GpJSV2.js +1 -0
  116. package/assets/vendor/monaco/vs/st-CuDFIVZ_.js +1 -0
  117. package/assets/vendor/monaco/vs/swift-t-PfMj7W.js +1 -0
  118. package/assets/vendor/monaco/vs/systemverilog-Ch4vA8Yt.js +1 -0
  119. package/assets/vendor/monaco/vs/tcl-D74tq1nH.js +1 -0
  120. package/assets/vendor/monaco/vs/toggleHighContrast-qGX7E9o7.js +61 -0
  121. package/assets/vendor/monaco/vs/ts.worker-2QLmBukE.js +1 -0
  122. package/assets/vendor/monaco/vs/tsMode-B0S_kp2q.js +11 -0
  123. package/assets/vendor/monaco/vs/twig-C6taOxMV.js +1 -0
  124. package/assets/vendor/monaco/vs/typescript-_2t_511t.js +1 -0
  125. package/assets/vendor/monaco/vs/typespec-8OfoLt6R.js +1 -0
  126. package/assets/vendor/monaco/vs/vb-Dyb2648j.js +1 -0
  127. package/assets/vendor/monaco/vs/wgsl-BhLXMOR0.js +298 -0
  128. package/assets/vendor/monaco/vs/workers-BBttULjf.js +1 -0
  129. package/assets/vendor/monaco/vs/xml-cZjNDqDT.js +1 -0
  130. package/assets/vendor/monaco/vs/yaml-A1fOIdH6.js +1 -0
  131. package/cordis.patch.yml +5 -0
  132. package/lib/client.js +1914 -0
  133. package/lib/client.js.map +1 -0
  134. package/lib/index.js +1298 -0
  135. package/lib/index.js.map +1 -0
  136. package/package.json +60 -0
  137. package/src/capture.ts +73 -0
  138. package/src/client/events.ts +36 -0
  139. package/src/client/index.ts +37 -0
  140. package/src/client/monaco/diffRender.ts +162 -0
  141. package/src/client/monaco/loader.ts +71 -0
  142. package/src/client/rpc.ts +28 -0
  143. package/src/client/state/records.ts +90 -0
  144. package/src/client/state/regions.ts +81 -0
  145. package/src/client/styles/css.d.ts +2 -0
  146. package/src/client/styles/editor.css +87 -0
  147. package/src/client/ui/DiffBadge.ts +51 -0
  148. package/src/client/ui/DiffBarEmpty.ts +37 -0
  149. package/src/client/ui/DiffBox.ts +79 -0
  150. package/src/client/ui/DiffLauncher.ts +124 -0
  151. package/src/client/ui/EditorView.ts +577 -0
  152. package/src/client/ui/QuickOpen.ts +91 -0
  153. package/src/client/ui/shared.ts +28 -0
  154. package/src/index.ts +43 -0
  155. package/src/model.ts +197 -0
  156. package/src/registry.ts +35 -0
  157. package/src/revert.ts +107 -0
  158. package/src/routes.ts +138 -0
  159. package/src/rpc.ts +295 -0
  160. package/src/shared/rpc.ts +67 -0
  161. package/src/shared/types.ts +121 -0
  162. package/src/store.ts +244 -0
  163. package/src/workspace.ts +74 -0
package/lib/index.js ADDED
@@ -0,0 +1,1298 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { dirname, extname, join, normalize, resolve, sep } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ //#region src/shared/rpc.ts
5
+ /** webServer 精确路由。 */
6
+ const RPC_PATH = "/edrv/rpc";
7
+ //#endregion
8
+ //#region src/routes.ts
9
+ /**
10
+ * dsh-vscode-mode host — webServer 路由注册(/edrv/rpc、/edrv/assets/*、/edrv/vendor/*)。
11
+ * 迁移自原 src/index.ts 的路由部分,路径/行为一字不改。
12
+ * 作者 ddj 2026-08-20
13
+ */
14
+ const IMG_ROUTES = [{
15
+ url: "/edrv/assets/compare-idle.png",
16
+ file: "compare_idle.png"
17
+ }, {
18
+ url: "/edrv/assets/compare-select.png",
19
+ file: "compare_select.png"
20
+ }];
21
+ /** Monaco AMD 构建静态资源(随包发布,离线可用;前缀路由 /edrv/vendor/*)。 */
22
+ const VENDOR_PREFIX = "/edrv/vendor";
23
+ const VENDOR_MIME = {
24
+ ".js": "text/javascript; charset=utf-8",
25
+ ".css": "text/css; charset=utf-8",
26
+ ".json": "application/json",
27
+ ".map": "application/json",
28
+ ".ts": "text/plain; charset=utf-8",
29
+ ".svg": "image/svg+xml",
30
+ ".png": "image/png",
31
+ ".gif": "image/gif",
32
+ ".ico": "image/x-icon",
33
+ ".woff": "font/woff",
34
+ ".woff2": "font/woff2",
35
+ ".ttf": "font/ttf",
36
+ ".html": "text/html; charset=utf-8",
37
+ ".md": "text/markdown"
38
+ };
39
+ /** 图标目录:默认插件包内 assets/(随包发布);config.imageDir 可覆盖(用于自定义换图)。 */
40
+ function imageDirOf(config) {
41
+ const cfg = config;
42
+ if (cfg && typeof cfg.imageDir === "string" && cfg.imageDir) return cfg.imageDir.replace(/\\/g, "/");
43
+ return join(dirname(fileURLToPath(import.meta.url)), "..", "assets");
44
+ }
45
+ /** Monaco AMD 静态目录(随包发布)。 */
46
+ const vendorDir = join(dirname(fileURLToPath(import.meta.url)), "..", "assets", "vendor");
47
+ /**
48
+ * 注册全部 webServer 路由。
49
+ * @author ddj 2026年08月20号
50
+ * @param ctx DSH 上下文
51
+ * @param config 插件配置(可选 imageDir 等)
52
+ * @param handleRpc RPC 分发入口(method,args → RpcResult)
53
+ */
54
+ function registerRoutes(ctx, config, handleRpc) {
55
+ const imgDir = imageDirOf(config);
56
+ const web = ctx.get("webServer");
57
+ if (!web) return;
58
+ ctx.effect(() => web.register({
59
+ kind: "exact",
60
+ path: RPC_PATH,
61
+ handler: async (req, res) => {
62
+ try {
63
+ const chunks = [];
64
+ for await (const chunk of req) chunks.push(chunk);
65
+ let input = {};
66
+ try {
67
+ input = JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}");
68
+ } catch (error) {
69
+ input = {};
70
+ }
71
+ const result = await handleRpc(typeof input.method === "string" ? input.method : "", input.args ?? {});
72
+ res.statusCode = 200;
73
+ res.setHeader("content-type", "application/json");
74
+ res.end(JSON.stringify(result));
75
+ } catch (error) {
76
+ res.statusCode = 500;
77
+ res.end(JSON.stringify({
78
+ ok: false,
79
+ error: String(error)
80
+ }));
81
+ }
82
+ }
83
+ }), "edrv: /edrv/rpc route");
84
+ for (const r of IMG_ROUTES) ctx.effect(() => web.register({
85
+ kind: "exact",
86
+ path: r.url,
87
+ handler: async (req, res) => {
88
+ try {
89
+ const body = await readFile(imgDir + "/" + r.file);
90
+ res.statusCode = 200;
91
+ res.setHeader("content-type", "image/png");
92
+ res.setHeader("cache-control", "no-cache");
93
+ res.end(body);
94
+ } catch (error) {
95
+ res.statusCode = 404;
96
+ res.end();
97
+ }
98
+ }
99
+ }), "edrv: static " + r.file);
100
+ ctx.effect(() => web.register({
101
+ kind: "prefix",
102
+ path: VENDOR_PREFIX,
103
+ handler: async (req, res) => {
104
+ try {
105
+ if (req.method !== "GET" && req.method !== "HEAD") {
106
+ res.statusCode = 405;
107
+ res.end();
108
+ return;
109
+ }
110
+ const pathname = new URL(req.url ?? "/", "http://x").pathname;
111
+ const rel = decodeURIComponent(pathname.slice(12)).replace(/^\/+/, "");
112
+ if (!rel) {
113
+ res.statusCode = 404;
114
+ res.end();
115
+ return;
116
+ }
117
+ const target = resolve(normalize(join(vendorDir, rel)));
118
+ if (target !== vendorDir && !target.startsWith(vendorDir + sep)) {
119
+ res.statusCode = 403;
120
+ res.end();
121
+ return;
122
+ }
123
+ const body = await readFile(target);
124
+ res.statusCode = 200;
125
+ res.setHeader("content-type", VENDOR_MIME[extname(target)] ?? "application/octet-stream");
126
+ res.setHeader("cache-control", "public, max-age=3600");
127
+ res.end(body);
128
+ } catch (error) {
129
+ res.statusCode = 404;
130
+ res.end();
131
+ }
132
+ }
133
+ }), "edrv: /edrv/vendor prefix");
134
+ }
135
+ /**
136
+ * 归一化一条持久化记录(容忍旧版/损坏字段)。
137
+ * @author ddj 2026年08月20号
138
+ * @param raw 反序列化的原始记录
139
+ * @returns 合法记录或 null
140
+ */
141
+ function normalizeRecord(raw) {
142
+ if (!raw || typeof raw !== "object") return null;
143
+ const r = raw;
144
+ if (typeof r.callId !== "string" || typeof r.path !== "string") return null;
145
+ const hunks = Array.isArray(r.hunks) ? r.hunks.filter((h) => h && typeof h.newText === "string") : [];
146
+ const toolName = r.toolName === "write" ? "write" : "edit";
147
+ return {
148
+ callId: r.callId,
149
+ toolName,
150
+ path: r.path,
151
+ before: typeof r.before === "string" ? r.before : null,
152
+ create: r.create === true,
153
+ callHunk: r.callHunk && typeof r.callHunk.oldText === "string" ? {
154
+ oldText: r.callHunk.oldText,
155
+ newText: r.callHunk.newText
156
+ } : null,
157
+ hunks: hunks.map((h) => ({
158
+ oldText: typeof h.oldText === "string" ? h.oldText : null,
159
+ newText: h.newText
160
+ })),
161
+ decisions: r.decisions && typeof r.decisions === "object" ? r.decisions : {
162
+ call: "pending",
163
+ perHunk: hunks.map(() => "pending")
164
+ },
165
+ note: typeof r.note === "string" ? r.note : null,
166
+ superseded: r.superseded === true,
167
+ archived: r.archived === true,
168
+ batch: Number.isInteger(r.batch) ? r.batch : 0,
169
+ at: typeof r.at === "string" ? r.at : (/* @__PURE__ */ new Date()).toISOString()
170
+ };
171
+ }
172
+ /**
173
+ * 写决策到记录(scope=call 整体 / hunk 单个块)。
174
+ * @author ddj 2026年08月20号
175
+ * @param record 记录
176
+ * @param scope 作用域
177
+ * @param idx hunk 下标(仅 scope=hunk 时生效)
178
+ * @param value 决策值
179
+ * @returns 更新后的记录(原对象就地修改)
180
+ */
181
+ function markDecision(record, scope, idx, value) {
182
+ const dec = record.decisions;
183
+ if (scope === "hunk" && Array.isArray(dec.perHunk) && Number.isInteger(idx)) {
184
+ const i = idx;
185
+ if (i >= 0 && i < dec.perHunk.length) dec.perHunk[i] = value;
186
+ } else {
187
+ dec.call = value;
188
+ if (Array.isArray(dec.perHunk)) dec.perHunk = dec.perHunk.map(() => value);
189
+ }
190
+ record.at = (/* @__PURE__ */ new Date()).toISOString();
191
+ return record;
192
+ }
193
+ /** 记录是否已无任何待决策差异(superseded 或全部已决策)。 */
194
+ function recordResolved(record) {
195
+ if (record.superseded === true) return true;
196
+ const perHunk = Array.isArray(record.decisions.perHunk) ? record.decisions.perHunk : [];
197
+ if (perHunk.length) return perHunk.every((v) => v === "accepted" || v === "rejected");
198
+ return record.decisions.call === "accepted" || record.decisions.call === "rejected";
199
+ }
200
+ /**
201
+ * 记录决策摘要(采纳/拒绝/待处理计数,superseded 标记)。
202
+ * @author ddj 2026年08月20号
203
+ * @param record 记录
204
+ * @returns 摘要
205
+ */
206
+ function recSummary(record) {
207
+ const perHunk = Array.isArray(record.decisions.perHunk) ? record.decisions.perHunk : [];
208
+ const n = Math.max(1, Array.isArray(record.hunks) ? record.hunks.length : 1);
209
+ let accepted = 0;
210
+ let rejected = 0;
211
+ let pending = 0;
212
+ if (perHunk.length) for (const v of perHunk) if (v === "accepted") accepted++;
213
+ else if (v === "rejected") rejected++;
214
+ else pending++;
215
+ else if (record.decisions.call === "accepted") accepted = n;
216
+ else if (record.decisions.call === "rejected") rejected = n;
217
+ else pending = n;
218
+ return {
219
+ accepted,
220
+ rejected,
221
+ pending,
222
+ superseded: record.superseded === true
223
+ };
224
+ }
225
+ /**
226
+ * 重建"本批次修改前"内容:把仍待处理(pending)的差异块按 新→旧 顺序从当前内容反解
227
+ * (newText → oldText)。已采纳/已拒绝的块跳过。反解失败的块标记为 stale。
228
+ * @author ddj 2026年08月20号
229
+ * @param records 该文件的活动记录
230
+ * @param content 当前磁盘内容
231
+ * @returns 重建内容与 stale 定位
232
+ */
233
+ function reconstructOriginal(records, content) {
234
+ const pending = [];
235
+ for (const rec of records) {
236
+ const perHunk = Array.isArray(rec.decisions.perHunk) ? rec.decisions.perHunk : [];
237
+ for (let i = 0; i < rec.hunks.length; i++) if ((perHunk.length ? perHunk[i] : rec.decisions.call) === "pending") pending.push({
238
+ rec,
239
+ idx: i
240
+ });
241
+ }
242
+ pending.sort((a, b) => a.rec.at < b.rec.at ? 1 : a.rec.at > b.rec.at ? -1 : 0);
243
+ let out = content;
244
+ const stale = [];
245
+ for (const p of pending) {
246
+ const h = p.rec.hunks[p.idx];
247
+ const precise = p.rec.toolName === "edit" && p.rec.hunks.length <= 1 && p.rec.callHunk ? p.rec.callHunk : h;
248
+ const newText = precise.newText ?? "";
249
+ const at = out.indexOf(newText);
250
+ if (at < 0) {
251
+ stale.push({
252
+ callId: p.rec.callId,
253
+ idx: p.idx
254
+ });
255
+ continue;
256
+ }
257
+ const oldText = precise.oldText === null ? "" : precise.oldText;
258
+ out = out.slice(0, at) + oldText + out.slice(at + newText.length);
259
+ }
260
+ return {
261
+ content: out,
262
+ stale
263
+ };
264
+ }
265
+ /** 文件当前最大批次号(无记录为 0)。 */
266
+ function fileMaxBatch(records, path) {
267
+ let m = 0;
268
+ for (const r of records.values()) if (r.path === path && Number.isInteger(r.batch) && r.batch > m) m = r.batch;
269
+ return m;
270
+ }
271
+ /** 记录桶裁剪:超出 MAX_RECORDS 时剔除最旧的。 */
272
+ function prune(map, max = 200) {
273
+ if (map.size <= max) return;
274
+ const sorted = [...map.values()].sort((a, b) => a.at < b.at ? -1 : 1);
275
+ for (let i = 0; i < sorted.length - max; i++) map.delete(sorted[i].callId);
276
+ }
277
+ /** 按批次分组记录。 */
278
+ function groupByBatch(recs) {
279
+ const map = /* @__PURE__ */ new Map();
280
+ for (const r of recs) {
281
+ const k = Number.isInteger(r.batch) ? r.batch : null;
282
+ if (!map.has(k)) map.set(k, []);
283
+ map.get(k).push(r);
284
+ }
285
+ return map;
286
+ }
287
+ /**
288
+ * 归档条目构造(一条批次一条)。
289
+ * @author ddj 2026年08月20号
290
+ * @param recs 同批次记录(非空)
291
+ * @param cwd 工作区
292
+ * @param reason 归档原因
293
+ */
294
+ function archiveEntryFor(recs, cwd, reason) {
295
+ const first = recs[0];
296
+ return {
297
+ at: (/* @__PURE__ */ new Date()).toISOString(),
298
+ cwd,
299
+ path: first.path,
300
+ batch: Number.isInteger(first.batch) ? first.batch : null,
301
+ reason,
302
+ records: recs.map((r) => ({
303
+ callId: r.callId,
304
+ toolName: r.toolName,
305
+ path: r.path,
306
+ create: r.create === true,
307
+ callHunk: r.callHunk,
308
+ hunks: r.hunks,
309
+ decisions: r.decisions,
310
+ note: r.note ?? null,
311
+ before: typeof r.before === "string" ? r.before : null,
312
+ superseded: r.superseded === true,
313
+ batch: Number.isInteger(r.batch) ? r.batch : null,
314
+ at: r.at,
315
+ summary: recSummary(r)
316
+ }))
317
+ };
318
+ }
319
+ //#endregion
320
+ //#region src/store.ts
321
+ const SIDECAR = ".dsh-edit-review.json";
322
+ const SIDECAR_ARCHIVE = ".dsh-edit-review-archive.json";
323
+ /** 会话沙箱策略(policyOf):可能不存在,调用方需容忍 undefined。 */
324
+ function policyOf(ctx, session) {
325
+ const svc = ctx.get("sandboxPolicy");
326
+ if (!svc) return void 0;
327
+ return session ? svc.resolve({ session }) : svc.resolve();
328
+ }
329
+ /** 按会话 cwd 解析目标路径。 */
330
+ async function resolveTarget(ctx, session, path) {
331
+ const fs = ctx.get("fs");
332
+ const cwd = session?.header?.cwd;
333
+ return fs.resolve(path, cwd ? { cwd } : {});
334
+ }
335
+ /** 读取工作区主 sidecar 文本(缺失/失败 → null)。 */
336
+ async function readSidecarText(ctx, cwd) {
337
+ const fs = ctx.get("fs");
338
+ if (!fs || !cwd) return null;
339
+ try {
340
+ return await fs.readText(await fs.resolve(SIDECAR, { cwd }));
341
+ } catch (error) {
342
+ return null;
343
+ }
344
+ }
345
+ /**
346
+ * 解析主 sidecar:v2 直接返回;v1 迁移为 v2 结构(按会话 cwd 分桶)。
347
+ * @author ddj 2026年08月20号
348
+ * @param text sidecar 文本
349
+ * @returns v2 数据或 null
350
+ */
351
+ function parseSidecar(text) {
352
+ if (!text) return null;
353
+ try {
354
+ const data = JSON.parse(text);
355
+ if (data && typeof data === "object" && data.version === 2 && data.workspaces && typeof data.workspaces === "object") return data;
356
+ if (data && data.version === 1 && data.sessions && typeof data.sessions === "object") {
357
+ const workspaces = {};
358
+ for (const bucket of Object.values(data.sessions)) {
359
+ const b = bucket;
360
+ if (typeof b.cwd === "string" && b.records) workspaces[b.cwd] = {
361
+ at: typeof b.at === "number" ? b.at : Date.now(),
362
+ records: b.records
363
+ };
364
+ }
365
+ return {
366
+ version: 2,
367
+ updatedAt: typeof data.updatedAt === "string" ? data.updatedAt : (/* @__PURE__ */ new Date()).toISOString(),
368
+ workspaces
369
+ };
370
+ }
371
+ return null;
372
+ } catch (error) {
373
+ return null;
374
+ }
375
+ }
376
+ /** 加载某工作区的记录桶(内存 Map;缺失 → 空)。 */
377
+ async function loadBucket(ctx, cwd) {
378
+ const data = parseSidecar(await readSidecarText(ctx, cwd));
379
+ const map = /* @__PURE__ */ new Map();
380
+ if (data && data.workspaces[cwd] && typeof data.workspaces[cwd].records === "object") for (const rec of Object.values(data.workspaces[cwd].records)) {
381
+ const n = normalizeRecord(rec);
382
+ if (n) map.set(n.callId, n);
383
+ }
384
+ return map;
385
+ }
386
+ /** 保存某工作区的记录桶(写前合并,不覆盖其他工作区)。 */
387
+ async function saveBucket(ctx, cwd, recsMap, session) {
388
+ const fs = ctx.get("fs");
389
+ if (!fs || !cwd) return;
390
+ try {
391
+ const target = await fs.resolve(SIDECAR, { cwd });
392
+ const existing = parseSidecar(await readSidecarText(ctx, cwd)) ?? {
393
+ version: 2,
394
+ updatedAt: "",
395
+ workspaces: {}
396
+ };
397
+ existing.workspaces[cwd] = {
398
+ at: Date.now(),
399
+ records: Object.fromEntries(recsMap)
400
+ };
401
+ existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
402
+ await fs.writeText(target, JSON.stringify(existing), void 0, void 0, policyOf(ctx, session));
403
+ } catch (error) {
404
+ console.error("edrv saveBucket failed", error);
405
+ }
406
+ }
407
+ /**
408
+ * 读取记录目标文件内容(按 path 缓存一次;缺失/过大/失败 → null/'' 占位)。
409
+ * @author ddj 2026年08月20号
410
+ * @param cache path → 内容(null=缺失/读取失败,''=过大跳过)
411
+ */
412
+ async function readForCache(ctx, session, cache, path) {
413
+ if (cache.has(path)) return cache.get(path) ?? null;
414
+ const fs = ctx.get("fs");
415
+ if (!fs) {
416
+ cache.set(path, null);
417
+ return null;
418
+ }
419
+ try {
420
+ const target = await resolveTarget(ctx, session, path);
421
+ const info = await fs.stat(target);
422
+ if (!info || info.type !== "file") {
423
+ cache.set(path, null);
424
+ return null;
425
+ }
426
+ if ((info.size ?? 0) > 8388608) {
427
+ cache.set(path, "");
428
+ return "";
429
+ }
430
+ const content = await fs.readText(target);
431
+ cache.set(path, content);
432
+ return content;
433
+ } catch (error) {
434
+ cache.set(path, null);
435
+ return null;
436
+ }
437
+ }
438
+ /**
439
+ * 判断记录是否已无任何可操作差异(stale):待决策 hunk 的新文本在磁盘均找不到,
440
+ * 或新建文件已不存在,或全为空差异。满足则应由 host 自动归档。
441
+ * @author ddj 2026年08月20号
442
+ */
443
+ async function recordIsStale(ctx, session, cache, rec) {
444
+ if (rec.superseded === true) return true;
445
+ if (!Array.isArray(rec.hunks) || !rec.hunks.length) return true;
446
+ const perHunk = Array.isArray(rec.decisions.perHunk) ? rec.decisions.perHunk : [];
447
+ let hasLocatablePending = false;
448
+ for (let i = 0; i < rec.hunks.length; i++) {
449
+ const st = perHunk.length ? perHunk[i] : rec.decisions.call;
450
+ if (st === "accepted" || st === "rejected") continue;
451
+ if (rec.create === true) {
452
+ if (await readForCache(ctx, session, cache, rec.path) !== null) {
453
+ hasLocatablePending = true;
454
+ break;
455
+ }
456
+ continue;
457
+ }
458
+ const precise = rec.toolName === "edit" && rec.callHunk ? rec.callHunk : rec.hunks[i];
459
+ const oldText = precise?.oldText ?? null;
460
+ const newText = precise?.newText ?? null;
461
+ if (oldText !== null && oldText === newText) continue;
462
+ if (newText === null || newText === void 0) continue;
463
+ const content = await readForCache(ctx, session, cache, rec.path);
464
+ if (content !== null && content.indexOf(newText) >= 0) {
465
+ hasLocatablePending = true;
466
+ break;
467
+ }
468
+ }
469
+ return !hasLocatablePending;
470
+ }
471
+ /**
472
+ * edrv.list 全量轮询时自动清理 stale 记录:标记 superseded 并归档,
473
+ * 防止"幽灵差异"长期留在审查列表且无法操作。
474
+ * @author ddj 2026年08月20号
475
+ * @returns 自动归档条数
476
+ */
477
+ async function autoArchiveStale(ctx, session, cwd, bucket) {
478
+ if (!ctx.get("fs")) return 0;
479
+ const cache = /* @__PURE__ */ new Map();
480
+ const stale = [];
481
+ for (const rec of bucket.values()) {
482
+ if (rec.archived || rec.superseded === true) continue;
483
+ if (await recordIsStale(ctx, session, cache, rec)) stale.push(rec);
484
+ }
485
+ if (!stale.length) return 0;
486
+ for (const r of stale) {
487
+ r.superseded = true;
488
+ r.at = (/* @__PURE__ */ new Date()).toISOString();
489
+ }
490
+ await archiveRecords(ctx, session, cwd, bucket, stale, "差异无法定位(已被后续修改覆盖),自动归档");
491
+ return stale.length;
492
+ }
493
+ /** 读取归档 sidecar 文本(缺失/失败 → null)。 */
494
+ async function readArchiveText(ctx, cwd) {
495
+ const fs = ctx.get("fs");
496
+ if (!fs || !cwd) return null;
497
+ try {
498
+ return await fs.readText(await fs.resolve(SIDECAR_ARCHIVE, { cwd }));
499
+ } catch (error) {
500
+ return null;
501
+ }
502
+ }
503
+ /** 解析归档 sidecar(损坏 → 空数组)。 */
504
+ function parseArchive(text) {
505
+ if (!text) return [];
506
+ try {
507
+ const data = JSON.parse(text);
508
+ if (data && typeof data === "object" && Array.isArray(data.batches)) return data.batches;
509
+ } catch (error) {}
510
+ return [];
511
+ }
512
+ /** 追加归档条目(按 cwd+path+batch 合并,同 callId 去重)。 */
513
+ async function appendArchiveEntries(ctx, cwd, entries, session) {
514
+ const fs = ctx.get("fs");
515
+ if (!fs || !cwd) return;
516
+ try {
517
+ const target = await fs.resolve(SIDECAR_ARCHIVE, { cwd });
518
+ const existing = parseArchive(await readArchiveText(ctx, cwd));
519
+ for (const e of entries) {
520
+ const idx = existing.findIndex((x) => x.cwd === e.cwd && x.path === e.path && x.batch === e.batch);
521
+ if (idx >= 0) {
522
+ const exist = existing[idx];
523
+ const seen = new Set((exist.records || []).map((r) => r.callId));
524
+ for (const rec of e.records) if (!seen.has(String(rec.callId))) {
525
+ exist.records.push(rec);
526
+ seen.add(String(rec.callId));
527
+ }
528
+ exist.lastAt = e.at;
529
+ exist.reason = e.reason;
530
+ } else existing.push(e);
531
+ }
532
+ const data = {
533
+ version: 1,
534
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
535
+ batches: existing
536
+ };
537
+ await fs.writeText(target, JSON.stringify(data), void 0, void 0, policyOf(ctx, session));
538
+ } catch (error) {
539
+ console.error("edrv appendArchiveEntries failed", error);
540
+ }
541
+ }
542
+ /** 归档记录:标记 archived、按批次写入归档、落盘工作区桶。 */
543
+ async function archiveRecords(ctx, session, cwd, bucket, recs, reason) {
544
+ if (!recs.length) return;
545
+ const fresh = recs.filter((r) => !r.archived);
546
+ for (const r of recs) r.archived = true;
547
+ const entries = [];
548
+ for (const list of groupByBatch(recs).values()) entries.push(archiveEntryFor(list, cwd, reason));
549
+ await appendArchiveEntries(ctx, cwd, entries, session);
550
+ if (fresh.length) await saveBucket(ctx, cwd, bucket, session);
551
+ }
552
+ //#endregion
553
+ //#region src/registry.ts
554
+ /** 按 sessionId 取会话;缺省时仅当唯一活跃会话才返回。 */
555
+ function sessionOf(ctx, sessionId) {
556
+ const sessions = ctx.get("sessions");
557
+ if (!sessions) return void 0;
558
+ if (sessionId) return sessions.get(sessionId);
559
+ const live = sessions.list().filter((s) => s.id);
560
+ return live.length === 1 ? live[0] : void 0;
561
+ }
562
+ /** 会话工作区 cwd。 */
563
+ function cwdOf(session) {
564
+ return session?.header?.cwd ?? null;
565
+ }
566
+ /** 取(或加载)某工作区记录桶。 */
567
+ function bucketOf(registry, ctx, cwd) {
568
+ const existing = registry.get(cwd);
569
+ if (existing) return Promise.resolve(existing);
570
+ return loadBucket(ctx, cwd).then((map) => {
571
+ registry.set(cwd, map);
572
+ return map;
573
+ });
574
+ }
575
+ //#endregion
576
+ //#region src/capture.ts
577
+ /**
578
+ * 捕获一次工具执行结果(tools/result):edit/write 成功时构造记录、
579
+ * 递增文件批次、融合归档被后续修改取代的旧差异、落盘。
580
+ * @author ddj 2026年08月20号
581
+ * @param ctx DSH 上下文
582
+ * @param registry 工作区记录桶注册表
583
+ * @param exec 工具执行描述(name/arguments/callId/agent.session)
584
+ * @param result 执行结果(value/meta/isError)
585
+ */
586
+ async function captureToolResult(ctx, registry, exec, result) {
587
+ const session = exec?.agent?.session;
588
+ if (!session || exec?.name !== "edit" && exec?.name !== "write") return;
589
+ if (result?.isError || !result?.value) return;
590
+ try {
591
+ const cwd = cwdOf(session);
592
+ if (!cwd) return;
593
+ const value = result.value;
594
+ const args = exec.arguments || {};
595
+ const path = typeof value.path === "string" ? value.path : args.file_path;
596
+ if (!path) return;
597
+ const metaDiffs = Array.isArray(result.meta?.diffs) ? result.meta.diffs : [];
598
+ const before = typeof value.before === "string" ? value.before : null;
599
+ const create = exec.name === "write" && value.before === null;
600
+ const callHunk = exec.name === "edit" && typeof args.old_string === "string" ? {
601
+ oldText: args.old_string,
602
+ newText: args.new_string
603
+ } : null;
604
+ const synthHunk = create ? {
605
+ oldText: null,
606
+ newText: typeof args.content === "string" ? args.content : typeof value.after === "string" ? value.after : ""
607
+ } : null;
608
+ const hunks = metaDiffs.length ? metaDiffs : synthHunk ? [synthHunk] : callHunk ? [callHunk] : [];
609
+ if (!hunks.length) return;
610
+ const record = {
611
+ callId: exec.callId,
612
+ toolName: exec.name,
613
+ path,
614
+ before,
615
+ create,
616
+ callHunk,
617
+ hunks,
618
+ decisions: {
619
+ call: "pending",
620
+ perHunk: hunks.map(() => "pending")
621
+ },
622
+ note: create ? "新建文件:全部拒绝将删除该文件" : before === null ? "未捕获修改前内容(大文件)" : null,
623
+ superseded: false,
624
+ archived: false,
625
+ batch: 0,
626
+ at: (/* @__PURE__ */ new Date()).toISOString()
627
+ };
628
+ let bucket = registry.get(cwd);
629
+ if (!bucket) {
630
+ bucket = await loadBucket(ctx, cwd);
631
+ registry.set(cwd, bucket);
632
+ }
633
+ record.batch = fileMaxBatch(bucket, path) + 1;
634
+ bucket.set(exec.callId, record);
635
+ const prior = [];
636
+ for (const r of bucket.values()) if (r.path === path && r.callId !== exec.callId && !r.archived && (r.batch ?? 0) < record.batch) prior.push(r);
637
+ if (prior.length) await archiveRecords(ctx, session, cwd, bucket, prior, prior.every(recordResolved) ? "已处理且被后续修改取代" : "被后续修改融合(内容已含其效果)");
638
+ prune(bucket);
639
+ await saveBucket(ctx, cwd, bucket, session);
640
+ } catch (error) {
641
+ console.error("edrv capture failed", error);
642
+ }
643
+ }
644
+ //#endregion
645
+ //#region src/workspace.ts
646
+ /** 文件清单缓存 TTL 与扫描上限(避免大工作区反复全量扫描)。 */
647
+ const FILE_INDEX_TTL = 6e4;
648
+ const SCAN_CAP = 6e3;
649
+ /** cwd → { at, files }(随会话销毁清理)。 */
650
+ const fileIndex = /* @__PURE__ */ new Map();
651
+ /**
652
+ * 扫描工作区文件清单(快速打开/搜索用):subprocess(Windows powershell / POSIX find),
653
+ * 排除常见重目录,容量上限 SCAN_CAP;结果缓存(TTL FILE_INDEX_TTL)。失败返回 null。
654
+ * @author ddj 2026年08月20号
655
+ * @param ctx DSH 上下文
656
+ * @param session 会话(取工作区根)
657
+ * @param cwd 工作区目录
658
+ * @returns 绝对路径清单;不可用(无 subprocess/fs)或扫描失败返回 null
659
+ */
660
+ async function listWorkspaceFiles(ctx, session, cwd) {
661
+ const cached = fileIndex.get(cwd);
662
+ if (cached && Date.now() - cached.at < FILE_INDEX_TTL) return cached.files;
663
+ const sub = ctx.get("subprocess");
664
+ const fs = ctx.get("fs");
665
+ if (!sub || !fs) return null;
666
+ const policy = policyOf(ctx, session);
667
+ let root;
668
+ try {
669
+ const rootTarget = await fs.resolve(policy?.workspaceRoot ?? ".", {});
670
+ root = fs.processPath(rootTarget);
671
+ } catch (error) {
672
+ root = cwd;
673
+ }
674
+ const lines = [];
675
+ const run = async (argv) => {
676
+ const handle = sub.spawn({
677
+ argv,
678
+ stdio: {
679
+ stdout: { maxBytes: 8 << 20 },
680
+ stderr: { maxBytes: 65536 },
681
+ stdin: "ignore"
682
+ },
683
+ graceMs: 2e4
684
+ });
685
+ const outcome = await handle.done;
686
+ const code = outcome?.exitCode ?? outcome?.code;
687
+ if (code !== 0) throw new Error("scan exit " + code);
688
+ const text = handle.collected?.stdout?.readFrom(0).text ?? "";
689
+ for (const ln of text.split(/\r?\n/)) {
690
+ const s = ln.trim();
691
+ if (s) lines.push(s);
692
+ }
693
+ };
694
+ try {
695
+ if (process.platform === "win32") await run([
696
+ "powershell",
697
+ "-NoProfile",
698
+ "-NonInteractive",
699
+ "-Command",
700
+ "Get-ChildItem -LiteralPath \"" + String(root).replace(/"/g, "\"\"") + "\" -Recurse -File -ErrorAction SilentlyContinue | Where-Object { $_.FullName -notmatch '(node_modules|\\.git|\\.tmp|\\.cache|dist|build|vendor|coverage|__pycache__)' } | Select-Object -First 6000 | ForEach-Object { $_.FullName }"
701
+ ]);
702
+ else await run([
703
+ "/bin/sh",
704
+ "-c",
705
+ "find " + JSON.stringify(root) + " -type f -not -path '*/node_modules/*' -not -path '*/.git/*' -not -path '*/.tmp/*' -not -path '*/.cache/*' -not -path '*/dist/*' -not -path '*/build/*' -not -path '*/vendor/*' -not -path '*/coverage/*' 2>/dev/null | head -n 6000"
706
+ ]);
707
+ } catch (error) {
708
+ console.error("edrv scan failed", error);
709
+ return null;
710
+ }
711
+ const files = lines.slice(0, SCAN_CAP);
712
+ if (files.length) fileIndex.set(cwd, {
713
+ at: Date.now(),
714
+ files
715
+ });
716
+ return files;
717
+ }
718
+ /** 会话销毁时清理文件索引缓存。 */
719
+ function dropFileIndex(cwd) {
720
+ fileIndex.delete(cwd);
721
+ }
722
+ //#endregion
723
+ //#region src/revert.ts
724
+ /**
725
+ * 删除新建文件(拒绝创建时):subprocess 删除,路径先经 fs.contains 校验工作区边界。
726
+ * @author ddj 2026年08月20号
727
+ * @returns 成功或失败原因
728
+ */
729
+ async function deleteCreated(ctx, session, record) {
730
+ const sub = ctx.get("subprocess");
731
+ const fs = ctx.get("fs");
732
+ if (!sub || !fs) return {
733
+ ok: false,
734
+ error: "回滚不可用:缺少 subprocess/fs"
735
+ };
736
+ const policy = policyOf(ctx, session);
737
+ const rootTarget = await fs.resolve(policy?.workspaceRoot ?? ".", {});
738
+ const target = await resolveTarget(ctx, session, record.path);
739
+ if (!fs.contains(rootTarget, target)) return {
740
+ ok: false,
741
+ error: "拒绝删除:目标不在会话工作区内"
742
+ };
743
+ const p = fs.processPath(target);
744
+ const attempt = async (argv) => {
745
+ const handle = sub.spawn({
746
+ argv,
747
+ stdio: {
748
+ stdout: { maxBytes: 65536 },
749
+ stderr: { maxBytes: 65536 },
750
+ stdin: "ignore"
751
+ },
752
+ graceMs: 1e4
753
+ });
754
+ const outcome = await handle.done;
755
+ const code = outcome?.exitCode ?? outcome?.code;
756
+ if (code !== 0) {
757
+ const err = handle.collected?.stderr?.readFrom(0).text ?? "";
758
+ throw new Error("exit " + code + " " + err);
759
+ }
760
+ };
761
+ try {
762
+ await attempt([
763
+ "powershell",
764
+ "-NoProfile",
765
+ "-NonInteractive",
766
+ "-Command",
767
+ "Remove-Item -LiteralPath \"" + p + "\" -Force"
768
+ ]);
769
+ return { ok: true };
770
+ } catch (error) {
771
+ try {
772
+ await attempt([
773
+ "/bin/rm",
774
+ "-f",
775
+ "--",
776
+ p
777
+ ]);
778
+ return { ok: true };
779
+ } catch (error2) {
780
+ return {
781
+ ok: false,
782
+ error: "删除失败(文件仍存在):" + String(error) + " / " + String(error2)
783
+ };
784
+ }
785
+ }
786
+ }
787
+ /** 整调用回滚:write/编辑用 before 整文件恢复;新建文件转 deleteCreated。 */
788
+ async function revertCall(ctx, session, record) {
789
+ const fs = ctx.get("fs");
790
+ if (!fs) return {
791
+ ok: false,
792
+ error: "回滚不可用:缺少 fs"
793
+ };
794
+ if (record.before === null) {
795
+ if (!record.create) return {
796
+ ok: false,
797
+ error: "无法回滚:缺少修改前内容(大文件或旧记录)"
798
+ };
799
+ return deleteCreated(ctx, session, record);
800
+ }
801
+ try {
802
+ const target = await resolveTarget(ctx, session, record.path);
803
+ await fs.writeText(target, record.before, void 0, void 0, policyOf(ctx, session));
804
+ return { ok: true };
805
+ } catch (error) {
806
+ return {
807
+ ok: false,
808
+ error: "回滚失败:" + String(error)
809
+ };
810
+ }
811
+ }
812
+ /** 单 hunk 回滚:用 callHunk(edit 单 hunk 精确串)经 fs.editText 反替换。 */
813
+ async function revertHunk(ctx, session, record, idx) {
814
+ const fs = ctx.get("fs");
815
+ if (!fs) return {
816
+ ok: false,
817
+ error: "回滚不可用:缺少 fs"
818
+ };
819
+ const hunk = record.hunks[idx];
820
+ const precise = record.toolName === "edit" && record.hunks.length <= 1 && record.callHunk ? record.callHunk : hunk;
821
+ if (!precise) return {
822
+ ok: false,
823
+ error: "找不到该差异块"
824
+ };
825
+ try {
826
+ const target = await resolveTarget(ctx, session, record.path);
827
+ await fs.editText(target, {
828
+ oldString: precise.newText,
829
+ newString: precise.oldText === null ? "" : precise.oldText
830
+ }, void 0, void 0, policyOf(ctx, session));
831
+ return { ok: true };
832
+ } catch (error) {
833
+ return {
834
+ ok: false,
835
+ error: "回滚失败:该区域可能已被后续修改影响(" + String(error) + ")"
836
+ };
837
+ }
838
+ }
839
+ /**
840
+ * 恢复文件到记录对应状态(批次回滚用):返回错误文案或 null(成功)。
841
+ * @author ddj 2026年08月20号
842
+ */
843
+ async function restoreFile(ctx, session, rec) {
844
+ const fs = ctx.get("fs");
845
+ if (!fs) return "缺少 fs";
846
+ if (rec.before === null || rec.before === void 0) {
847
+ if (rec.create === true) {
848
+ const out = await deleteCreated(ctx, session, rec);
849
+ return out.ok ? null : out.error;
850
+ }
851
+ return "缺少修改前内容(大文件或旧记录),无法回滚";
852
+ }
853
+ try {
854
+ const target = await resolveTarget(ctx, session, rec.path);
855
+ await fs.writeText(target, rec.before, void 0, void 0, policyOf(ctx, session));
856
+ return null;
857
+ } catch (error) {
858
+ return "回滚失败:" + String(error);
859
+ }
860
+ }
861
+ //#endregion
862
+ //#region src/rpc.ts
863
+ /** cwd → Promise 链:串行化 debug 日志追加(fs read+write 非原子,避免并发丢行)。 */
864
+ const debugWriteQueues = /* @__PURE__ */ new Map();
865
+ /** 记录 → 客户端视图(不含 before 全文,仅长度)。 */
866
+ function recView(record) {
867
+ return {
868
+ callId: record.callId,
869
+ toolName: record.toolName,
870
+ path: record.path,
871
+ beforeLen: typeof record.before === "string" ? record.before.length : 0,
872
+ create: record.create === true,
873
+ callHunk: record.callHunk,
874
+ hunks: record.hunks,
875
+ decisions: record.decisions,
876
+ note: record.note ?? null,
877
+ superseded: record.superseded === true,
878
+ at: record.at
879
+ };
880
+ }
881
+ /** 会话/工作区公共前置:返回 {session,cwd} 或错误文案。 */
882
+ async function requireSession(ctx, sessionId) {
883
+ const session = sessionOf(ctx, sessionId);
884
+ if (!session) return { err: "会话不存在" };
885
+ const cwd = cwdOf(session);
886
+ if (!cwd) return { err: "会话无工作区" };
887
+ return {
888
+ session,
889
+ cwd
890
+ };
891
+ }
892
+ /** 各方法 handler 表(类型由 shared/rpc 的 RpcHandlerMap 约束)。 */
893
+ function buildHandlers(ctx, registry) {
894
+ return {
895
+ "edrv.list": async (args) => {
896
+ const sc = await requireSession(ctx, args.sessionId);
897
+ if ("err" in sc) return {
898
+ ok: false,
899
+ error: sc.err
900
+ };
901
+ const bucket = await bucketOf(registry, ctx, sc.cwd);
902
+ const want = Array.isArray(args.callIds) ? new Set(args.callIds) : null;
903
+ if (!want) await autoArchiveStale(ctx, sc.session, sc.cwd, bucket);
904
+ const out = [];
905
+ for (const rec of bucket.values()) {
906
+ if (!want && rec.archived) continue;
907
+ if (want && !want.has(rec.callId)) continue;
908
+ out.push(recView(rec));
909
+ }
910
+ out.sort((a, b) => a.at < b.at ? -1 : 1);
911
+ return {
912
+ ok: true,
913
+ records: out
914
+ };
915
+ },
916
+ "edrv.accept": async (args) => {
917
+ const sc = await requireSession(ctx, args.sessionId);
918
+ if ("err" in sc) return {
919
+ ok: false,
920
+ error: sc.err
921
+ };
922
+ const bucket = await bucketOf(registry, ctx, sc.cwd);
923
+ const record = bucket.get(args.callId);
924
+ if (!record) return {
925
+ ok: false,
926
+ error: "记录不存在"
927
+ };
928
+ markDecision(record, args.scope ?? "call", args.hunkIndex, "accepted");
929
+ if (recordResolved(record)) await archiveRecords(ctx, sc.session, sc.cwd, bucket, [record], "已处理");
930
+ else await saveBucket(ctx, sc.cwd, bucket, sc.session);
931
+ return {
932
+ ok: true,
933
+ record: recView(record)
934
+ };
935
+ },
936
+ "edrv.reject": async (args) => {
937
+ const sc = await requireSession(ctx, args.sessionId);
938
+ if ("err" in sc) return {
939
+ ok: false,
940
+ error: sc.err
941
+ };
942
+ const bucket = await bucketOf(registry, ctx, sc.cwd);
943
+ const record = bucket.get(args.callId);
944
+ if (!record) return {
945
+ ok: false,
946
+ error: "记录不存在"
947
+ };
948
+ const scope = args.scope ?? "call";
949
+ const outcome = scope === "hunk" ? await revertHunk(ctx, sc.session, record, args.hunkIndex ?? -1) : await revertCall(ctx, sc.session, record);
950
+ if (!outcome.ok) return {
951
+ ok: false,
952
+ error: outcome.error
953
+ };
954
+ markDecision(record, scope, args.hunkIndex, "rejected");
955
+ record.at = (/* @__PURE__ */ new Date()).toISOString();
956
+ if (recordResolved(record)) await archiveRecords(ctx, sc.session, sc.cwd, bucket, [record], "已处理(回滚)");
957
+ else await saveBucket(ctx, sc.cwd, bucket, sc.session);
958
+ return {
959
+ ok: true,
960
+ record: recView(record)
961
+ };
962
+ },
963
+ "edrv.read": async (args) => {
964
+ const sc = await requireSession(ctx, args.sessionId);
965
+ if ("err" in sc) return {
966
+ ok: false,
967
+ error: sc.err
968
+ };
969
+ const fs = ctx.get("fs");
970
+ if (!fs) return {
971
+ ok: false,
972
+ error: "缺少 fs"
973
+ };
974
+ try {
975
+ const target = await resolveTarget(ctx, sc.session, args.path);
976
+ const info = await fs.stat(target);
977
+ if (!info || info.type !== "file") return {
978
+ ok: false,
979
+ error: "文件不存在"
980
+ };
981
+ if ((info.size ?? 0) > 8388608) return {
982
+ ok: false,
983
+ error: "文件过大(>8MB),不支持整文件预览"
984
+ };
985
+ const content = await fs.readText(target);
986
+ return {
987
+ ok: true,
988
+ content,
989
+ size: content.length
990
+ };
991
+ } catch (error) {
992
+ return {
993
+ ok: false,
994
+ error: "读取失败:" + String(error)
995
+ };
996
+ }
997
+ },
998
+ "edrv.original": async (args) => {
999
+ const sc = await requireSession(ctx, args.sessionId);
1000
+ if ("err" in sc) return {
1001
+ ok: false,
1002
+ error: sc.err
1003
+ };
1004
+ const fs = ctx.get("fs");
1005
+ if (!fs) return {
1006
+ ok: false,
1007
+ error: "缺少 fs"
1008
+ };
1009
+ try {
1010
+ const target = await resolveTarget(ctx, sc.session, args.path);
1011
+ const info = await fs.stat(target);
1012
+ if (!info || info.type !== "file") return {
1013
+ ok: false,
1014
+ error: "文件不存在"
1015
+ };
1016
+ if ((info.size ?? 0) > 8388608) return {
1017
+ ok: false,
1018
+ error: "文件过大(>8MB),不支持差异重建"
1019
+ };
1020
+ const content = await fs.readText(target);
1021
+ const records = [...(await bucketOf(registry, ctx, sc.cwd)).values()].filter((r) => r.path === args.path && !r.archived).sort((a, b) => a.at < b.at ? -1 : 1);
1022
+ if (!records.length) return {
1023
+ ok: true,
1024
+ content,
1025
+ size: content.length,
1026
+ stale: [],
1027
+ fallback: false
1028
+ };
1029
+ const rebuilt = reconstructOriginal(records, content);
1030
+ if (rebuilt.stale.length && records[0].before !== null) return {
1031
+ ok: true,
1032
+ content: records[0].before,
1033
+ size: records[0].before.length,
1034
+ stale: rebuilt.stale,
1035
+ fallback: true
1036
+ };
1037
+ return {
1038
+ ok: true,
1039
+ content: rebuilt.content,
1040
+ size: rebuilt.content.length,
1041
+ stale: rebuilt.stale,
1042
+ fallback: false
1043
+ };
1044
+ } catch (error) {
1045
+ return {
1046
+ ok: false,
1047
+ error: "重建失败:" + String(error)
1048
+ };
1049
+ }
1050
+ },
1051
+ "edrv.save": async (args) => {
1052
+ const sc = await requireSession(ctx, args.sessionId);
1053
+ if ("err" in sc) return {
1054
+ ok: false,
1055
+ error: sc.err
1056
+ };
1057
+ const fs = ctx.get("fs");
1058
+ if (!fs) return {
1059
+ ok: false,
1060
+ error: "缺少 fs"
1061
+ };
1062
+ try {
1063
+ const target = await resolveTarget(ctx, sc.session, args.path);
1064
+ await fs.writeText(target, args.content, void 0, void 0, policyOf(ctx, sc.session));
1065
+ const bucket = await bucketOf(registry, ctx, sc.cwd);
1066
+ let changed = false;
1067
+ for (const rec of bucket.values()) if (rec.path === args.path && rec.superseded !== true) {
1068
+ rec.superseded = true;
1069
+ rec.at = (/* @__PURE__ */ new Date()).toISOString();
1070
+ changed = true;
1071
+ }
1072
+ if (changed) {
1073
+ const done = [];
1074
+ for (const rec of bucket.values()) if (rec.path === args.path && rec.superseded) done.push(rec);
1075
+ if (done.length) await archiveRecords(ctx, sc.session, sc.cwd, bucket, done, "被手动编辑覆盖");
1076
+ else await saveBucket(ctx, sc.cwd, bucket, sc.session);
1077
+ }
1078
+ return { ok: true };
1079
+ } catch (error) {
1080
+ return {
1081
+ ok: false,
1082
+ error: "保存失败:" + String(error)
1083
+ };
1084
+ }
1085
+ },
1086
+ "edrv.archiveList": async (args) => {
1087
+ const sc = await requireSession(ctx, args.sessionId);
1088
+ if ("err" in sc) return {
1089
+ ok: false,
1090
+ error: sc.err
1091
+ };
1092
+ const entries = parseArchive(await readArchiveText(ctx, sc.cwd)).filter((b) => b.cwd === sc.cwd).map((b) => {
1093
+ const recs = Array.isArray(b.records) ? b.records : [];
1094
+ const sum = recs.reduce((s, r) => {
1095
+ const sm = r.summary || {
1096
+ accepted: 0,
1097
+ rejected: 0,
1098
+ pending: 0,
1099
+ superseded: false
1100
+ };
1101
+ s.accepted += sm.accepted || 0;
1102
+ s.rejected += sm.rejected || 0;
1103
+ s.pending += sm.pending || 0;
1104
+ if (sm.superseded) s.superseded++;
1105
+ return s;
1106
+ }, {
1107
+ accepted: 0,
1108
+ rejected: 0,
1109
+ pending: 0,
1110
+ superseded: 0
1111
+ });
1112
+ return {
1113
+ at: b.at,
1114
+ lastAt: b.lastAt || b.at,
1115
+ path: b.path,
1116
+ batch: b.batch ?? null,
1117
+ reason: b.reason ?? null,
1118
+ nRecords: recs.length,
1119
+ summary: sum
1120
+ };
1121
+ });
1122
+ entries.sort((a, b) => Number(b.batch ?? -1) - Number(a.batch ?? -1) || (a.at < b.at ? 1 : -1));
1123
+ return {
1124
+ ok: true,
1125
+ entries
1126
+ };
1127
+ },
1128
+ "edrv.archiveRead": async (args) => {
1129
+ const sc = await requireSession(ctx, args.sessionId);
1130
+ if ("err" in sc) return {
1131
+ ok: false,
1132
+ error: sc.err
1133
+ };
1134
+ const batchPath = args.path;
1135
+ return {
1136
+ ok: true,
1137
+ batches: parseArchive(await readArchiveText(ctx, sc.cwd)).filter((b) => b.cwd === sc.cwd && (!batchPath || b.path === batchPath))
1138
+ };
1139
+ },
1140
+ "edrv.rollback": async (args) => {
1141
+ const sc = await requireSession(ctx, args.sessionId);
1142
+ if ("err" in sc) return {
1143
+ ok: false,
1144
+ error: sc.err
1145
+ };
1146
+ if (!ctx.get("fs")) return {
1147
+ ok: false,
1148
+ error: "缺少 fs"
1149
+ };
1150
+ const batch = args.batch;
1151
+ const bucket = await bucketOf(registry, ctx, sc.cwd);
1152
+ let affected = [];
1153
+ let beforeRec = null;
1154
+ if (batch !== void 0) {
1155
+ const all = parseArchive(await readArchiveText(ctx, sc.cwd)).filter((b) => b.cwd === sc.cwd && b.path === args.path);
1156
+ const recs = [];
1157
+ for (const b of all) if (b.batch === batch) for (const r of b.records || []) recs.push(r);
1158
+ if (!recs.length) return {
1159
+ ok: false,
1160
+ error: "归档中找不到该批次"
1161
+ };
1162
+ recs.sort((a, b2) => a.at < b2.at ? -1 : 1);
1163
+ beforeRec = recs[0];
1164
+ affected = recs;
1165
+ } else {
1166
+ for (const r of bucket.values()) if (r.path === args.path && !r.archived) affected.push(r);
1167
+ if (!affected.length) return {
1168
+ ok: false,
1169
+ error: "该文件没有可回滚的差异"
1170
+ };
1171
+ affected.sort((a, b2) => a.at < b2.at ? -1 : 1);
1172
+ beforeRec = affected[0];
1173
+ }
1174
+ const rerr = await restoreFile(ctx, sc.session, beforeRec);
1175
+ if (rerr) return {
1176
+ ok: false,
1177
+ error: rerr
1178
+ };
1179
+ if (batch === void 0) await archiveRecords(ctx, sc.session, sc.cwd, bucket, affected, "已回滚");
1180
+ else {
1181
+ const sumRecs = parseArchive(await readArchiveText(ctx, sc.cwd)).filter((b) => b.cwd === sc.cwd && b.path === args.path && b.batch === batch).reduce((s, b) => s + (b.records || []).length, 0);
1182
+ const logRec = Object.assign({}, affected[0], { note: (affected[0].note ? affected[0].note + ";" : "") + "回滚至本批次前(批次 " + batch + ")" });
1183
+ await appendArchiveEntries(ctx, sc.cwd, [archiveEntryFor([logRec], sc.cwd, "已回滚(批次 " + batch + "," + sumRecs + " 条)")], sc.session);
1184
+ const activeRecs = [];
1185
+ for (const r of bucket.values()) if (r.path === args.path && !r.archived) activeRecs.push(r);
1186
+ if (activeRecs.length) await archiveRecords(ctx, sc.session, sc.cwd, bucket, activeRecs, "已回滚(批次回滚覆盖)");
1187
+ }
1188
+ return {
1189
+ ok: true,
1190
+ path: args.path,
1191
+ batch: batch ?? null
1192
+ };
1193
+ },
1194
+ "edrv.debug": async (args) => {
1195
+ const sc = await requireSession(ctx, args.sessionId);
1196
+ if ("err" in sc) return {
1197
+ ok: false,
1198
+ error: sc.err
1199
+ };
1200
+ const fs = ctx.get("fs");
1201
+ const text = String(args.text ?? "");
1202
+ const task = (debugWriteQueues.get(sc.cwd) || Promise.resolve()).then(async () => {
1203
+ try {
1204
+ const target = await fs.resolve(".dsh-edit-review-debug.log", { cwd: sc.cwd });
1205
+ const old = await fs.readText(target).catch(() => "");
1206
+ const line = (/* @__PURE__ */ new Date()).toISOString() + " " + text + "\n";
1207
+ await fs.writeText(target, (old || "") + line, void 0, void 0, policyOf(ctx, sc.session));
1208
+ } catch (e) {}
1209
+ });
1210
+ debugWriteQueues.set(sc.cwd, task);
1211
+ await task;
1212
+ console.error("[edrv-debug] " + text);
1213
+ return { ok: true };
1214
+ },
1215
+ "edrv.searchFiles": async (args) => {
1216
+ const sc = await requireSession(ctx, args.sessionId);
1217
+ if ("err" in sc) return {
1218
+ ok: false,
1219
+ error: sc.err
1220
+ };
1221
+ const query = String(args.query ?? "").trim().toLowerCase();
1222
+ const files = [];
1223
+ if (query.length >= 2) {
1224
+ const bucket = await bucketOf(registry, ctx, sc.cwd);
1225
+ const pool = /* @__PURE__ */ new Set();
1226
+ for (const rec of bucket.values()) if (rec.path) pool.add(rec.path);
1227
+ const listed = await listWorkspaceFiles(ctx, sc.session, sc.cwd);
1228
+ if (listed) for (const f of listed) pool.add(f);
1229
+ const score = (p) => {
1230
+ const pl = p.toLowerCase();
1231
+ if ((pl.split(/[\\/]/).pop() || "").includes(query)) return 0;
1232
+ if (pl.includes(query)) return 1;
1233
+ return -1;
1234
+ };
1235
+ const hit = [...pool].filter((p) => score(p) >= 0);
1236
+ hit.sort((a, b) => score(a) - score(b) || (a < b ? -1 : 1));
1237
+ files.push(...hit.slice(0, 50));
1238
+ }
1239
+ return {
1240
+ ok: true,
1241
+ files,
1242
+ truncated: files.length >= 50
1243
+ };
1244
+ }
1245
+ };
1246
+ }
1247
+ /**
1248
+ * 统一入口:按方法分发到 handler 表。
1249
+ * @author ddj 2026年08月20号
1250
+ */
1251
+ async function handleRpc(ctx, registry, method, args) {
1252
+ const handler = buildHandlers(ctx, registry)[method];
1253
+ if (!handler) return {
1254
+ ok: false,
1255
+ error: "未知方法: " + String(method)
1256
+ };
1257
+ return handler(args);
1258
+ }
1259
+ //#endregion
1260
+ //#region src/index.ts
1261
+ /**
1262
+ * @dsh-external 生态 → dsh-vscode-mode:DSH 上的类 VSCode 编码体验(Host 半入口)。
1263
+ * 职责:装配 capture(tools/result 捕获 edit/write 差异)、RPC 分发(11 个 edrv.* 方法)、
1264
+ * webServer 路由(/edrv/rpc、/edrv/assets/*、/edrv/vendor/*)、工作区旁车持久化。
1265
+ * 结构:shared/(双面契约) + model/store/capture/workspace/revert/registry/rpc/routes(host 模块)。
1266
+ * 作者 ddj 2026-08-20
1267
+ */
1268
+ const name = "dsh-vscode-mode";
1269
+ const inject = [
1270
+ "sessions",
1271
+ "fs",
1272
+ "webServer"
1273
+ ];
1274
+ /**
1275
+ * 装配插件:挂事件监听、注册路由。
1276
+ * @author ddj 2026年08月20号
1277
+ * @param ctx DSH 上下文(sessions/fs/webServer 由 inject 提供;sandboxPolicy/subprocess 惰性获取)
1278
+ * @param config 插件配置(可选 imageDir 覆盖图标目录)
1279
+ */
1280
+ function apply(ctx, config) {
1281
+ const registry = /* @__PURE__ */ new Map();
1282
+ ctx.on("tools/result", (exec, result) => {
1283
+ captureToolResult(ctx, registry, exec, result);
1284
+ });
1285
+ ctx.on("session/disposed", (session) => {
1286
+ const cwd = cwdOf(session);
1287
+ if (cwd) {
1288
+ registry.delete(cwd);
1289
+ dropFileIndex(cwd);
1290
+ }
1291
+ });
1292
+ registerRoutes(ctx, config, (method, args) => handleRpc(ctx, registry, method, args));
1293
+ ctx.logger?.info?.("[dsh-vscode-mode] 编辑差异审查已装配(/edrv/rpc 路由就绪)");
1294
+ }
1295
+ //#endregion
1296
+ export { apply, inject, name };
1297
+
1298
+ //# sourceMappingURL=index.js.map