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
@@ -0,0 +1,28 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * dsh-vscode-mode client — UI 共用小工具(按钮样式/状态文案/等宽字体)。
4
+ * 迁移自原 src/client/index.ts 的 btn/badgeOf/MONO,语义不改。
5
+ * 作者 ddj 2026-08-20
6
+ */
7
+ import { ST } from '../state/records.js'
8
+
9
+ /** 次级按钮内联样式(accept=true 绿色 / false 红色)。 */
10
+ export function btn(accept) {
11
+ return {
12
+ fontSize: 11,
13
+ padding: '1px 8px',
14
+ border: '1px solid var(--dsw-alias-border-l2,#555)',
15
+ borderRadius: 4,
16
+ cursor: 'pointer',
17
+ background: 'var(--dsw-alias-bg-layer-1,transparent)',
18
+ color: accept ? 'var(--dsw-alias-state-success-primary,#2e9e44)' : 'var(--dsw-alias-state-error-primary,#d9534f)',
19
+ }
20
+ }
21
+
22
+ /** 状态中文文案。 */
23
+ export function badgeOf(status) {
24
+ return status === ST.ACCEPTED ? '已采纳' : status === ST.REJECTED ? '已拒绝' : '待处理'
25
+ }
26
+
27
+ /** 等宽字体常量(差异行渲染用)。 */
28
+ export const MONO = { fontFamily: 'var(--ds-font-family-code, ui-monospace, monospace)', fontSize: 12, lineHeight: 1.55 }
package/src/index.ts ADDED
@@ -0,0 +1,43 @@
1
+ /**
2
+ * @dsh-external 生态 → dsh-vscode-mode:DSH 上的类 VSCode 编码体验(Host 半入口)。
3
+ * 职责:装配 capture(tools/result 捕获 edit/write 差异)、RPC 分发(11 个 edrv.* 方法)、
4
+ * webServer 路由(/edrv/rpc、/edrv/assets/*、/edrv/vendor/*)、工作区旁车持久化。
5
+ * 结构:shared/(双面契约) + model/store/capture/workspace/revert/registry/rpc/routes(host 模块)。
6
+ * 作者 ddj 2026-08-20
7
+ */
8
+ import { registerRoutes } from './routes.js'
9
+ import { captureToolResult } from './capture.js'
10
+ import { handleRpc } from './rpc.js'
11
+ import { dropFileIndex } from './workspace.js'
12
+ import { cwdOf } from './registry.js'
13
+ import type { Registry } from './registry.js'
14
+ import type { Ctx } from './store.js'
15
+
16
+ export const name = "dsh-vscode-mode"
17
+ export const inject = ['sessions', 'fs', 'webServer']
18
+
19
+ /**
20
+ * 装配插件:挂事件监听、注册路由。
21
+ * @author ddj 2026年08月20号
22
+ * @param ctx DSH 上下文(sessions/fs/webServer 由 inject 提供;sandboxPolicy/subprocess 惰性获取)
23
+ * @param config 插件配置(可选 imageDir 覆盖图标目录)
24
+ */
25
+ export function apply(ctx: Ctx, config?: unknown): void {
26
+ const registry: Registry = new Map()
27
+
28
+ ctx.on('tools/result', (exec: unknown, result: unknown) => {
29
+ void captureToolResult(ctx, registry, exec, result)
30
+ })
31
+
32
+ ctx.on('session/disposed', (session: unknown) => {
33
+ const cwd = cwdOf(session as never)
34
+ if (cwd) {
35
+ registry.delete(cwd)
36
+ dropFileIndex(cwd)
37
+ }
38
+ })
39
+
40
+ registerRoutes(ctx, config, (method, args) => handleRpc(ctx, registry, method, args))
41
+
42
+ ctx.logger?.info?.('[' + name + '] 编辑差异审查已装配(/edrv/rpc 路由就绪)')
43
+ }
package/src/model.ts ADDED
@@ -0,0 +1,197 @@
1
+ /**
2
+ * dsh-vscode-mode host — 纯域逻辑(无 ctx/fs/IO 依赖,可直接单测)。
3
+ * 迁移自原 src/index.ts 的纯函数部分,语义一字不改。
4
+ * 作者 ddj 2026-08-20
5
+ */
6
+ import type { Decision, DiffRecord, Hunk, RecordSummary } from './shared/types.js'
7
+
8
+ /** 工作区记录桶上限(超过后按 at 最旧剔除)。 */
9
+ export const MAX_RECORDS = 200
10
+
11
+ /**
12
+ * 归一化一条持久化记录(容忍旧版/损坏字段)。
13
+ * @author ddj 2026年08月20号
14
+ * @param raw 反序列化的原始记录
15
+ * @returns 合法记录或 null
16
+ */
17
+ export function normalizeRecord(raw: unknown): DiffRecord | null {
18
+ if (!raw || typeof raw !== 'object') return null
19
+ const r = raw as Record<string, unknown>
20
+ if (typeof r.callId !== 'string' || typeof r.path !== 'string') return null
21
+ const hunks: Hunk[] = Array.isArray(r.hunks)
22
+ ? (r.hunks as unknown[]).filter((h) => h && typeof (h as { newText?: unknown }).newText === 'string') as Hunk[]
23
+ : []
24
+ const toolName: DiffRecord['toolName'] = r.toolName === 'write' ? 'write' : 'edit'
25
+ return {
26
+ callId: r.callId,
27
+ toolName,
28
+ path: r.path,
29
+ before: typeof r.before === 'string' ? r.before : null,
30
+ create: r.create === true,
31
+ callHunk: r.callHunk && typeof (r.callHunk as { oldText?: unknown }).oldText === 'string'
32
+ ? { oldText: (r.callHunk as { oldText: string }).oldText, newText: (r.callHunk as { newText: string }).newText }
33
+ : null,
34
+ hunks: hunks.map((h) => ({ oldText: typeof h.oldText === 'string' ? h.oldText : null, newText: h.newText })),
35
+ decisions: r.decisions && typeof r.decisions === 'object'
36
+ ? r.decisions as DiffRecord['decisions']
37
+ : { call: 'pending', perHunk: hunks.map(() => 'pending' as Decision) },
38
+ note: typeof r.note === 'string' ? r.note : null,
39
+ superseded: r.superseded === true,
40
+ archived: r.archived === true,
41
+ batch: Number.isInteger(r.batch) ? (r.batch as number) : 0,
42
+ at: typeof r.at === 'string' ? r.at : new Date().toISOString(),
43
+ }
44
+ }
45
+
46
+ /**
47
+ * 写决策到记录(scope=call 整体 / hunk 单个块)。
48
+ * @author ddj 2026年08月20号
49
+ * @param record 记录
50
+ * @param scope 作用域
51
+ * @param idx hunk 下标(仅 scope=hunk 时生效)
52
+ * @param value 决策值
53
+ * @returns 更新后的记录(原对象就地修改)
54
+ */
55
+ export function markDecision(record: DiffRecord, scope: string, idx: number | undefined, value: Decision): DiffRecord {
56
+ const dec = record.decisions
57
+ if (scope === 'hunk' && Array.isArray(dec.perHunk) && Number.isInteger(idx)) {
58
+ const i = idx as number
59
+ if (i >= 0 && i < dec.perHunk.length) dec.perHunk[i] = value
60
+ } else {
61
+ dec.call = value
62
+ if (Array.isArray(dec.perHunk)) dec.perHunk = dec.perHunk.map(() => value)
63
+ }
64
+ record.at = new Date().toISOString()
65
+ return record
66
+ }
67
+
68
+ /** 记录是否已无任何待决策差异(superseded 或全部已决策)。 */
69
+ export function recordResolved(record: DiffRecord): boolean {
70
+ if (record.superseded === true) return true
71
+ const perHunk = Array.isArray(record.decisions.perHunk) ? record.decisions.perHunk : []
72
+ if (perHunk.length) return perHunk.every((v) => v === 'accepted' || v === 'rejected')
73
+ return record.decisions.call === 'accepted' || record.decisions.call === 'rejected'
74
+ }
75
+
76
+ /**
77
+ * 记录决策摘要(采纳/拒绝/待处理计数,superseded 标记)。
78
+ * @author ddj 2026年08月20号
79
+ * @param record 记录
80
+ * @returns 摘要
81
+ */
82
+ export function recSummary(record: DiffRecord): RecordSummary {
83
+ const perHunk = Array.isArray(record.decisions.perHunk) ? record.decisions.perHunk : []
84
+ const n = Math.max(1, Array.isArray(record.hunks) ? record.hunks.length : 1)
85
+ let accepted = 0
86
+ let rejected = 0
87
+ let pending = 0
88
+ if (perHunk.length) {
89
+ for (const v of perHunk) {
90
+ if (v === 'accepted') accepted++
91
+ else if (v === 'rejected') rejected++
92
+ else pending++
93
+ }
94
+ } else if (record.decisions.call === 'accepted') accepted = n
95
+ else if (record.decisions.call === 'rejected') rejected = n
96
+ else pending = n
97
+ return { accepted, rejected, pending, superseded: record.superseded === true }
98
+ }
99
+
100
+ /**
101
+ * 重建"本批次修改前"内容:把仍待处理(pending)的差异块按 新→旧 顺序从当前内容反解
102
+ * (newText → oldText)。已采纳/已拒绝的块跳过。反解失败的块标记为 stale。
103
+ * @author ddj 2026年08月20号
104
+ * @param records 该文件的活动记录
105
+ * @param content 当前磁盘内容
106
+ * @returns 重建内容与 stale 定位
107
+ */
108
+ export function reconstructOriginal(records: DiffRecord[], content: string): { content: string; stale: Array<{ callId: string; idx: number }> } {
109
+ const pending: Array<{ rec: DiffRecord; idx: number }> = []
110
+ for (const rec of records) {
111
+ const perHunk = Array.isArray(rec.decisions.perHunk) ? rec.decisions.perHunk : []
112
+ for (let i = 0; i < rec.hunks.length; i++) {
113
+ const st = perHunk.length ? perHunk[i] : rec.decisions.call
114
+ if (st === 'pending') pending.push({ rec, idx: i })
115
+ }
116
+ }
117
+ pending.sort((a, b) => (a.rec.at < b.rec.at ? 1 : a.rec.at > b.rec.at ? -1 : 0))
118
+ let out = content
119
+ const stale: Array<{ callId: string; idx: number }> = []
120
+ for (const p of pending) {
121
+ const h = p.rec.hunks[p.idx]
122
+ const precise = p.rec.toolName === 'edit' && p.rec.hunks.length <= 1 && p.rec.callHunk ? p.rec.callHunk : h
123
+ const newText = precise.newText ?? ''
124
+ const at = out.indexOf(newText)
125
+ if (at < 0) { stale.push({ callId: p.rec.callId, idx: p.idx }); continue }
126
+ const oldText = precise.oldText === null ? '' : precise.oldText
127
+ out = out.slice(0, at) + oldText + out.slice(at + newText.length)
128
+ }
129
+ return { content: out, stale }
130
+ }
131
+
132
+ /** 文件当前最大批次号(无记录为 0)。 */
133
+ export function fileMaxBatch(records: Map<string, DiffRecord>, path: string): number {
134
+ let m = 0
135
+ for (const r of records.values()) {
136
+ if (r.path === path && Number.isInteger(r.batch) && r.batch > m) m = r.batch
137
+ }
138
+ return m
139
+ }
140
+
141
+ /** 记录桶裁剪:超出 MAX_RECORDS 时剔除最旧的。 */
142
+ export function prune(map: Map<string, DiffRecord>, max = MAX_RECORDS): void {
143
+ if (map.size <= max) return
144
+ const sorted = [...map.values()].sort((a, b) => (a.at < b.at ? -1 : 1))
145
+ for (let i = 0; i < sorted.length - max; i++) map.delete(sorted[i].callId)
146
+ }
147
+
148
+ /** 按批次分组记录。 */
149
+ export function groupByBatch(recs: DiffRecord[]): Map<number | null, DiffRecord[]> {
150
+ const map = new Map<number | null, DiffRecord[]>()
151
+ for (const r of recs) {
152
+ const k = Number.isInteger(r.batch) ? r.batch : null
153
+ if (!map.has(k)) map.set(k, [])
154
+ map.get(k)!.push(r)
155
+ }
156
+ return map
157
+ }
158
+
159
+ /**
160
+ * 归档条目构造(一条批次一条)。
161
+ * @author ddj 2026年08月20号
162
+ * @param recs 同批次记录(非空)
163
+ * @param cwd 工作区
164
+ * @param reason 归档原因
165
+ */
166
+ export function archiveEntryFor(recs: DiffRecord[], cwd: string, reason: string): {
167
+ at: string
168
+ cwd: string
169
+ path: string
170
+ batch: number | null
171
+ reason: string
172
+ records: Array<Record<string, unknown>>
173
+ } {
174
+ const first = recs[0]
175
+ return {
176
+ at: new Date().toISOString(),
177
+ cwd,
178
+ path: first.path,
179
+ batch: Number.isInteger(first.batch) ? first.batch : null,
180
+ reason,
181
+ records: recs.map((r) => ({
182
+ callId: r.callId,
183
+ toolName: r.toolName,
184
+ path: r.path,
185
+ create: r.create === true,
186
+ callHunk: r.callHunk,
187
+ hunks: r.hunks,
188
+ decisions: r.decisions,
189
+ note: r.note ?? null,
190
+ before: typeof r.before === 'string' ? r.before : null,
191
+ superseded: r.superseded === true,
192
+ batch: Number.isInteger(r.batch) ? r.batch : null,
193
+ at: r.at,
194
+ summary: recSummary(r),
195
+ })),
196
+ }
197
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * dsh-vscode-mode host — 工作区记录桶注册表与会话助手。
3
+ * 迁移自原 src/index.ts 的 registry/sessionOf/cwdOf/bucketOf,语义不改。
4
+ * 作者 ddj 2026-08-20
5
+ */
6
+ import type { DiffRecord } from './shared/types.js'
7
+ import type { Ctx, Session } from './store.js'
8
+ import { loadBucket } from './store.js'
9
+
10
+ /** cwd → 记录桶(内存缓存;随会话销毁清理)。 */
11
+ export type Registry = Map<string, Map<string, DiffRecord>>
12
+
13
+ /** 按 sessionId 取会话;缺省时仅当唯一活跃会话才返回。 */
14
+ export function sessionOf(ctx: Ctx, sessionId?: string): Session | undefined {
15
+ const sessions = ctx.get('sessions')
16
+ if (!sessions) return undefined
17
+ if (sessionId) return sessions.get(sessionId)
18
+ const live = sessions.list().filter((s: Session) => s.id)
19
+ return live.length === 1 ? live[0] : undefined
20
+ }
21
+
22
+ /** 会话工作区 cwd。 */
23
+ export function cwdOf(session: Session | undefined): string | null {
24
+ return session?.header?.cwd ?? null
25
+ }
26
+
27
+ /** 取(或加载)某工作区记录桶。 */
28
+ export function bucketOf(registry: Registry, ctx: Ctx, cwd: string): Promise<Map<string, DiffRecord>> {
29
+ const existing = registry.get(cwd)
30
+ if (existing) return Promise.resolve(existing)
31
+ return loadBucket(ctx, cwd).then((map) => {
32
+ registry.set(cwd, map)
33
+ return map
34
+ })
35
+ }
package/src/revert.ts ADDED
@@ -0,0 +1,107 @@
1
+ /**
2
+ * dsh-vscode-mode host — 回滚/删除(fs + subprocess 操作)。
3
+ * 迁移自原 src/index.ts 的 revertCall/revertHunk/deleteCreated/restoreFile,语义不改。
4
+ * 作者 ddj 2026-08-20
5
+ */
6
+ import type { DiffRecord } from './shared/types.js'
7
+ import type { Ctx, Session } from './store.js'
8
+ import { policyOf, resolveTarget } from './store.js'
9
+
10
+ export type Result = { ok: true } | { ok: false; error: string }
11
+
12
+ /**
13
+ * 删除新建文件(拒绝创建时):subprocess 删除,路径先经 fs.contains 校验工作区边界。
14
+ * @author ddj 2026年08月20号
15
+ * @returns 成功或失败原因
16
+ */
17
+ export async function deleteCreated(ctx: Ctx, session: Session, record: DiffRecord): Promise<Result> {
18
+ const sub = ctx.get('subprocess')
19
+ const fs = ctx.get('fs')
20
+ if (!sub || !fs) return { ok: false, error: '回滚不可用:缺少 subprocess/fs' }
21
+ const policy = policyOf(ctx, session)
22
+ const rootTarget = await fs.resolve(policy?.workspaceRoot ?? '.', {})
23
+ const target = await resolveTarget(ctx, session, record.path)
24
+ if (!fs.contains(rootTarget, target)) return { ok: false, error: '拒绝删除:目标不在会话工作区内' }
25
+ const p = fs.processPath(target)
26
+ // subprocess 契约:spawn({ argv, stdio, graceMs }),done → { exitCode },输出走 handle.collected
27
+ const attempt = async (argv: string[]): Promise<void> => {
28
+ const handle = sub.spawn({
29
+ argv,
30
+ stdio: { stdout: { maxBytes: 1 << 16 }, stderr: { maxBytes: 1 << 16 }, stdin: 'ignore' },
31
+ graceMs: 10000,
32
+ })
33
+ const outcome = await handle.done
34
+ const code = outcome?.exitCode ?? outcome?.code
35
+ if (code !== 0) {
36
+ const err = handle.collected?.stderr?.readFrom(0).text ?? ''
37
+ throw new Error('exit ' + code + ' ' + err)
38
+ }
39
+ }
40
+ try {
41
+ await attempt(['powershell', '-NoProfile', '-NonInteractive', '-Command', 'Remove-Item -LiteralPath "' + p + '" -Force'])
42
+ return { ok: true }
43
+ } catch (error) {
44
+ try {
45
+ await attempt(['/bin/rm', '-f', '--', p])
46
+ return { ok: true }
47
+ } catch (error2) {
48
+ return { ok: false, error: '删除失败(文件仍存在):' + String(error) + ' / ' + String(error2) }
49
+ }
50
+ }
51
+ }
52
+
53
+ /** 整调用回滚:write/编辑用 before 整文件恢复;新建文件转 deleteCreated。 */
54
+ export async function revertCall(ctx: Ctx, session: Session, record: DiffRecord): Promise<Result> {
55
+ const fs = ctx.get('fs')
56
+ if (!fs) return { ok: false, error: '回滚不可用:缺少 fs' }
57
+ if (record.before === null) {
58
+ if (!record.create) return { ok: false, error: '无法回滚:缺少修改前内容(大文件或旧记录)' }
59
+ return deleteCreated(ctx, session, record)
60
+ }
61
+ try {
62
+ const target = await resolveTarget(ctx, session, record.path)
63
+ await fs.writeText(target, record.before, void 0, void 0, policyOf(ctx, session))
64
+ return { ok: true }
65
+ } catch (error) {
66
+ return { ok: false, error: '回滚失败:' + String(error) }
67
+ }
68
+ }
69
+
70
+ /** 单 hunk 回滚:用 callHunk(edit 单 hunk 精确串)经 fs.editText 反替换。 */
71
+ export async function revertHunk(ctx: Ctx, session: Session, record: DiffRecord, idx: number): Promise<Result> {
72
+ const fs = ctx.get('fs')
73
+ if (!fs) return { ok: false, error: '回滚不可用:缺少 fs' }
74
+ const hunk = record.hunks[idx]
75
+ const precise = record.toolName === 'edit' && record.hunks.length <= 1 && record.callHunk ? record.callHunk : hunk
76
+ if (!precise) return { ok: false, error: '找不到该差异块' }
77
+ try {
78
+ const target = await resolveTarget(ctx, session, record.path)
79
+ await fs.editText(target, { oldString: precise.newText, newString: precise.oldText === null ? '' : precise.oldText }, void 0, void 0, policyOf(ctx, session))
80
+ return { ok: true }
81
+ } catch (error) {
82
+ return { ok: false, error: '回滚失败:该区域可能已被后续修改影响(' + String(error) + ')' }
83
+ }
84
+ }
85
+
86
+ /**
87
+ * 恢复文件到记录对应状态(批次回滚用):返回错误文案或 null(成功)。
88
+ * @author ddj 2026年08月20号
89
+ */
90
+ export async function restoreFile(ctx: Ctx, session: Session, rec: DiffRecord): Promise<string | null> {
91
+ const fs = ctx.get('fs')
92
+ if (!fs) return '缺少 fs'
93
+ if (rec.before === null || rec.before === undefined) {
94
+ if (rec.create === true) {
95
+ const out = await deleteCreated(ctx, session, rec)
96
+ return out.ok ? null : out.error
97
+ }
98
+ return '缺少修改前内容(大文件或旧记录),无法回滚'
99
+ }
100
+ try {
101
+ const target = await resolveTarget(ctx, session, rec.path)
102
+ await fs.writeText(target, rec.before, void 0, void 0, policyOf(ctx, session))
103
+ return null
104
+ } catch (error) {
105
+ return '回滚失败:' + String(error)
106
+ }
107
+ }
package/src/routes.ts ADDED
@@ -0,0 +1,138 @@
1
+ /**
2
+ * dsh-vscode-mode host — webServer 路由注册(/edrv/rpc、/edrv/assets/*、/edrv/vendor/*)。
3
+ * 迁移自原 src/index.ts 的路由部分,路径/行为一字不改。
4
+ * 作者 ddj 2026-08-20
5
+ */
6
+ import { readFile } from 'node:fs/promises'
7
+ import { dirname, extname, join, normalize, resolve, sep } from 'node:path'
8
+ import { fileURLToPath } from 'node:url'
9
+ import { RPC_PATH } from './shared/rpc.js'
10
+ import type { RpcMethod, RpcRequestMap } from './shared/rpc.js'
11
+ import type { Ctx } from './store.js'
12
+
13
+ const IMG_ROUTES = [
14
+ { url: '/edrv/assets/compare-idle.png', file: 'compare_idle.png' },
15
+ { url: '/edrv/assets/compare-select.png', file: 'compare_select.png' },
16
+ ]
17
+
18
+ /** Monaco AMD 构建静态资源(随包发布,离线可用;前缀路由 /edrv/vendor/*)。 */
19
+ const VENDOR_PREFIX = '/edrv/vendor'
20
+ const VENDOR_MIME: Record<string, string> = {
21
+ '.js': 'text/javascript; charset=utf-8',
22
+ '.css': 'text/css; charset=utf-8',
23
+ '.json': 'application/json',
24
+ '.map': 'application/json',
25
+ '.ts': 'text/plain; charset=utf-8',
26
+ '.svg': 'image/svg+xml',
27
+ '.png': 'image/png',
28
+ '.gif': 'image/gif',
29
+ '.ico': 'image/x-icon',
30
+ '.woff': 'font/woff',
31
+ '.woff2': 'font/woff2',
32
+ '.ttf': 'font/ttf',
33
+ '.html': 'text/html; charset=utf-8',
34
+ '.md': 'text/markdown',
35
+ }
36
+
37
+ /** 图标目录:默认插件包内 assets/(随包发布);config.imageDir 可覆盖(用于自定义换图)。 */
38
+ function imageDirOf(config: unknown): string {
39
+ const cfg = config as { imageDir?: unknown } | undefined
40
+ if (cfg && typeof cfg.imageDir === 'string' && cfg.imageDir) return cfg.imageDir.replace(/\\/g, '/')
41
+ return join(dirname(fileURLToPath(import.meta.url)), '..', 'assets')
42
+ }
43
+
44
+ /** Monaco AMD 静态目录(随包发布)。 */
45
+ const vendorDir = join(dirname(fileURLToPath(import.meta.url)), '..', 'assets', 'vendor')
46
+
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
+ export function registerRoutes(
55
+ ctx: Ctx,
56
+ config: unknown,
57
+ handleRpc: <M extends RpcMethod>(method: M, args: RpcRequestMap[M]) => Promise<unknown>,
58
+ ): void {
59
+ const imgDir = imageDirOf(config)
60
+ const web = ctx.get('webServer')
61
+ if (!web) return
62
+
63
+ ctx.effect(() => web.register({
64
+ kind: 'exact',
65
+ path: RPC_PATH,
66
+ handler: async (req: unknown, res: { statusCode?: number; setHeader: (k: string, v: string) => void; end: (b?: string) => void }) => {
67
+ try {
68
+ const chunks: Uint8Array[] = []
69
+ for await (const chunk of req as AsyncIterable<Uint8Array>) chunks.push(chunk)
70
+ let input: { method?: string; args?: unknown } = {}
71
+ try {
72
+ input = JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}')
73
+ } catch (error) {
74
+ input = {}
75
+ }
76
+ const method = (typeof input.method === 'string' ? input.method : '') as RpcMethod
77
+ const result = await handleRpc(method, (input.args ?? {}) as never)
78
+ res.statusCode = 200
79
+ res.setHeader('content-type', 'application/json')
80
+ res.end(JSON.stringify(result))
81
+ } catch (error) {
82
+ res.statusCode = 500
83
+ res.end(JSON.stringify({ ok: false, error: String(error) }))
84
+ }
85
+ },
86
+ }), 'edrv: /edrv/rpc route')
87
+
88
+ for (const r of IMG_ROUTES) {
89
+ ctx.effect(() => web.register({
90
+ kind: 'exact',
91
+ path: r.url,
92
+ handler: async (req: unknown, res: { statusCode?: number; setHeader: (k: string, v: string) => void; end: (b?: string | Uint8Array) => void }) => {
93
+ try {
94
+ const body = await readFile(imgDir + '/' + r.file)
95
+ res.statusCode = 200
96
+ res.setHeader('content-type', 'image/png')
97
+ res.setHeader('cache-control', 'no-cache')
98
+ res.end(body)
99
+ } catch (error) {
100
+ res.statusCode = 404
101
+ res.end()
102
+ }
103
+ },
104
+ }), 'edrv: static ' + r.file)
105
+ }
106
+
107
+ // Monaco Editor AMD 构建:/edrv/vendor/* → assets/vendor/*(路径穿越防护 + 按扩展名 MIME)
108
+ ctx.effect(() => web.register({
109
+ kind: 'prefix',
110
+ path: VENDOR_PREFIX,
111
+ handler: async (req: { method?: string; url?: string }, res: { statusCode?: number; setHeader: (k: string, v: string) => void; end: (b?: string | Uint8Array) => void }) => {
112
+ try {
113
+ if (req.method !== 'GET' && req.method !== 'HEAD') {
114
+ res.statusCode = 405
115
+ res.end()
116
+ return
117
+ }
118
+ const pathname = new URL(req.url ?? '/', 'http://x').pathname
119
+ const rel = decodeURIComponent(pathname.slice(VENDOR_PREFIX.length)).replace(/^\/+/, '')
120
+ if (!rel) { res.statusCode = 404; res.end(); return }
121
+ const target = resolve(normalize(join(vendorDir, rel)))
122
+ if (target !== vendorDir && !target.startsWith(vendorDir + sep)) {
123
+ res.statusCode = 403
124
+ res.end()
125
+ return
126
+ }
127
+ const body = await readFile(target)
128
+ res.statusCode = 200
129
+ res.setHeader('content-type', VENDOR_MIME[extname(target)] ?? 'application/octet-stream')
130
+ res.setHeader('cache-control', 'public, max-age=3600')
131
+ res.end(body)
132
+ } catch (error) {
133
+ res.statusCode = 404
134
+ res.end()
135
+ }
136
+ },
137
+ }), 'edrv: /edrv/vendor prefix')
138
+ }