dsh-patch-edit-plus 0.3.1 → 0.4.0
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/CHANGELOG.md +13 -0
- package/CHANGELOG.zh.md +13 -0
- package/README.de.md +9 -4
- package/README.es.md +9 -4
- package/README.fr.md +9 -4
- package/README.it.md +9 -4
- package/README.ja.md +9 -4
- package/README.ko.md +9 -4
- package/README.md +9 -4
- package/README.ru.md +9 -4
- package/README.zh.md +9 -4
- package/cordis.patch.yml +6 -0
- package/dsh.plugin.json +2 -2
- package/lib/client/index.js +151 -0
- package/lib/client.js +213 -0
- package/lib/client.js.map +1 -0
- package/lib/compat.js +101 -0
- package/lib/config.js +4 -3
- package/lib/fsops.js +23 -4
- package/lib/index.js +32 -4
- package/lib/register.js +2 -0
- package/lib/shellops.js +12 -2
- package/lib/tool.js +12 -3
- package/lib/types/client/index.d.ts +48 -0
- package/lib/types/compat.d.ts +46 -0
- package/lib/types/config.d.ts +6 -4
- package/lib/types/fsops.d.ts +0 -5
- package/lib/types/index.d.ts +2 -0
- package/lib/types/tool.d.ts +4 -2
- package/package.json +34 -10
package/lib/client.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "dsh-patch-edit-plus",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let react = require("react");
|
|
8
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
9
|
+
//#region src/client/index.tsx
|
|
10
|
+
/**
|
|
11
|
+
* dsh-patch-edit-plus — 浏览器 half(0.2.0 新增)。
|
|
12
|
+
*
|
|
13
|
+
* 单一职责:在 Plugins 页 bundle 详情页的 `plugins.bundle.config` 席位
|
|
14
|
+
* (key = 包名)下注册一张配置卡,暴露两个补丁风格开关——
|
|
15
|
+
* - `allowUnifiedDiff`(Git/unified diff,默认开)
|
|
16
|
+
* - `allowCodexPatch`(Codex apply_patch,默认关)
|
|
17
|
+
*
|
|
18
|
+
* 字段全部是 host 侧 `Config` 的 `.volatile()` 字段:卡片经
|
|
19
|
+
* `configForms.get('dsh-patch-edit-plus')` 自绑本插件 entry(entry id 与
|
|
20
|
+
* 包名一致,无错位),写入走 `form.set(field, !cur)`,提交后宿主经
|
|
21
|
+
* `loader/volatile-update` 原地重注册工具(见 src/index.ts 的 rejudge)。
|
|
22
|
+
*
|
|
23
|
+
* 构建:tsdown → lib/client.js(__ModuleLoader__.load 闭包工厂,id = 包名)。
|
|
24
|
+
* react 是唯一 value-import(平台 external);slots/configForms 经 cordis
|
|
25
|
+
* 服务消费,configForms 缺席时(理论上仅剩极老宿主)卡片整卡隐藏,不阻塞
|
|
26
|
+
* 客户端半。文案内联双语字典(zh/en),不建 locale 文件。
|
|
27
|
+
*/
|
|
28
|
+
/** 客户端所需服务:slots 全世代都有,准声明;configForms 是 0.1.7+ 服务,
|
|
29
|
+
* **不准**进顶层 inject——在老宿主上声明缺失服务会把 client entry 永久挂起
|
|
30
|
+
* (web boot 拒渲染整树,「1 entry did not activate」家族教训)。configForms
|
|
31
|
+
* 走软读(strict proxy 直读 throw → try/catch 落 ctx.get),缺席时整卡隐藏。 */
|
|
32
|
+
const inject = ["slots"];
|
|
33
|
+
/** 内联双语字典(zh/en;按 document.documentElement.lang → navigator.language 兜底)。 */
|
|
34
|
+
const STRINGS = {
|
|
35
|
+
zh: {
|
|
36
|
+
cardName: "补丁风格",
|
|
37
|
+
cardDesc: "apply_patch 接受的补丁语法",
|
|
38
|
+
unifiedTitle: "Git 风格补丁",
|
|
39
|
+
unifiedDesc: "接受 Git/unified diff 补丁语法(默认开)",
|
|
40
|
+
codexTitle: "Codex 风格补丁",
|
|
41
|
+
codexDesc: "接受 Codex apply_patch 语法(默认关)",
|
|
42
|
+
readonlyHint: "当前只读,无法修改。"
|
|
43
|
+
},
|
|
44
|
+
en: {
|
|
45
|
+
cardName: "Patch style",
|
|
46
|
+
cardDesc: "Patch syntax accepted by apply_patch",
|
|
47
|
+
unifiedTitle: "Git-style patches",
|
|
48
|
+
unifiedDesc: "Accept Git/unified diff patch syntax (default on)",
|
|
49
|
+
codexTitle: "Codex-style patches",
|
|
50
|
+
codexDesc: "Accept Codex apply_patch syntax (default off)",
|
|
51
|
+
readonlyHint: "Read-only right now; changes are not allowed."
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
/** 解析当前语言:document.documentElement.lang 优先,navigator.language 兜底,再退 zh。 */
|
|
55
|
+
function pickLang() {
|
|
56
|
+
try {
|
|
57
|
+
const docLang = typeof document !== "undefined" ? document.documentElement.lang : "";
|
|
58
|
+
const navLang = typeof navigator !== "undefined" ? navigator.language : "";
|
|
59
|
+
for (const tag of [docLang, navLang]) {
|
|
60
|
+
if (typeof tag === "string" && /^zh/i.test(tag)) return "zh";
|
|
61
|
+
if (typeof tag === "string" && /^en/i.test(tag)) return "en";
|
|
62
|
+
}
|
|
63
|
+
} catch {}
|
|
64
|
+
return "zh";
|
|
65
|
+
}
|
|
66
|
+
/** 卡片样式,注入一次(保持 bundle CSS-free;变量与 session-guard 卡同源)。 */
|
|
67
|
+
const CARD_CSS = `
|
|
68
|
+
.ppeCard{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;padding:4px 16px 8px}
|
|
69
|
+
.ppeCardName{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4;margin:10px 0 2px}
|
|
70
|
+
.ppeCardDesc{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5;margin:0 0 4px}
|
|
71
|
+
.ppeRow{border-bottom:1px solid var(--dsw-alias-border-l2);align-items:center;gap:8px;padding:14px 0;display:flex}
|
|
72
|
+
.ppeRow:last-of-type{border-bottom:0}
|
|
73
|
+
.ppeRowText{flex-direction:column;flex:1;gap:4px;min-width:0;padding-right:48px;display:flex}
|
|
74
|
+
.ppeTitle{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:400;line-height:22px}
|
|
75
|
+
.ppeDesc{color:var(--dsw-alias-label-tertiary);font-size:12px;font-weight:400;line-height:18px}
|
|
76
|
+
.ppeSwitch{appearance:none;position:relative;width:40px;height:22px;flex:none;cursor:pointer;background:transparent;border:0;padding:0}
|
|
77
|
+
.ppeSwitch:disabled{cursor:not-allowed}
|
|
78
|
+
.ppeSwitchTrack{position:absolute;inset:0;border-radius:22px;background:var(--dsw-alias-interactive-bg-hover);transition:background .16s}
|
|
79
|
+
.ppeSwitch-on>.ppeSwitchTrack{background:var(--dsw-alias-button-primary-fill)}
|
|
80
|
+
.ppeSwitchThumb{position:absolute;top:2px;left:2px;width:18px;height:18px;border-radius:50%;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.25);transition:transform .16s}
|
|
81
|
+
.ppeSwitch-on>.ppeSwitchThumb{transform:translateX(18px)}
|
|
82
|
+
.ppeSwitch:disabled>.ppeSwitchTrack{opacity:.5}
|
|
83
|
+
.ppeReadonly{color:var(--dsw-alias-label-tertiary);font-size:12px;margin:8px 0 4px}
|
|
84
|
+
`;
|
|
85
|
+
/** 注入一次卡片样式。 */
|
|
86
|
+
function injectCss() {
|
|
87
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=\"patch-edit-plus-card\"]") === null) {
|
|
88
|
+
const tag = document.createElement("style");
|
|
89
|
+
tag.dataset.plugin = "dsh-patch-edit-plus";
|
|
90
|
+
tag.dataset.pluginCss = "patch-edit-plus-card";
|
|
91
|
+
tag.textContent = CARD_CSS;
|
|
92
|
+
document.head.appendChild(tag);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const NOOP_SNAPSHOT = {
|
|
96
|
+
status: "unavailable",
|
|
97
|
+
value: void 0,
|
|
98
|
+
writable: false
|
|
99
|
+
};
|
|
100
|
+
const unsubscribeNoop = () => {};
|
|
101
|
+
/** 一行 role="switch" 开关(aria-checked + data-* 便于测试选择器)。 */
|
|
102
|
+
function SwitchRow(props) {
|
|
103
|
+
const { field, title, description, checked, disabled, onToggle } = props;
|
|
104
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
105
|
+
className: "ppeRow",
|
|
106
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
107
|
+
className: "ppeRowText",
|
|
108
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
109
|
+
className: "ppeTitle",
|
|
110
|
+
children: title
|
|
111
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
112
|
+
className: "ppeDesc",
|
|
113
|
+
children: description
|
|
114
|
+
})]
|
|
115
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
116
|
+
type: "button",
|
|
117
|
+
role: "switch",
|
|
118
|
+
"aria-checked": checked,
|
|
119
|
+
"data-plugin": "dsh-patch-edit-plus",
|
|
120
|
+
"data-field": field,
|
|
121
|
+
"data-checked": checked ? "true" : "false",
|
|
122
|
+
className: "ppeSwitch" + (checked ? " ppeSwitch-on" : ""),
|
|
123
|
+
disabled,
|
|
124
|
+
onClick: () => onToggle(field, checked),
|
|
125
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "ppeSwitchTrack" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "ppeSwitchThumb" })]
|
|
126
|
+
})]
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* 插件页配置卡主体:两行补丁风格开关。
|
|
131
|
+
* - status === 'unavailable'(无 configForms / entry 未装配)→ 整卡隐藏(null);
|
|
132
|
+
* - status === 'loading' → 开关禁用;
|
|
133
|
+
* - 仅 snapshot.writable 时可写(写路径 `form.set(field, !cur)`)。
|
|
134
|
+
*/
|
|
135
|
+
function PatchStyleCard(props) {
|
|
136
|
+
const form = props.form;
|
|
137
|
+
const snapshot = (0, react.useSyncExternalStore)(form ? (listener) => form.subscribe(listener) : () => unsubscribeNoop, () => form?.getSnapshot() ?? NOOP_SNAPSHOT);
|
|
138
|
+
injectCss();
|
|
139
|
+
if (snapshot.status === "unavailable") return null;
|
|
140
|
+
const t = STRINGS[pickLang()];
|
|
141
|
+
const editable = snapshot.status === "ready" && snapshot.writable;
|
|
142
|
+
const value = snapshot.value ?? {};
|
|
143
|
+
const toggle = (field, current) => {
|
|
144
|
+
if (form === void 0 || !editable) return;
|
|
145
|
+
form.set(field, !current).catch(() => {});
|
|
146
|
+
};
|
|
147
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
148
|
+
className: "ppeCard",
|
|
149
|
+
"data-plugin": "dsh-patch-edit-plus",
|
|
150
|
+
children: [
|
|
151
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
152
|
+
className: "ppeCardName",
|
|
153
|
+
children: t.cardName
|
|
154
|
+
}),
|
|
155
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
156
|
+
className: "ppeCardDesc",
|
|
157
|
+
children: t.cardDesc
|
|
158
|
+
}),
|
|
159
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SwitchRow, {
|
|
160
|
+
field: "allowUnifiedDiff",
|
|
161
|
+
title: t.unifiedTitle,
|
|
162
|
+
description: t.unifiedDesc,
|
|
163
|
+
checked: value.allowUnifiedDiff ?? true,
|
|
164
|
+
disabled: !editable,
|
|
165
|
+
onToggle: toggle
|
|
166
|
+
}),
|
|
167
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SwitchRow, {
|
|
168
|
+
field: "allowCodexPatch",
|
|
169
|
+
title: t.codexTitle,
|
|
170
|
+
description: t.codexDesc,
|
|
171
|
+
checked: value.allowCodexPatch ?? false,
|
|
172
|
+
disabled: !editable,
|
|
173
|
+
onToggle: toggle
|
|
174
|
+
}),
|
|
175
|
+
snapshot.status === "ready" && !snapshot.writable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
176
|
+
className: "ppeReadonly",
|
|
177
|
+
children: t.readonlyHint
|
|
178
|
+
})
|
|
179
|
+
]
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* 客户端插件体:在 Plugins 页 bundle 详情页的 keyed 席位(key = 包名)下
|
|
184
|
+
* 注册配置卡。卡片自绑 `configForms.get('dsh-patch-edit-plus')`——form 在
|
|
185
|
+
* 渲染期解析(而非 apply 期固化),宿主晚于客户端半开始服务该 entry 时
|
|
186
|
+
* 也能追上;页面注入的 owner props(`{ view: 'page', form }`)不消费,
|
|
187
|
+
* 以本插件 entry 的作用域为唯一事实来源。
|
|
188
|
+
*/
|
|
189
|
+
function apply(ctx) {
|
|
190
|
+
let configForms;
|
|
191
|
+
try {
|
|
192
|
+
configForms = ctx.configForms ?? (typeof ctx.get === "function" ? ctx.get("configForms") : void 0);
|
|
193
|
+
} catch {
|
|
194
|
+
configForms = void 0;
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
ctx.slots.inject("plugins.bundle.config", () => ctx.slots.register({
|
|
198
|
+
name: "plugins.bundle.config",
|
|
199
|
+
key: "dsh-patch-edit-plus"
|
|
200
|
+
}, () => (0, react.createElement)(PatchStyleCard, { form: configForms?.get?.("dsh-patch-edit-plus") })));
|
|
201
|
+
} catch (error) {
|
|
202
|
+
console.warn("[dsh-patch-edit-plus] plugins.bundle.config seat unavailable on this host line:", error);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
//#endregion
|
|
206
|
+
exports.PatchStyleCard = PatchStyleCard;
|
|
207
|
+
exports.apply = apply;
|
|
208
|
+
exports.inject = inject;
|
|
209
|
+
return module.exports;
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","names":["useSyncExternalStore","createElement"],"sources":["../src/client/index.tsx"],"sourcesContent":["/**\n * dsh-patch-edit-plus — 浏览器 half(0.2.0 新增)。\n *\n * 单一职责:在 Plugins 页 bundle 详情页的 `plugins.bundle.config` 席位\n * (key = 包名)下注册一张配置卡,暴露两个补丁风格开关——\n * - `allowUnifiedDiff`(Git/unified diff,默认开)\n * - `allowCodexPatch`(Codex apply_patch,默认关)\n *\n * 字段全部是 host 侧 `Config` 的 `.volatile()` 字段:卡片经\n * `configForms.get('dsh-patch-edit-plus')` 自绑本插件 entry(entry id 与\n * 包名一致,无错位),写入走 `form.set(field, !cur)`,提交后宿主经\n * `loader/volatile-update` 原地重注册工具(见 src/index.ts 的 rejudge)。\n *\n * 构建:tsdown → lib/client.js(__ModuleLoader__.load 闭包工厂,id = 包名)。\n * react 是唯一 value-import(平台 external);slots/configForms 经 cordis\n * 服务消费,configForms 缺席时(理论上仅剩极老宿主)卡片整卡隐藏,不阻塞\n * 客户端半。文案内联双语字典(zh/en),不建 locale 文件。\n */\nimport { createElement, useSyncExternalStore } from 'react'\nimport type { JSX } from 'react'\n\n/** 本插件消费的 configForms 值形状(与 host ResolvedConfig 的易变布尔对齐)。 */\nexport interface PatchEditPlusConfig {\n allowUnifiedDiff: boolean\n allowCodexPatch: boolean\n}\n\n/** configForms 作用域面(结构性子集;0.1.7+ 缝,避免硬 peer 类型)。 */\nexport interface PatchFormScope {\n getSnapshot(): {\n status: 'loading' | 'ready' | 'unavailable'\n value: Partial<PatchEditPlusConfig> | undefined\n writable: boolean\n }\n subscribe(listener: () => void): () => void\n set(field: string, value: unknown): Promise<boolean>\n}\n\n/** configForms 服务面(以 entry id 取句柄)。 */\ninterface ConfigFormsFace {\n get?(entryId: string): PatchFormScope | undefined\n}\n\n/** 客户端所需服务:slots 全世代都有,准声明;configForms 是 0.1.7+ 服务,\n * **不准**进顶层 inject——在老宿主上声明缺失服务会把 client entry 永久挂起\n * (web boot 拒渲染整树,「1 entry did not activate」家族教训)。configForms\n * 走软读(strict proxy 直读 throw → try/catch 落 ctx.get),缺席时整卡隐藏。 */\nexport const inject = ['slots']\n\n/** 轻量 ctx 类型(仅本客户端用到的方法;构建时类型被剥离)。 */\ninterface SlotsFace {\n inject: (_name: string, _fn: () => unknown) => () => void\n register: (_options: Record<string, unknown>, _component: unknown) => () => void\n}\ninterface ClientCtx {\n slots: SlotsFace\n get?: (_name: string) => unknown\n}\n\n/** 内联双语字典(zh/en;按 document.documentElement.lang → navigator.language 兜底)。 */\nconst STRINGS = {\n zh: {\n cardName: '补丁风格',\n cardDesc: 'apply_patch 接受的补丁语法',\n unifiedTitle: 'Git 风格补丁',\n unifiedDesc: '接受 Git/unified diff 补丁语法(默认开)',\n codexTitle: 'Codex 风格补丁',\n codexDesc: '接受 Codex apply_patch 语法(默认关)',\n readonlyHint: '当前只读,无法修改。',\n },\n en: {\n cardName: 'Patch style',\n cardDesc: 'Patch syntax accepted by apply_patch',\n unifiedTitle: 'Git-style patches',\n unifiedDesc: 'Accept Git/unified diff patch syntax (default on)',\n codexTitle: 'Codex-style patches',\n codexDesc: 'Accept Codex apply_patch syntax (default off)',\n readonlyHint: 'Read-only right now; changes are not allowed.',\n },\n} as const\n\ntype Lang = keyof typeof STRINGS\n\n/** 解析当前语言:document.documentElement.lang 优先,navigator.language 兜底,再退 zh。 */\nfunction pickLang(): Lang {\n try {\n const docLang = typeof document !== 'undefined' ? document.documentElement.lang : ''\n const navLang = typeof navigator !== 'undefined' ? navigator.language : ''\n for (const tag of [docLang, navLang]) {\n if (typeof tag === 'string' && /^zh/i.test(tag)) return 'zh'\n if (typeof tag === 'string' && /^en/i.test(tag)) return 'en'\n }\n } catch {\n // 非 DOM 环境保底 zh\n }\n return 'zh'\n}\n\n/** 卡片样式,注入一次(保持 bundle CSS-free;变量与 session-guard 卡同源)。 */\nconst CARD_CSS = `\n.ppeCard{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;padding:4px 16px 8px}\n.ppeCardName{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4;margin:10px 0 2px}\n.ppeCardDesc{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5;margin:0 0 4px}\n.ppeRow{border-bottom:1px solid var(--dsw-alias-border-l2);align-items:center;gap:8px;padding:14px 0;display:flex}\n.ppeRow:last-of-type{border-bottom:0}\n.ppeRowText{flex-direction:column;flex:1;gap:4px;min-width:0;padding-right:48px;display:flex}\n.ppeTitle{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:400;line-height:22px}\n.ppeDesc{color:var(--dsw-alias-label-tertiary);font-size:12px;font-weight:400;line-height:18px}\n.ppeSwitch{appearance:none;position:relative;width:40px;height:22px;flex:none;cursor:pointer;background:transparent;border:0;padding:0}\n.ppeSwitch:disabled{cursor:not-allowed}\n.ppeSwitchTrack{position:absolute;inset:0;border-radius:22px;background:var(--dsw-alias-interactive-bg-hover);transition:background .16s}\n.ppeSwitch-on>.ppeSwitchTrack{background:var(--dsw-alias-button-primary-fill)}\n.ppeSwitchThumb{position:absolute;top:2px;left:2px;width:18px;height:18px;border-radius:50%;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.25);transition:transform .16s}\n.ppeSwitch-on>.ppeSwitchThumb{transform:translateX(18px)}\n.ppeSwitch:disabled>.ppeSwitchTrack{opacity:.5}\n.ppeReadonly{color:var(--dsw-alias-label-tertiary);font-size:12px;margin:8px 0 4px}\n`\n\n/** 注入一次卡片样式。 */\nfunction injectCss(): void {\n if (typeof document !== 'undefined' && document.querySelector('style[data-plugin-css=\"patch-edit-plus-card\"]') === null) {\n const tag = document.createElement('style')\n tag.dataset.plugin = 'dsh-patch-edit-plus'\n tag.dataset.pluginCss = 'patch-edit-plus-card'\n tag.textContent = CARD_CSS\n document.head.appendChild(tag)\n }\n}\n\nconst NOOP_SNAPSHOT = { status: 'unavailable' as const, value: undefined, writable: false }\nconst unsubscribeNoop = (): void => {}\n\n/** 一行 role=\"switch\" 开关(aria-checked + data-* 便于测试选择器)。 */\nfunction SwitchRow(props: {\n field: 'allowUnifiedDiff' | 'allowCodexPatch'\n title: string\n description: string\n checked: boolean\n disabled: boolean\n onToggle: (_field: 'allowUnifiedDiff' | 'allowCodexPatch', _current: boolean) => void\n}): JSX.Element {\n const { field, title, description, checked, disabled, onToggle } = props\n return (\n <div className=\"ppeRow\">\n <div className=\"ppeRowText\">\n <div className=\"ppeTitle\">{title}</div>\n <div className=\"ppeDesc\">{description}</div>\n </div>\n <button\n type=\"button\"\n role=\"switch\"\n aria-checked={checked}\n data-plugin=\"dsh-patch-edit-plus\"\n data-field={field}\n data-checked={checked ? 'true' : 'false'}\n className={'ppeSwitch' + (checked ? ' ppeSwitch-on' : '')}\n disabled={disabled}\n onClick={() => onToggle(field, checked)}\n >\n <span className=\"ppeSwitchTrack\" />\n <span className=\"ppeSwitchThumb\" />\n </button>\n </div>\n )\n}\n\n/**\n * 插件页配置卡主体:两行补丁风格开关。\n * - status === 'unavailable'(无 configForms / entry 未装配)→ 整卡隐藏(null);\n * - status === 'loading' → 开关禁用;\n * - 仅 snapshot.writable 时可写(写路径 `form.set(field, !cur)`)。\n */\nexport function PatchStyleCard(props: { form?: PatchFormScope | undefined }): JSX.Element | null {\n const form = props.form\n const snapshot = useSyncExternalStore(\n form\n ? (listener) => form.subscribe(listener)\n : () => unsubscribeNoop,\n () => form?.getSnapshot() ?? NOOP_SNAPSHOT,\n )\n\n injectCss()\n\n if (snapshot.status === 'unavailable') return null\n const t = STRINGS[pickLang()]\n // loading / 只读都归约为「不可写」:开关禁用,仅 ready + writable 时可写。\n const editable = snapshot.status === 'ready' && snapshot.writable\n const value = snapshot.value ?? {}\n const toggle = (field: 'allowUnifiedDiff' | 'allowCodexPatch', current: boolean): void => {\n if (form === undefined || !editable) return\n void form.set(field, !current).catch(() => {})\n }\n return (\n <section className=\"ppeCard\" data-plugin=\"dsh-patch-edit-plus\">\n <div className=\"ppeCardName\">{t.cardName}</div>\n <p className=\"ppeCardDesc\">{t.cardDesc}</p>\n <SwitchRow\n field=\"allowUnifiedDiff\"\n title={t.unifiedTitle}\n description={t.unifiedDesc}\n checked={value.allowUnifiedDiff ?? true}\n disabled={!editable}\n onToggle={toggle}\n />\n <SwitchRow\n field=\"allowCodexPatch\"\n title={t.codexTitle}\n description={t.codexDesc}\n checked={value.allowCodexPatch ?? false}\n disabled={!editable}\n onToggle={toggle}\n />\n {snapshot.status === 'ready' && !snapshot.writable && <p className=\"ppeReadonly\">{t.readonlyHint}</p>}\n </section>\n )\n}\n\n/**\n * 客户端插件体:在 Plugins 页 bundle 详情页的 keyed 席位(key = 包名)下\n * 注册配置卡。卡片自绑 `configForms.get('dsh-patch-edit-plus')`——form 在\n * 渲染期解析(而非 apply 期固化),宿主晚于客户端半开始服务该 entry 时\n * 也能追上;页面注入的 owner props(`{ view: 'page', form }`)不消费,\n * 以本插件 entry 的作用域为唯一事实来源。\n */\nexport function apply(ctx: ClientCtx): void {\n // 软读:strict ctx proxy 对未声明服务的属性直读会 throw(「1 entry did not\n // activate」家族纪律:按代服务一律软取,绝不进顶层 inject)。\n let configForms: ConfigFormsFace | undefined\n try {\n configForms = ((ctx as unknown as { configForms?: ConfigFormsFace }).configForms\n ?? (typeof ctx.get === 'function' ? ctx.get('configForms') : undefined)) as ConfigFormsFace | undefined\n } catch {\n configForms = undefined\n }\n // 老宿主不认识 plugins.bundle.config 席位——激活期 throw 会拖死整个\n // client entry,包 safeSeat 降级为「本线无配置卡」。\n try {\n ctx.slots.inject('plugins.bundle.config', () => ctx.slots.register(\n { name: 'plugins.bundle.config', key: 'dsh-patch-edit-plus' },\n () => createElement(PatchStyleCard, { form: configForms?.get?.('dsh-patch-edit-plus') }),\n ))\n } catch (error) {\n console.warn('[dsh-patch-edit-plus] plugins.bundle.config seat unavailable on this host line:', error)\n }\n}\n"],"mappings":"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"}
|
package/lib/compat.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generation-compat seams for the single max-range build (0.1.0-rc.8 → 0.2.0).
|
|
3
|
+
*
|
|
4
|
+
* Three old-line traps live here:
|
|
5
|
+
* 1. `.volatile()` — the schema method only exists on the 0.1.7+ schemastery
|
|
6
|
+
* line; calling it blind is a load-time TypeError on older hosts. Condition
|
|
7
|
+
* it with {@link maybeVolatile}.
|
|
8
|
+
* 2. The settings registration API is three generations deep: instance
|
|
9
|
+
* `settings.installSection` (0.1.2/0.1.5, real-host verified), the module
|
|
10
|
+
* `installSettingsSection` export of `@deepseek-ai/dsh-settings`
|
|
11
|
+
* (0.1.0/0.1.1 — verified against the 0.1.0-rc.8 tarball; the
|
|
12
|
+
* dsh-client-ui-settings package is a host-side no-op there), and the
|
|
13
|
+
* declarative `.volatile()` form on 0.1.7+. {@link installSettingsCompat}
|
|
14
|
+
* picks by feature detection, never by version number.
|
|
15
|
+
* 3. Every cross-generation import must ride a namespace soft-import: a static
|
|
16
|
+
* named import of an absent export dies at ESM link time and takes the
|
|
17
|
+
* whole loader entry down.
|
|
18
|
+
* @module dsh-patch-edit-plus/compat
|
|
19
|
+
*/
|
|
20
|
+
/** Wrap a schema field with `.volatile()` only when the host line supports it. */
|
|
21
|
+
export function maybeVolatile(schema) {
|
|
22
|
+
const candidate = schema;
|
|
23
|
+
if (typeof candidate?.volatile === 'function') {
|
|
24
|
+
try {
|
|
25
|
+
return candidate.volatile();
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return schema;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return schema;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Register the settings section across host generations. 0.1.7+ declarative
|
|
35
|
+
* volatile hosts never call this (their schema fields are already volatile and
|
|
36
|
+
* no registration call is wanted); this is the 0.1.5-and-below path.
|
|
37
|
+
*/
|
|
38
|
+
export function installSettingsCompat(ctx, namespace, schema, entry, hooks) {
|
|
39
|
+
console.log('[dsh-patch-edit-plus] compat waist mounted');
|
|
40
|
+
try {
|
|
41
|
+
ctx.inject(['settings'], (settingsCtx) => {
|
|
42
|
+
try {
|
|
43
|
+
const settings = settingsCtx.settings
|
|
44
|
+
?? settingsCtx;
|
|
45
|
+
if (typeof settings?.installSection === 'function') {
|
|
46
|
+
try {
|
|
47
|
+
settings.installSection(ctx, namespace, schema, entry, hooks);
|
|
48
|
+
console.log('[dsh-patch-edit-plus] settings via installSection (0.1.2/0.1.5 line)');
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
console.warn('[dsh-patch-edit-plus] settings.installSection unavailable:', error);
|
|
52
|
+
}
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (typeof settings?.register === 'function') {
|
|
56
|
+
// 0.1.0–0.1.5 imperative register(宿主模块级 installSettingsSection 的
|
|
57
|
+
// 内部实现就是这一段)。0.1.7+ 声明式线永远走不到这里——本函数只在组合
|
|
58
|
+
// 条目携带普通布尔(非 live ref)时才会被调用。真机注:0.1.0 沙盒根本
|
|
59
|
+
// 不发货 @deepseek-ai/dsh-settings 包,模块级 helper 不可解析,实例
|
|
60
|
+
// register 是唯一可达面。
|
|
61
|
+
try {
|
|
62
|
+
const scope = settings.register(namespace, schema, { base: entry });
|
|
63
|
+
if (scope !== undefined) {
|
|
64
|
+
hooks.setSource(() => scope.get());
|
|
65
|
+
scope.watch(() => { hooks.onChange(); });
|
|
66
|
+
hooks.onChange();
|
|
67
|
+
console.log('[dsh-patch-edit-plus] settings via register (0.1.0-0.1.5 imperative face)');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
console.warn('[dsh-patch-edit-plus] settings.register unavailable:', error);
|
|
72
|
+
}
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
// 0.1.0/0.1.1: no instance API — the module-level helper wraps
|
|
76
|
+
// settings.register with the resolved-scope plumbing.
|
|
77
|
+
import('@deepseek-ai/dsh-settings').then((mod) => {
|
|
78
|
+
const m = mod;
|
|
79
|
+
const helper = m?.installSettingsSection ?? m?.default?.installSettingsSection;
|
|
80
|
+
if (typeof helper === 'function') {
|
|
81
|
+
try {
|
|
82
|
+
helper(ctx, namespace, schema, entry, hooks);
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
console.warn('[dsh-patch-edit-plus] installSettingsSection unavailable:', error);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}).catch(() => {
|
|
89
|
+
// Module absent on this line — no settings surface, tool still works.
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
catch (cbError) {
|
|
93
|
+
console.warn('[dsh-patch-edit-plus] settings callback threw:', cbError);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
// Hosts without the sub-inject seam (very old cordis) skip settings entirely.
|
|
99
|
+
console.warn('[dsh-patch-edit-plus] settings sub-inject unavailable on this host line:', error);
|
|
100
|
+
}
|
|
101
|
+
}
|
package/lib/config.js
CHANGED
|
@@ -6,13 +6,14 @@
|
|
|
6
6
|
* @module dsh-patch-edit-plus/config
|
|
7
7
|
*/
|
|
8
8
|
import z from '@deepseek-ai/schemastery';
|
|
9
|
+
import { maybeVolatile } from './compat.js';
|
|
9
10
|
/** Schemastery schema for Loader defaults and generated configuration docs. */
|
|
10
11
|
export const Config = z.object({
|
|
11
12
|
toolName: z.string().default('apply_patch'),
|
|
12
13
|
conflictPolicy: z.union(['rename', 'skip', 'fail']).default('rename'),
|
|
13
14
|
renameSuffix: z.string().default('_1'),
|
|
14
|
-
allowUnifiedDiff: z.boolean().default(true),
|
|
15
|
-
allowCodexPatch: z.boolean().default(false)
|
|
15
|
+
allowUnifiedDiff: maybeVolatile(z.boolean().default(true)),
|
|
16
|
+
allowCodexPatch: maybeVolatile(z.boolean().default(false)),
|
|
16
17
|
deleteBackend: z.union(['shell', 'none']).default('shell'),
|
|
17
18
|
shellDialect: z.union(['auto', 'posix', 'pwsh']).default('auto'),
|
|
18
19
|
deleteCommand: z.string(),
|
|
@@ -38,7 +39,7 @@ export function resolveConfig(config) {
|
|
|
38
39
|
toolName: typeof c.toolName === 'string' && c.toolName.trim() !== '' ? c.toolName : 'apply_patch',
|
|
39
40
|
conflictPolicy: c.conflictPolicy ?? 'rename',
|
|
40
41
|
renameSuffix: typeof c.renameSuffix === 'string' ? c.renameSuffix : '_1',
|
|
41
|
-
allowUnifiedDiff: c.allowUnifiedDiff
|
|
42
|
+
allowUnifiedDiff: readVolatileBoolean(c.allowUnifiedDiff, true),
|
|
42
43
|
allowCodexPatch: readVolatileBoolean(c.allowCodexPatch, false),
|
|
43
44
|
deleteBackend: c.deleteBackend ?? 'shell',
|
|
44
45
|
shellDialect: c.shellDialect ?? 'auto',
|
package/lib/fsops.js
CHANGED
|
@@ -16,14 +16,33 @@ import { ioError } from './errors.js';
|
|
|
16
16
|
/**
|
|
17
17
|
* Write one file through the full intent dance. Must stay byte-for-byte the
|
|
18
18
|
* official sequence — the verification script asserts it.
|
|
19
|
+
* Old lines (0.1.0/0.1.1) may lack the write-intent events or the 5-param
|
|
20
|
+
* writeText; degrade to a plain 2-param write with a one-time warning — the
|
|
21
|
+
* read-before-write gate is a hardening, not a hard dependency.
|
|
19
22
|
* @throws {@link PatchError} code `IO` when the backend rejects the write.
|
|
20
23
|
*/
|
|
24
|
+
let fsShapeWarned = false;
|
|
25
|
+
function warnFsShapeOnce(detail) {
|
|
26
|
+
if (fsShapeWarned)
|
|
27
|
+
return;
|
|
28
|
+
fsShapeWarned = true;
|
|
29
|
+
console.warn(`[dsh-patch-edit-plus] falling back to plain fs writes on this host line (${detail}); the read-before-write intent gate is unavailable.`);
|
|
30
|
+
}
|
|
21
31
|
export async function writeFile(ctx, exec, target, content, sandboxPolicy) {
|
|
22
32
|
try {
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
33
|
+
const writeText = ctx.fs.writeText.bind(ctx.fs);
|
|
34
|
+
// 5 参意图舞(write-intent + observed 事件)只在实现了它的代上走;判定取等:仅当形参恰为 2(老线签名)才降级直写——stub/新线的
|
|
35
|
+
// mock length 常为 0,取等避免误伤意图舞。
|
|
36
|
+
if (writeText.length !== 2 && typeof ctx.waterfall === 'function') {
|
|
37
|
+
const intent = await ctx.waterfall('fs/write-intent', target, exec, () => undefined);
|
|
38
|
+
const outcome = await writeText(target, content, intent, exec.signal, sandboxPolicy);
|
|
39
|
+
if (typeof ctx.emit === 'function') {
|
|
40
|
+
ctx.emit('fs/observed', target, { kind: 'present', version: outcome.version }, exec);
|
|
41
|
+
}
|
|
42
|
+
return outcome;
|
|
43
|
+
}
|
|
44
|
+
warnFsShapeOnce('no write-intent arity / no waterfall');
|
|
45
|
+
return await writeText(target, content);
|
|
27
46
|
}
|
|
28
47
|
catch (error) {
|
|
29
48
|
if (error instanceof Error && error.name === 'PatchError')
|
package/lib/index.js
CHANGED
|
@@ -14,6 +14,11 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { resolveConfig } from './config.js';
|
|
16
16
|
import { registerApplyPatchTool } from './register.js';
|
|
17
|
+
import { installSettingsCompat } from './compat.js';
|
|
18
|
+
import { readVolatileBoolean } from './config.js';
|
|
19
|
+
import { Config } from './config.js';
|
|
20
|
+
/** 设置命名空间(0.1.5 及以下实例/模块注册面用;0.1.7+ 声明式线不消费)。 */
|
|
21
|
+
export const SETTINGS_NAMESPACE = 'patch-edit-plus';
|
|
17
22
|
/** Cordis plugin name used by Loader diagnostics. */
|
|
18
23
|
export const name = 'dsh-patch-edit-plus';
|
|
19
24
|
/**
|
|
@@ -43,10 +48,13 @@ export function apply(ctx, config) {
|
|
|
43
48
|
// 组合条目(0.1.7 起 volatile 字段在其中是 live 引用):rejudge 每次整体
|
|
44
49
|
// 重解析一次,一次调用内是同一份快照;跨调用的最新值由事件驱动重取。
|
|
45
50
|
const compositionEntry = config ?? {};
|
|
51
|
+
// 权威来源:设置层挂载时是解析后的 scope(installSection/installSettingsSection
|
|
52
|
+
// 的 setSource 在 attach 时先于 onChange 调用),否则是组合条目。
|
|
53
|
+
let readSource = () => compositionEntry;
|
|
46
54
|
let disposer = null;
|
|
47
55
|
let registeredKey = null;
|
|
48
56
|
const rejudge = () => {
|
|
49
|
-
const next = resolveConfig(
|
|
57
|
+
const next = resolveConfig(readSource());
|
|
50
58
|
const key = JSON.stringify(next);
|
|
51
59
|
if (key === registeredKey)
|
|
52
60
|
return;
|
|
@@ -62,9 +70,29 @@ export function apply(ctx, config) {
|
|
|
62
70
|
registeredKey = null;
|
|
63
71
|
};
|
|
64
72
|
});
|
|
73
|
+
// 三代腰无条件挂载:loader 在每一代都经由 entry schema 传 config——live ref
|
|
74
|
+
// 连命令式宿主上也在(TL 实证),所以不能用「entry 带对象」判代。世代分流
|
|
75
|
+
// 全在设置服务面内做:0.1.7+ 服务无 register/installSection → 回调空转;
|
|
76
|
+
// 0.1.2/0.1.5 installSection;0.1.0/0.1.1 register。base 用 readVolatile 展平。
|
|
77
|
+
const plainBase = {
|
|
78
|
+
...compositionEntry,
|
|
79
|
+
allowUnifiedDiff: readVolatileBoolean(compositionEntry.allowUnifiedDiff, true),
|
|
80
|
+
allowCodexPatch: readVolatileBoolean(compositionEntry.allowCodexPatch, false),
|
|
81
|
+
};
|
|
82
|
+
installSettingsCompat(ctx, SETTINGS_NAMESPACE, Config, plainBase, {
|
|
83
|
+
setSource: (current) => { readSource = current; },
|
|
84
|
+
onChange: () => { rejudge(); },
|
|
85
|
+
});
|
|
65
86
|
// 0.1.7+: 只有 volatile 字段变更才走这里(不 remount);普通字段变更会整体
|
|
66
87
|
// remount 插件,新一次 `apply` 自然读到全部新值。等值变更宿主不通知。
|
|
67
|
-
ctx.on
|
|
68
|
-
|
|
69
|
-
|
|
88
|
+
// 老宿主不声明该事件——ctx.on 对未知事件名在极老 cordis 上的行为未知,
|
|
89
|
+
// 包一层 try/catch 让静默无更新成为唯一代价。
|
|
90
|
+
try {
|
|
91
|
+
ctx.on('loader/volatile-update', () => {
|
|
92
|
+
rejudge();
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// pre-0.1.7 host: no volatile updates; normal config edits remount the plugin.
|
|
97
|
+
}
|
|
70
98
|
}
|
package/lib/register.js
CHANGED
package/lib/shellops.js
CHANGED
|
@@ -32,6 +32,16 @@ export function deleteTemplate(cfg) {
|
|
|
32
32
|
export function moveTemplate(cfg) {
|
|
33
33
|
return cfg.moveCommand ?? (resolveDialect(cfg) === 'pwsh' ? PWSH_MOVE : POSIX_MOVE);
|
|
34
34
|
}
|
|
35
|
+
/** Raw path for shell env vars; old dsh-fs lines lack processPath — fall back to the target's encoded path. */
|
|
36
|
+
function targetPath(ctx, target) {
|
|
37
|
+
try {
|
|
38
|
+
if (typeof ctx.fs.processPath === 'function') {
|
|
39
|
+
return ctx.fs.processPath(target);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch { /* strict proxy or absent service */ }
|
|
43
|
+
return (target.path ?? target.displayPath);
|
|
44
|
+
}
|
|
35
45
|
/** Structurally resolve `ctx.shell` without assuming the service is mounted. */
|
|
36
46
|
export function resolveShell(ctx) {
|
|
37
47
|
try {
|
|
@@ -60,7 +70,7 @@ export async function removeFile(ctx, exec, cfg, target, sandboxPolicy) {
|
|
|
60
70
|
const shell = requireShell(ctx, cfg);
|
|
61
71
|
const request = {
|
|
62
72
|
command: deleteTemplate(cfg),
|
|
63
|
-
env: { DSH_PATCH_TARGET: ctx
|
|
73
|
+
env: { DSH_PATCH_TARGET: targetPath(ctx, target) },
|
|
64
74
|
signal: exec.signal,
|
|
65
75
|
timeoutMs: SHELL_TIMEOUT_MS,
|
|
66
76
|
sandboxPolicy,
|
|
@@ -73,7 +83,7 @@ export async function moveFile(ctx, exec, cfg, source, target, sandboxPolicy) {
|
|
|
73
83
|
const shell = requireShell(ctx, cfg);
|
|
74
84
|
const request = {
|
|
75
85
|
command: moveTemplate(cfg),
|
|
76
|
-
env: { DSH_PATCH_SOURCE: ctx
|
|
86
|
+
env: { DSH_PATCH_SOURCE: targetPath(ctx, source), DSH_PATCH_TARGET: targetPath(ctx, target) },
|
|
77
87
|
signal: exec.signal,
|
|
78
88
|
timeoutMs: SHELL_TIMEOUT_MS,
|
|
79
89
|
sandboxPolicy,
|
package/lib/tool.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* batch edits, creation, deletion and renames.
|
|
8
8
|
* @module dsh-patch-edit-plus/tool
|
|
9
9
|
*/
|
|
10
|
-
import
|
|
10
|
+
import * as dshToolsNamespace from '@deepseek-ai/dsh-tools';
|
|
11
11
|
import { PatchError, codexDisabledError, limitError, unifiedDisabledError } from './errors.js';
|
|
12
12
|
import { detectFormat } from './detect.js';
|
|
13
13
|
import { resolveSandboxPolicy } from './policy.js';
|
|
@@ -24,15 +24,24 @@ const ROUTING_GUIDE = [
|
|
|
24
24
|
'- Do not use apply_patch for trivial one-line single-file edits when the native edit tool is available.',
|
|
25
25
|
].join('\n');
|
|
26
26
|
/**
|
|
27
|
-
|
|
27
|
+
/** Build the (frozen) tool definition. The tool name is passed in resolved
|
|
28
28
|
* form so registration-side rename avoidance can mint variant names.
|
|
29
|
+
* Returns `null` when the host line's dsh-tools lacks `defineTool` — the
|
|
30
|
+
* caller skips registration instead of failing the loader entry.
|
|
29
31
|
*/
|
|
30
32
|
export function buildApplyPatchTool(toolName, ctx, cfg) {
|
|
31
33
|
const styles = [
|
|
32
34
|
cfg.allowUnifiedDiff ? 'git/unified diff (default)' : null,
|
|
33
35
|
cfg.allowCodexPatch ? 'Codex apply_patch syntax' : null,
|
|
34
36
|
].filter(style => style !== null).join(' and ');
|
|
35
|
-
const
|
|
37
|
+
const dshToolsSoft = dshToolsNamespace;
|
|
38
|
+
const defineToolImpl = dshToolsSoft.defineTool
|
|
39
|
+
?? dshToolsSoft.default?.defineTool;
|
|
40
|
+
if (defineToolImpl === undefined) {
|
|
41
|
+
console.warn('[dsh-patch-edit-plus] @deepseek-ai/dsh-tools has no defineTool export on this host line; the apply_patch tool is not available.');
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
const definition = defineToolImpl({
|
|
36
45
|
name: toolName,
|
|
37
46
|
description: `Apply a patch-style change to one or more text files, all-or-nothing: every hunk is verified against current file content first, and nothing is written unless the whole patch applies. Accepts ${styles}. `
|
|
38
47
|
+ 'Add, update (multi-hunk), delete and move/rename operations are supported. Pass dryRun: true to validate without writing.\n' + ROUTING_GUIDE,
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { JSX } from 'react';
|
|
2
|
+
/** 本插件消费的 configForms 值形状(与 host ResolvedConfig 的易变布尔对齐)。 */
|
|
3
|
+
export interface PatchEditPlusConfig {
|
|
4
|
+
allowUnifiedDiff: boolean;
|
|
5
|
+
allowCodexPatch: boolean;
|
|
6
|
+
}
|
|
7
|
+
/** configForms 作用域面(结构性子集;0.1.7+ 缝,避免硬 peer 类型)。 */
|
|
8
|
+
export interface PatchFormScope {
|
|
9
|
+
getSnapshot(): {
|
|
10
|
+
status: 'loading' | 'ready' | 'unavailable';
|
|
11
|
+
value: Partial<PatchEditPlusConfig> | undefined;
|
|
12
|
+
writable: boolean;
|
|
13
|
+
};
|
|
14
|
+
subscribe(listener: () => void): () => void;
|
|
15
|
+
set(field: string, value: unknown): Promise<boolean>;
|
|
16
|
+
}
|
|
17
|
+
/** 客户端所需服务:slots 全世代都有,准声明;configForms 是 0.1.7+ 服务,
|
|
18
|
+
* **不准**进顶层 inject——在老宿主上声明缺失服务会把 client entry 永久挂起
|
|
19
|
+
* (web boot 拒渲染整树,「1 entry did not activate」家族教训)。configForms
|
|
20
|
+
* 走软读(strict proxy 直读 throw → try/catch 落 ctx.get),缺席时整卡隐藏。 */
|
|
21
|
+
export declare const inject: string[];
|
|
22
|
+
/** 轻量 ctx 类型(仅本客户端用到的方法;构建时类型被剥离)。 */
|
|
23
|
+
interface SlotsFace {
|
|
24
|
+
inject: (_name: string, _fn: () => unknown) => () => void;
|
|
25
|
+
register: (_options: Record<string, unknown>, _component: unknown) => () => void;
|
|
26
|
+
}
|
|
27
|
+
interface ClientCtx {
|
|
28
|
+
slots: SlotsFace;
|
|
29
|
+
get?: (_name: string) => unknown;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* 插件页配置卡主体:两行补丁风格开关。
|
|
33
|
+
* - status === 'unavailable'(无 configForms / entry 未装配)→ 整卡隐藏(null);
|
|
34
|
+
* - status === 'loading' → 开关禁用;
|
|
35
|
+
* - 仅 snapshot.writable 时可写(写路径 `form.set(field, !cur)`)。
|
|
36
|
+
*/
|
|
37
|
+
export declare function PatchStyleCard(props: {
|
|
38
|
+
form?: PatchFormScope | undefined;
|
|
39
|
+
}): JSX.Element | null;
|
|
40
|
+
/**
|
|
41
|
+
* 客户端插件体:在 Plugins 页 bundle 详情页的 keyed 席位(key = 包名)下
|
|
42
|
+
* 注册配置卡。卡片自绑 `configForms.get('dsh-patch-edit-plus')`——form 在
|
|
43
|
+
* 渲染期解析(而非 apply 期固化),宿主晚于客户端半开始服务该 entry 时
|
|
44
|
+
* 也能追上;页面注入的 owner props(`{ view: 'page', form }`)不消费,
|
|
45
|
+
* 以本插件 entry 的作用域为唯一事实来源。
|
|
46
|
+
*/
|
|
47
|
+
export declare function apply(ctx: ClientCtx): void;
|
|
48
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generation-compat seams for the single max-range build (0.1.0-rc.8 → 0.2.0).
|
|
3
|
+
*
|
|
4
|
+
* Three old-line traps live here:
|
|
5
|
+
* 1. `.volatile()` — the schema method only exists on the 0.1.7+ schemastery
|
|
6
|
+
* line; calling it blind is a load-time TypeError on older hosts. Condition
|
|
7
|
+
* it with {@link maybeVolatile}.
|
|
8
|
+
* 2. The settings registration API is three generations deep: instance
|
|
9
|
+
* `settings.installSection` (0.1.2/0.1.5, real-host verified), the module
|
|
10
|
+
* `installSettingsSection` export of `@deepseek-ai/dsh-settings`
|
|
11
|
+
* (0.1.0/0.1.1 — verified against the 0.1.0-rc.8 tarball; the
|
|
12
|
+
* dsh-client-ui-settings package is a host-side no-op there), and the
|
|
13
|
+
* declarative `.volatile()` form on 0.1.7+. {@link installSettingsCompat}
|
|
14
|
+
* picks by feature detection, never by version number.
|
|
15
|
+
* 3. Every cross-generation import must ride a namespace soft-import: a static
|
|
16
|
+
* named import of an absent export dies at ESM link time and takes the
|
|
17
|
+
* whole loader entry down.
|
|
18
|
+
* @module dsh-patch-edit-plus/compat
|
|
19
|
+
*/
|
|
20
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
21
|
+
import type { Config as PluginConfig } from './config.js';
|
|
22
|
+
/** Wrap a schema field with `.volatile()` only when the host line supports it. */
|
|
23
|
+
export declare function maybeVolatile<T>(schema: T): T;
|
|
24
|
+
/** Settings service face covering both the 0.1.2+ and legacy registration APIs. */
|
|
25
|
+
export interface SettingsFace {
|
|
26
|
+
installSection?(owner: unknown, namespace: string, schema: unknown, entry: unknown, hooks: {
|
|
27
|
+
setSource(current: () => PluginConfig): void;
|
|
28
|
+
onChange(): void;
|
|
29
|
+
}): unknown;
|
|
30
|
+
register?(namespace: string, schema: unknown, options: {
|
|
31
|
+
base?: unknown;
|
|
32
|
+
}): {
|
|
33
|
+
get(): PluginConfig;
|
|
34
|
+
watch(cb: () => void): void;
|
|
35
|
+
} | undefined;
|
|
36
|
+
}
|
|
37
|
+
export interface SettingsHooks {
|
|
38
|
+
setSource(current: () => PluginConfig): void;
|
|
39
|
+
onChange(): void;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Register the settings section across host generations. 0.1.7+ declarative
|
|
43
|
+
* volatile hosts never call this (their schema fields are already volatile and
|
|
44
|
+
* no registration call is wanted); this is the 0.1.5-and-below path.
|
|
45
|
+
*/
|
|
46
|
+
export declare function installSettingsCompat(ctx: Context, namespace: string, schema: unknown, entry: PluginConfig, hooks: SettingsHooks): void;
|