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.
- package/README.md +95 -0
- package/assets/compare_idle.png +0 -0
- package/assets/compare_select.png +0 -0
- package/assets/vendor/monaco/vs/abap-D-t0cyap.js +1 -0
- package/assets/vendor/monaco/vs/apex-CcIm7xu6.js +1 -0
- package/assets/vendor/monaco/vs/assets/css.worker-URu8fCFR.js +97 -0
- package/assets/vendor/monaco/vs/assets/editor.worker-lj3bdIIn.js +30 -0
- package/assets/vendor/monaco/vs/assets/editorWebWorkerMain-CA_vMoUU.js +30 -0
- package/assets/vendor/monaco/vs/assets/html.worker-D1SL3iM8.js +474 -0
- package/assets/vendor/monaco/vs/assets/json.worker-CoJx_OPf.js +62 -0
- package/assets/vendor/monaco/vs/assets/ts.worker-BWKtMYOk.js +67722 -0
- package/assets/vendor/monaco/vs/azcli-BA0tQDCg.js +1 -0
- package/assets/vendor/monaco/vs/basic-languages/monaco.contribution.js +1 -0
- package/assets/vendor/monaco/vs/bat-C397hTD6.js +1 -0
- package/assets/vendor/monaco/vs/bicep-DF5aW17k.js +2 -0
- package/assets/vendor/monaco/vs/cameligo-plsz8qhj.js +1 -0
- package/assets/vendor/monaco/vs/clojure-Y2auQMzK.js +1 -0
- package/assets/vendor/monaco/vs/coffee-Bu45yuWE.js +1 -0
- package/assets/vendor/monaco/vs/cpp-CkKPQIni.js +1 -0
- package/assets/vendor/monaco/vs/csharp-CX28MZyh.js +1 -0
- package/assets/vendor/monaco/vs/csp-D8uWnyxW.js +1 -0
- package/assets/vendor/monaco/vs/css-CaeNmE3S.js +3 -0
- package/assets/vendor/monaco/vs/css.worker-CyhWkhHo.js +1 -0
- package/assets/vendor/monaco/vs/cssMode-CV6Ay48H.js +1 -0
- package/assets/vendor/monaco/vs/cypher-DVThT8BS.js +1 -0
- package/assets/vendor/monaco/vs/dart-CmGfCvrO.js +1 -0
- package/assets/vendor/monaco/vs/dockerfile-CZqqYdch.js +1 -0
- package/assets/vendor/monaco/vs/ecl-30fUercY.js +1 -0
- package/assets/vendor/monaco/vs/editor/editor.main.css +1 -0
- package/assets/vendor/monaco/vs/editor/editor.main.js +1 -0
- package/assets/vendor/monaco/vs/editor/editor.worker.js +1 -0
- package/assets/vendor/monaco/vs/editor-KLE6jdfb.js +831 -0
- package/assets/vendor/monaco/vs/editor.js +1 -0
- package/assets/vendor/monaco/vs/editorWorkerHost-fVE1cjcC.js +35 -0
- package/assets/vendor/monaco/vs/elixir-xjPaIfzF.js +1 -0
- package/assets/vendor/monaco/vs/flow9-DqtmStfK.js +1 -0
- package/assets/vendor/monaco/vs/freemarker2-FWreY7v7.js +3 -0
- package/assets/vendor/monaco/vs/fsharp-BOMdg4U1.js +1 -0
- package/assets/vendor/monaco/vs/go-D_hbi-Jt.js +1 -0
- package/assets/vendor/monaco/vs/graphql-CKUU4kLG.js +1 -0
- package/assets/vendor/monaco/vs/handlebars-CZi4pHCH.js +1 -0
- package/assets/vendor/monaco/vs/hcl-DTaboeZW.js +1 -0
- package/assets/vendor/monaco/vs/html-j_7ZTNRU.js +1 -0
- package/assets/vendor/monaco/vs/html.worker-CA3iAimZ.js +1 -0
- package/assets/vendor/monaco/vs/htmlMode-27by_KM4.js +1 -0
- package/assets/vendor/monaco/vs/index-049xrXrp.js +1 -0
- package/assets/vendor/monaco/vs/index-CBVt3dzv.js +5 -0
- package/assets/vendor/monaco/vs/index.js +1 -0
- package/assets/vendor/monaco/vs/ini-CsNwO04R.js +1 -0
- package/assets/vendor/monaco/vs/initialize-DL0l1TGY.js +1 -0
- package/assets/vendor/monaco/vs/java-CI4ZMsH9.js +1 -0
- package/assets/vendor/monaco/vs/javascript-DTJl4Jn1.js +1 -0
- package/assets/vendor/monaco/vs/json.worker-BizpAl9O.js +1 -0
- package/assets/vendor/monaco/vs/jsonMode-DLUiMUrI.js +1 -0
- package/assets/vendor/monaco/vs/julia-BwzEvaQw.js +1 -0
- package/assets/vendor/monaco/vs/kotlin-IUYPiTV8.js +1 -0
- package/assets/vendor/monaco/vs/language/css/css.worker.js +66 -0
- package/assets/vendor/monaco/vs/language/css/monaco.contribution.js +1 -0
- package/assets/vendor/monaco/vs/language/html/html.worker.js +443 -0
- package/assets/vendor/monaco/vs/language/html/monaco.contribution.js +1 -0
- package/assets/vendor/monaco/vs/language/json/json.worker.js +17 -0
- package/assets/vendor/monaco/vs/language/json/monaco.contribution.js +1 -0
- package/assets/vendor/monaco/vs/language/typescript/monaco.contribution.js +1 -0
- package/assets/vendor/monaco/vs/language/typescript/ts.worker.js +67693 -0
- package/assets/vendor/monaco/vs/less-C0eDYdqa.js +2 -0
- package/assets/vendor/monaco/vs/lexon-iON-Kj97.js +1 -0
- package/assets/vendor/monaco/vs/liquid-CLJYelW6.js +1 -0
- package/assets/vendor/monaco/vs/loader.js +1368 -0
- package/assets/vendor/monaco/vs/lspLanguageFeatures-BIkJOWLw.js +2 -0
- package/assets/vendor/monaco/vs/lua-DtygF91M.js +1 -0
- package/assets/vendor/monaco/vs/m3-CsR4AuFi.js +1 -0
- package/assets/vendor/monaco/vs/main-BEx-Fmlo.js +15 -0
- package/assets/vendor/monaco/vs/main-DsK8pnKg.js +3 -0
- package/assets/vendor/monaco/vs/markdown-C_rD0bIw.js +1 -0
- package/assets/vendor/monaco/vs/mdx-CpzZIPHF.js +1 -0
- package/assets/vendor/monaco/vs/mips-CiYP61RB.js +1 -0
- package/assets/vendor/monaco/vs/monaco.contribution-9cKT3C7t.js +1 -0
- package/assets/vendor/monaco/vs/monaco.contribution-BE88ZNGY.js +1 -0
- package/assets/vendor/monaco/vs/monaco.contribution-BPhsneLd.js +1 -0
- package/assets/vendor/monaco/vs/monaco.contribution-BgRy6xDf.js +1 -0
- package/assets/vendor/monaco/vs/msdax-C38-sJlp.js +1 -0
- package/assets/vendor/monaco/vs/mysql-CdtbpvbG.js +1 -0
- package/assets/vendor/monaco/vs/nls/lang/zh-cn.d.ts +1 -0
- package/assets/vendor/monaco/vs/nls/lang/zh-cn.js +9 -0
- package/assets/vendor/monaco/vs/nls.messages-loader.js +1 -0
- package/assets/vendor/monaco/vs/objective-c-CntZFaHX.js +1 -0
- package/assets/vendor/monaco/vs/pascal-r6kuqfl_.js +1 -0
- package/assets/vendor/monaco/vs/pascaligo-BiXoTmXh.js +1 -0
- package/assets/vendor/monaco/vs/perl-DABw_TcH.js +1 -0
- package/assets/vendor/monaco/vs/pgsql-me_jFXeX.js +1 -0
- package/assets/vendor/monaco/vs/php-D_kh-9LK.js +1 -0
- package/assets/vendor/monaco/vs/pla-VfZjczW0.js +1 -0
- package/assets/vendor/monaco/vs/postiats-BBSzz8Pk.js +1 -0
- package/assets/vendor/monaco/vs/powerquery-Dt-g_2cc.js +1 -0
- package/assets/vendor/monaco/vs/powershell-B-7ap1zc.js +1 -0
- package/assets/vendor/monaco/vs/protobuf-BmtuEB1A.js +2 -0
- package/assets/vendor/monaco/vs/pug-BRpRNeEb.js +1 -0
- package/assets/vendor/monaco/vs/python-CqWUUgfu.js +1 -0
- package/assets/vendor/monaco/vs/qsharp-BzsFaUU9.js +1 -0
- package/assets/vendor/monaco/vs/r-f8dDdrp4.js +1 -0
- package/assets/vendor/monaco/vs/razor-hok5y9Km.js +1 -0
- package/assets/vendor/monaco/vs/redis-fvZQY4PI.js +1 -0
- package/assets/vendor/monaco/vs/redshift-45Et0LQi.js +1 -0
- package/assets/vendor/monaco/vs/restructuredtext-C7UUFKFD.js +1 -0
- package/assets/vendor/monaco/vs/ruby-CZO8zYTz.js +1 -0
- package/assets/vendor/monaco/vs/rust-Bfetafyc.js +1 -0
- package/assets/vendor/monaco/vs/sb-3GYllVck.js +1 -0
- package/assets/vendor/monaco/vs/scala-foMgrKo1.js +1 -0
- package/assets/vendor/monaco/vs/scheme-CHdMtr7p.js +1 -0
- package/assets/vendor/monaco/vs/scss-C1cmLt9V.js +3 -0
- package/assets/vendor/monaco/vs/shell-ClXCKCEW.js +1 -0
- package/assets/vendor/monaco/vs/solidity-MZ6ExpPy.js +1 -0
- package/assets/vendor/monaco/vs/sophia-DWkuSsPQ.js +1 -0
- package/assets/vendor/monaco/vs/sparql-AUGFYSyk.js +1 -0
- package/assets/vendor/monaco/vs/sql-32GpJSV2.js +1 -0
- package/assets/vendor/monaco/vs/st-CuDFIVZ_.js +1 -0
- package/assets/vendor/monaco/vs/swift-t-PfMj7W.js +1 -0
- package/assets/vendor/monaco/vs/systemverilog-Ch4vA8Yt.js +1 -0
- package/assets/vendor/monaco/vs/tcl-D74tq1nH.js +1 -0
- package/assets/vendor/monaco/vs/toggleHighContrast-qGX7E9o7.js +61 -0
- package/assets/vendor/monaco/vs/ts.worker-2QLmBukE.js +1 -0
- package/assets/vendor/monaco/vs/tsMode-B0S_kp2q.js +11 -0
- package/assets/vendor/monaco/vs/twig-C6taOxMV.js +1 -0
- package/assets/vendor/monaco/vs/typescript-_2t_511t.js +1 -0
- package/assets/vendor/monaco/vs/typespec-8OfoLt6R.js +1 -0
- package/assets/vendor/monaco/vs/vb-Dyb2648j.js +1 -0
- package/assets/vendor/monaco/vs/wgsl-BhLXMOR0.js +298 -0
- package/assets/vendor/monaco/vs/workers-BBttULjf.js +1 -0
- package/assets/vendor/monaco/vs/xml-cZjNDqDT.js +1 -0
- package/assets/vendor/monaco/vs/yaml-A1fOIdH6.js +1 -0
- package/cordis.patch.yml +5 -0
- package/lib/client.js +1914 -0
- package/lib/client.js.map +1 -0
- package/lib/index.js +1298 -0
- package/lib/index.js.map +1 -0
- package/package.json +60 -0
- package/src/capture.ts +73 -0
- package/src/client/events.ts +36 -0
- package/src/client/index.ts +37 -0
- package/src/client/monaco/diffRender.ts +162 -0
- package/src/client/monaco/loader.ts +71 -0
- package/src/client/rpc.ts +28 -0
- package/src/client/state/records.ts +90 -0
- package/src/client/state/regions.ts +81 -0
- package/src/client/styles/css.d.ts +2 -0
- package/src/client/styles/editor.css +87 -0
- package/src/client/ui/DiffBadge.ts +51 -0
- package/src/client/ui/DiffBarEmpty.ts +37 -0
- package/src/client/ui/DiffBox.ts +79 -0
- package/src/client/ui/DiffLauncher.ts +124 -0
- package/src/client/ui/EditorView.ts +577 -0
- package/src/client/ui/QuickOpen.ts +91 -0
- package/src/client/ui/shared.ts +28 -0
- package/src/index.ts +43 -0
- package/src/model.ts +197 -0
- package/src/registry.ts +35 -0
- package/src/revert.ts +107 -0
- package/src/routes.ts +138 -0
- package/src/rpc.ts +295 -0
- package/src/shared/rpc.ts +67 -0
- package/src/shared/types.ts +121 -0
- package/src/store.ts +244 -0
- 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
|
+
}
|
package/src/registry.ts
ADDED
|
@@ -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
|
+
}
|