@wenaixi/dsh-ponytail 5.0.0 → 5.2.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.
Files changed (49) hide show
  1. package/CHANGELOG.md +140 -226
  2. package/README.md +102 -98
  3. package/lib/client.js +303 -140
  4. package/lib/ponytail-config.d.ts +47 -21
  5. package/lib/ponytail-config.d.ts.map +1 -1
  6. package/lib/ponytail-config.js +42 -36
  7. package/lib/ponytail-config.js.map +1 -1
  8. package/lib/ponytail-instructions.d.ts.map +1 -1
  9. package/lib/ponytail-instructions.js +31 -31
  10. package/lib/ponytail-instructions.js.map +1 -1
  11. package/lib/ponytail-priority.d.ts +10 -8
  12. package/lib/ponytail-priority.d.ts.map +1 -1
  13. package/lib/ponytail-priority.js +12 -11
  14. package/lib/ponytail-priority.js.map +1 -1
  15. package/lib/ponytail-remote.d.ts +65 -0
  16. package/lib/ponytail-remote.d.ts.map +1 -0
  17. package/lib/ponytail-remote.js +141 -0
  18. package/lib/ponytail-remote.js.map +1 -0
  19. package/lib/ponytail-settings.d.ts +96 -0
  20. package/lib/ponytail-settings.d.ts.map +1 -0
  21. package/lib/ponytail-settings.js +165 -0
  22. package/lib/ponytail-settings.js.map +1 -0
  23. package/lib/ponytail-skills.d.ts +10 -0
  24. package/lib/ponytail-skills.d.ts.map +1 -1
  25. package/lib/ponytail-skills.js +23 -5
  26. package/lib/ponytail-skills.js.map +1 -1
  27. package/lib/ponytail-state.d.ts +8 -4
  28. package/lib/ponytail-state.d.ts.map +1 -1
  29. package/lib/ponytail-state.js +40 -39
  30. package/lib/ponytail-state.js.map +1 -1
  31. package/lib/ponytail.d.ts +28 -4
  32. package/lib/ponytail.d.ts.map +1 -1
  33. package/lib/ponytail.js +181 -83
  34. package/lib/ponytail.js.map +1 -1
  35. package/locale/en.json +37 -27
  36. package/locale/zh.json +37 -27
  37. package/package.json +3 -1
  38. package/skills/descriptions.en.json +8 -0
  39. package/skills/descriptions.zh.json +8 -0
  40. package/skills/ponytail/SKILL.md +89 -46
  41. package/skills/ponytail-audit/SKILL.md +27 -17
  42. package/skills/ponytail-debt/SKILL.md +27 -14
  43. package/skills/ponytail-gain/SKILL.md +32 -19
  44. package/skills/ponytail-help/SKILL.md +53 -32
  45. package/skills/ponytail-review/SKILL.md +36 -24
  46. package/lib/ponytail-http.d.ts +0 -45
  47. package/lib/ponytail-http.d.ts.map +0 -1
  48. package/lib/ponytail-http.js +0 -162
  49. package/lib/ponytail-http.js.map +0 -1
package/lib/client.js CHANGED
@@ -10,10 +10,10 @@ window.__ModuleLoader__.load({
10
10
  let e = React.createElement;
11
11
 
12
12
  // 官方 locale 接入:注册 ponytail 命名空间双语字典(真源 locale/*.json,构建期内嵌)
13
- // 技能描述不翻译(用户边界):由 server/frontmatter 下发,字典不含技能描述键
13
+ // 技能描述不翻译(用户边界):由 configForms 快照与快照通道下发,字典不含技能描述键
14
14
  var NS = "ponytail";
15
- var ZH = {"meta.title":"懒人模式(ponytail)","meta.description":"DietrichGebert/ponytail 的 DSH 完整移植:常驻懒人 senior 模式与七阶梯子(YAGNI 到最小实现),6 个中文原生技能(本体、评审、审计、债务、收益、帮助),零 tool 注册。","panel.title":"懒人模式配置","mode.title":"运行强度等级","mode.hint":"控制梯子提示词的注入强度,改动即时生效并写入配置文件,对所有会话生效。","mode.lockedHint":"当前有更高优先级的配置在生效,此处的修改不会改变实际运行等级。","mode.off":"关闭","mode.lite":"轻量","mode.full":"标准","mode.ultra":"激进","skills.title":"原生技能开关","skills.hint":"关闭后该技能不会出现在斜杠菜单,也不会被模型加载。","skills.toggleOn":"启用 {name}","skills.enabledToast":"技能 {name} 已启用","skills.hiddenToast":"技能 {name} 已隐藏","chain.hit":"生效中","chain.shadowed":"被覆盖","chain.unset":"未设置","chain.valueUnset":"(未设置)","priority.title":"配置优先级","priority.unavailable":"诊断信息不可用,请确认宿主版本已包含优先级诊断接口。","priority.intro":"运行等级按下列顺序取第一个有效值;被更高优先级压制的项,在界面上修改不会生效。当前生效:","state.loading":"读取中…","state.saved":"配置保存在 DSH 数据目录的 ponytail/config.json","error.operation":"操作失败:","confirm.reset":"确定恢复默认配置吗?等级回到标准(full),并重新启用全部 6 个技能。","button.reset":"恢复默认配置","toast.modeChanged":"运行等级已切为「{name}」","toast.resetDone":"已恢复默认配置","level.env":"环境变量","level.patch":"Profile 补丁","level.config":"用户配置文件","level.fallback":"内置兜底","problem.env":"值无效,已忽略","problem.patch":"值无效,已忽略","problem.config":"文件损坏或字段缺失"};
16
- var EN = {"meta.title":"Ponytail (lazy senior mode)","meta.description":"A full DSH port of DietrichGebert/ponytail: the always-on lazy senior developer mode and its seven-rung ladder (YAGNI down to the minimum implementation), with six native Chinese skills (core, review, audit, debt, gain, help) and zero tool registrations.","panel.title":"Ponytail Config","mode.title":"Intensity Level","mode.hint":"Controls how strongly the ladder prompt is injected. Changes apply immediately and are written to the config file for all sessions.","mode.lockedHint":"A higher-priority configuration is in effect; changes here will not affect the actual runtime level.","mode.off":"Off","mode.lite":"Lite","mode.full":"Standard","mode.ultra":"Ultra","skills.title":"Native Skills","skills.hint":"Disabled skills disappear from the slash menu and are not loaded by the model.","skills.toggleOn":"Enable {name}","skills.enabledToast":"Skill {name} enabled","skills.hiddenToast":"Skill {name} hidden","chain.hit":"In effect","chain.shadowed":"Shadowed","chain.unset":"Unset","chain.valueUnset":"(unset)","priority.title":"Config Priority","priority.unavailable":"Diagnostics unavailable. Please confirm the host version ships the priority diagnostics endpoint.","priority.intro":"The runtime level takes the first valid value in the following order; items shadowed by a higher-priority source cannot be changed from the UI. Current: ","state.loading":"Loading…","state.saved":"Config is stored in ponytail/config.json under the DSH data directory","error.operation":"Operation failed: ","confirm.reset":"Restore default config? Level returns to Standard (full) and all 6 skills are re-enabled.","button.reset":"Restore defaults","toast.modeChanged":"Level switched to {name}","toast.resetDone":"Defaults restored","level.env":"Environment variable","level.patch":"Profile patch","level.config":"User config file","level.fallback":"Built-in fallback","problem.env":"Invalid value, ignored","problem.patch":"Invalid value, ignored","problem.config":"File corrupted or field missing"};
15
+ var ZH = {"button.reset":"恢复默认配置","chain.hit":"生效中","chain.shadowed":"被覆盖","chain.unset":"未设置","chain.valueUnset":"(未设置)","confirm.reset":"确定恢复默认配置吗?等级回到标准(full),并重新启用全部 6 个技能。","error.operation":"操作失败:","error.rejected":"本部署没有接受这次修改,值已保留供你核对。","lang.changed":"技能描述语言已切为「{name}」","lang.en":"English","lang.title":"技能描述语言","lang.zh":"中文","level.env":"环境变量","level.fallback":"内置兜底","level.patch":"Profile 补丁","meta":{"title":"懒人模式","description":"常驻懒人资深开发者模式与七阶梯子(YAGNI 到最小实现),6 个中文原生技能(本体、评审、审计、债务、收益、帮助),零 Tool 注册。"},"meta.description":"DietrichGebert/ponytail 的 DSH 完整移植:常驻懒人 senior 模式与七阶梯子(YAGNI 到最小实现),6 个中文原生技能(本体、评审、审计、债务、收益、帮助),零 tool 注册。","meta.title":"懒人模式(ponytail)","mode.full":"标准","mode.hint":"控制梯子提示词的注入强度,改动即时生效并写入配置文件,对所有会话生效。","mode.lite":"轻量","mode.lockedHint":"当前有更高优先级的配置在生效,此处的修改不会改变实际运行等级。","mode.off":"关闭","mode.title":"运行强度等级","mode.ultra":"激进","mode.unset":"未设置","mode.unsetHint":"Profile 补丁未配置运行等级,当前由内置兜底决定。点上方任一档位即写入补丁。","panel.title":"懒人模式配置","priority.intro":"运行等级按下列顺序取第一个有效值;被更高优先级压制的项,在界面上修改不会生效。当前生效:","priority.title":"配置优先级","priority.unavailable":"诊断信息不可用,请确认宿主版本已包含优先级诊断接口。","problem.env":"值无效,已忽略","problem.patch":"值无效,已忽略","skills.enabledToast":"技能 {name} 已启用","skills.hiddenToast":"技能 {name} 已隐藏","skills.hint":"关闭后该技能不会出现在斜杠菜单,也不会被模型加载。","skills.title":"原生技能开关","skills.toggleOn":"启用 {name}","state.loading":"读取中…","state.readOnly":"本部署的设置为只读。","state.saved":"配置保存在当前 profile 的 ponytail 目录","toast.modeChanged":"运行等级已切为「{name}」","toast.resetDone":"已恢复默认配置"};
16
+ var EN = {"button.reset":"Restore defaults","chain.hit":"In effect","chain.shadowed":"Shadowed","chain.unset":"Unset","chain.valueUnset":"(unset)","confirm.reset":"Restore default config? Level returns to Standard (full) and all 6 skills are re-enabled.","error.operation":"Operation failed: ","error.rejected":"This deployment did not accept the change; your value was kept for you to review.","lang.changed":"Skill description language switched to {name}","lang.en":"English","lang.title":"Skill description language","lang.zh":"中文","level.env":"Environment variable","level.fallback":"Built-in fallback","level.patch":"Profile patch","meta":{"title":"Lazy Senior Dev Mode","description":"Persistent lazy senior developer mode with a seven-rung ladder (YAGNI down to the smallest implementation), six native skills, and zero tool registration."},"meta.description":"A full DSH port of DietrichGebert/ponytail: the always-on lazy senior developer mode and its seven-rung ladder (YAGNI down to the minimum implementation), with six native Chinese skills (core, review, audit, debt, gain, help) and zero tool registrations.","meta.title":"Ponytail (lazy senior mode)","mode.full":"Standard","mode.hint":"Controls how strongly the ladder prompt is injected. Changes apply immediately and are written to the config file for all sessions.","mode.lite":"Lite","mode.lockedHint":"A higher-priority configuration is in effect; changes here will not affect the actual runtime level.","mode.off":"Off","mode.title":"Intensity Level","mode.ultra":"Ultra","mode.unset":"Unset","mode.unsetHint":"The profile patch has no intensity level; the built-in fallback decides it now. Picking any level above writes it to the patch.","panel.title":"Lazy Senior Dev Mode settings","priority.intro":"The runtime level takes the first valid value in the following order; items shadowed by a higher-priority source cannot be changed from the UI. Current: ","priority.title":"Config Priority","priority.unavailable":"Diagnostics unavailable. Please confirm the host version ships the priority diagnostics endpoint.","problem.env":"Invalid value, ignored","problem.patch":"Invalid value, ignored","skills.enabledToast":"Skill {name} enabled","skills.hiddenToast":"Skill {name} hidden","skills.hint":"Disabled skills disappear from the slash menu and are not loaded by the model.","skills.title":"Native Skills","skills.toggleOn":"Enable {name}","state.loading":"Loading…","state.readOnly":"This deployment stores settings read-only.","state.saved":"Config is stored in the ponytail directory of the current profile","toast.modeChanged":"Level switched to {name}","toast.resetDone":"Defaults restored"};
17
17
  // t 在 apply 时经 ctx.locale.bind 赋值;组件读取 factory 级变量(不接收 ctx,铁律)
18
18
  var localeCtx = null;
19
19
  var t = function (key, params) {
@@ -63,8 +63,8 @@ window.__ModuleLoader__.load({
63
63
  display: "flex",
64
64
  flexDirection: "column",
65
65
  gap: "2px",
66
- minWidth: "0",
67
- flex: "1",
66
+ minWidth: 0,
67
+ flex: 1,
68
68
  },
69
69
  chainName: {
70
70
  fontSize: "13px",
@@ -87,8 +87,8 @@ window.__ModuleLoader__.load({
87
87
  display: "flex",
88
88
  flexDirection: "column",
89
89
  gap: "2px",
90
- minWidth: "0",
91
- flex: "1",
90
+ minWidth: 0,
91
+ flex: 1,
92
92
  },
93
93
  rowName: {
94
94
  fontSize: "13px",
@@ -110,26 +110,29 @@ window.__ModuleLoader__.load({
110
110
  },
111
111
  };
112
112
 
113
- const MODE_OPTIONS = [
114
- { value: "off", label: t("mode.off") },
115
- { value: "lite", label: t("mode.lite") },
116
- { value: "full", label: t("mode.full") },
117
- { value: "ultra", label: t("mode.ultra") },
118
- ];
119
-
120
-
121
- // 技能元数据从 SKILL.md frontmatter 构建期提取(唯一真源,与宿侧 readSkillMeta 同源)
122
- const SKILL_META = [{"id":"ponytail","description":"强制使用最懒但可用的解法,追求最简、最短、最精。像一位见过一切的老手:先质疑需求是否该存在(YAGNI),优先复用标准库、平台原生能力,一行能解决就不用五十行。支持 lite/full(默认)/ultra 三档强度。适用于任何编码任务:编写、新增、重构、修复、评审、设计代码,以及选型依赖。触发词:ponytail / 偷懒 / 懒人模式 / 最简解法 / 最小解法 / yagni / 少做一点 / 最短路径 / 讨厌过度设计、臃肿、样板代码、没必要的依赖时也请使用。非编码请求(常识、文案、翻译、总结、菜谱)请勿使用。\n"},{"id":"ponytail-audit","description":"全仓过度设计审计,类似 ponytail-review,但扫描的是整个代码库而非 diff:按可删行数排序,列出能删、能简化、能用标准库/原生替代的地方。触发词:审计代码库 / 审计过度设计 / 这个仓库能删什么 / 找臃肿 / ponytail-audit / /ponytail-audit。一次性报告,不直接改代码。\n"},{"id":"ponytail-debt","description":"把代码库中所有 `ponytail:` 注释收割为债务台账,让 ponytail 有意留下的捷径和延期不会悄悄烂成「以后再说就是永远不做」。触发词:ponytail debt / /ponytail-debt / ponytail 延期了什么 / 列出捷径 / ponytail 台账 / 标记了什么待做。一次性报告,不改代码。\n"},{"id":"ponytail-gain","description":"以精简看板展示 ponytail 的实测收益:更少代码、更低成本、更快速度,数据来自 benchmark 中位数。一次性展示,非持久模式,也非本仓实时统计。触发词:/ponytail-gain / ponytail gain / ponytail 能省多少 / 展示 ponytail 收益 / ponytail 看板。\n"},{"id":"ponytail-help","description":"ponytail 全量模式、技能与命令的速查卡,一次性展示,非持久模式。触发词:/ponytail-help / ponytail help / ponytail 有哪些命令 / 怎么用 ponytail。\n"},{"id":"ponytail-review","description":"专挑过度设计的代码评审,只找能删的东西:重复造的标准库、没必要的依赖、臆想的抽象、闲置的灵活性。每条发现一行写完:位置、该删什么、用什么替代。触发词:过度设计评审 / 能删什么 / 是否过度设计 / 简化评审 / ponytail-review / /ponytail-review。配合面向正确性的评审使用,本技能只猎复杂度。\n"}];
113
+ // 档位标签在 apply 时才绑定 locale(t 是 factory 级可变变量),因此按需取值而不是模块级求值。
114
+ //
115
+ // configured=false(补丁未写 defaultMode)时末尾追加一段真实的「未设置」选项,而不是把 value
116
+ // 置成一个不在 options 里的值:官方 SegmentedControl 用 options.findIndex 求下标,
117
+ // 下标 -1 会让所有段 tabIndex=-1(键盘不可达)且指示器整颗滑出轨道左缘
118
+ // (dsh-client-ui-primitives/lib/index.js:3410-3492 与 SegmentedControl.module.css)。
119
+ // 「未设置」排在末尾而非首位:未配置态下 关闭/轻量/标准/激进 仍占 0-3 段,
120
+ // 配好之后四档位置不变,用户已有的肌肉记忆不被打断。
121
+ function modeOptions(configured) {
122
+ var options = [
123
+ { value: "off", label: t("mode.off") },
124
+ { value: "lite", label: t("mode.lite") },
125
+ { value: "full", label: t("mode.full") },
126
+ { value: "ultra", label: t("mode.ultra") },
127
+ ];
128
+ if (!configured) options.push({ value: "unset", label: t("mode.unset") });
129
+ return options;
130
+ }
123
131
 
124
- const EMPTY_CONFIG = {
125
- currentMode: "full",
126
- defaultMode: "full",
127
- disabledSkills: [],
128
- skills: SKILL_META.map(function (s) {
129
- return { id: s.id, name: s.id, description: s.description, enabled: true };
130
- }),
131
- priority: null,
132
- };
132
+ // 技能 id 列表是构建期常量;描述与语言来自 remote 快照(与模型目录同源)。
133
+ // 构建期内嵌描述会让语言切换停在旧值,所以这里只带 id。
134
+ // 「是否启用」不再随服务端下发:它就是 configForms 快照里的 disabledSkills 取反。
135
+ const SKILL_IDS = ["ponytail","ponytail-audit","ponytail-debt","ponytail-gain","ponytail-help","ponytail-review"];
133
136
 
134
137
  // 诊断链一行的状态语义:生效 / 被覆盖 / 未设置 / 有问题
135
138
  function chainRowState(source) {
@@ -192,141 +195,250 @@ window.__ModuleLoader__.load({
192
195
  );
193
196
  }
194
197
 
195
- function PonytailConfigPanel() {
196
- const [state, setState] = React.useState({
197
- loading: true,
198
- saving: false,
199
- error: null,
200
- toast: null,
201
- config: EMPTY_CONFIG,
202
- });
198
+ /**
199
+ * 只读快照的最小 store:{ status, value },够 React 的 useSyncExternalStore 订阅。
200
+ * 不引第三方 store 库——快照就一个对象,一个 Set 监听者 + 一个 getSnapshot 足够。
201
+ */
202
+ function createStore(initial) {
203
+ var listeners = new Set();
204
+ var current = initial;
205
+ return {
206
+ getSnapshot: function () { return current; },
207
+ subscribe: function (listener) {
208
+ listeners.add(listener);
209
+ return function () { listeners.delete(listener); };
210
+ },
211
+ set: function (next) {
212
+ current = next;
213
+ listeners.forEach(function (listener) { listener(); });
214
+ },
215
+ };
216
+ }
203
217
 
204
- const applyResult = React.useCallback(function (data, message) {
205
- setState(function (prev) {
206
- return Object.assign({}, prev, {
207
- loading: false,
208
- saving: false,
209
- error: null,
210
- config: data && Array.isArray(data.skills) ? data : prev.config,
211
- toast: message,
212
- });
213
- });
214
- if (message) {
215
- setTimeout(function () {
216
- setState(function (prev) {
217
- return Object.assign({}, prev, { toast: null });
218
- });
219
- }, 2600);
218
+ /**
219
+ * 本插件在浏览器侧向官方网关声明的 Remote 贡献。
220
+ *
221
+ * 为什么必须显式声明:网关的命名空间不是按需自动开通的。浏览器侧装配时
222
+ * dsh-api-remotes 只遍历一份编译期写死的官方贡献清单并逐个 ctx.remote.$mount()
223
+ *(dsh-api-remotes/lib/client.js:13512-13538,25 项,不含本插件)。
224
+ * 宿主侧 TypertRemoteService 只负责把端点暴露出去;客户端不 $mount,
225
+ * remote.ponytail 就永远不会出现——面板因此永久停在「诊断信息不可用」,
226
+ * 而宿主完全正常。写法参照官方第三方插件 dsh-experimental-client-ui-voice-input。
227
+ *
228
+ * result.create 是网关不校验的钩子(requireStrictCodec 只看 mode === "strict",
229
+ * 见 dsh-api-gateway/lib/client.js:2073),解码走 result.decode,缺省即原样透传
230
+ * (同文件 1801 行)。因此返回恒等函数即可把服务端 JSON 原样送进面板,
231
+ * 不必为此引入 zod。
232
+ */
233
+ var PONYTAIL_REMOTE = {
234
+ package: "@wenaixi/dsh-ponytail",
235
+ descriptors: [
236
+ {
237
+ id: "@wenaixi/dsh-ponytail#ponytailRemote/snapshot",
238
+ service: "ponytailRemote",
239
+ namespace: "ponytail",
240
+ method: "snapshot",
241
+ invocation: { kind: "direct" },
242
+ parameters: [],
243
+ result: {
244
+ mode: "strict",
245
+ typeSymbol: "@wenaixi/dsh-ponytail/types#PonytailSnapshot",
246
+ create: function (value) { return value; },
247
+ },
248
+ sourceLocation: {
249
+ file: "src/ponytail-remote.ts",
250
+ line: 1,
251
+ column: 1,
252
+ },
253
+ },
254
+ ],
255
+ };
256
+ /** 挂载失败时的快照 store:面板按 status 降级为不显示该段。 */
257
+ function createUnavailableStore() {
258
+ var store = createStore({ status: "unavailable", value: null });
259
+ store.reload = function () {};
260
+ store.dispose = function () {};
261
+ return store;
262
+ }
263
+
264
+ /**
265
+ * 读取本插件已 $mount 的命名空间 snapshot,作为只读推导值的来源。
266
+ *
267
+ * 不再有轮询:命名空间由本文件下方的 PONYTAIL_REMOTE 经 ctx.remote.$mount() 挂载,
268
+ * $mount 返回的 disposer 之前挂载一定已经完成(它内部 await fiber,
269
+ * dsh-api-gateway/lib/client.js:1636-1646),因此同步问一次即可。
270
+ * 挂载失败(精简宿主没有 remote 服务)时立即降级为不显示,不再空转——
271
+ * 上一版的 40×50ms 轮询掩盖的是「压根没挂载」这个事实,而不只是时序。
272
+ */
273
+ function createRemoteStore(ctx, namespace) {
274
+ var store = createStore({ status: "loading", value: null });
275
+ var method = "snapshot";
276
+
277
+ var fail = function () {
278
+ store.set({ status: "unavailable", value: null });
279
+ };
280
+
281
+ var load = function () {
282
+ var ns = ctx.get("remote." + namespace);
283
+ if (ns === undefined || ns === null || typeof ns[method] !== "function") {
284
+ fail();
285
+ return;
220
286
  }
221
- }, []);
287
+ Promise.resolve(ns[method]()).then(function (response) {
288
+ if (response && response.ok === true) store.set({ status: "ready", value: response.value });
289
+ else fail();
290
+ }).catch(fail);
291
+ };
292
+ load();
293
+ // 配置表单写入成功后由面板调用:重新拉一次服务端真值(当前等级可能已变)
294
+ store.reload = load;
295
+ store.dispose = function () {};
296
+ return store;
297
+ }
222
298
 
223
- const load = React.useCallback(function () {
224
- fetch("/api/plugins/ponytail/config")
225
- .then(function (res) {
226
- if (!res.ok) throw new Error("HTTP " + res.status);
227
- return res.json();
228
- })
229
- .then(function (data) {
230
- applyResult(data, null);
231
- })
232
- .catch(function (err) {
233
- setState(function (prev) {
234
- return Object.assign({}, prev, { loading: false, error: String(err) });
235
- });
236
- });
237
- }, [applyResult]);
299
+ function PonytailConfigPanel(props) {
300
+ // 官方配置表单的快照:status / value / base / user / revision / writable
301
+ // (dsh-client-ui-settings 的 ConfigFormController,lib/client.js:1117-1131)。
302
+ const snapshot = props.usePonytailConfig(function (s) { return s; });
303
+ const [busy, setBusy] = React.useState(false);
304
+ const [note, setNote] = React.useState(null);
305
+
306
+ // 只读推导值来自远程命名空间;服务端不可用时降级为 null(不显示该段)
307
+ const snapshotRemote = props.usePonytailSnapshot(function (s) { return s; });
308
+ const remote = snapshotRemote.value;
238
309
 
239
- React.useEffect(function () {
240
- load();
241
- // 语言切换后重取(官方 LocaleFace subscribe 语义:字典注册 bump revision)
242
- var off = localeCtx ? localeCtx.subscribe(function () { load(); }) : null;
243
- return function () { if (off) off(); };
244
- }, [load]);
310
+ const writable = snapshot.writable === true && snapshot.status === "ready";
245
311
 
246
- const post = function (body, message) {
247
- setState(function (prev) {
248
- return Object.assign({}, prev, { saving: true, error: null, toast: null });
312
+ // 写入:全部经官方 settings 通道,携带 revision 做冲突检测。
313
+ // 服务端拒绝时 mutate 返回 false,此时以服务端读回的下一份快照为准,
314
+ // 不做本地乐观改值——面板显示的必须是真值。
315
+ const applyOps = React.useCallback(function (ops, message) {
316
+ setBusy(true);
317
+ setNote(null);
318
+ // 写入经 face 注入的官方表单控制器(renderable 的 inject 面会把 hooks 绑成
319
+ // use<Name>,其余键原样作为 props)。此前这里读 props.scope,而 scope 是 apply
320
+ // 闭包里的变量、从未放进 face——点击即 TypeError,界面表现为「改了没反应且卡住」。
321
+ props.mutate(ops, snapshot.revision).then(function (landed) {
322
+ setBusy(false);
323
+ setNote(landed ? message : t("error.rejected"));
324
+ // 档位写入后重新拉一次服务端真值:当前生效等级可能随之改变,
325
+ // 而它不在 configForms 快照里(那是持久化配置,不是运行时推导值)。
326
+ if (landed && props.reloadRemote) props.reloadRemote();
249
327
  });
250
- fetch("/api/plugins/ponytail/config", {
251
- method: "POST",
252
- headers: { "Content-Type": "application/json" },
253
- body: JSON.stringify(body),
254
- })
255
- .then(function (res) {
256
- return res.json();
257
- })
258
- .then(function (data) {
259
- applyResult(data, message);
260
- })
261
- .catch(function (err) {
262
- setState(function (prev) {
263
- return Object.assign({}, prev, { saving: false, error: String(err) });
264
- });
265
- });
266
- };
328
+ }, [props.mutate, snapshot.revision]);
329
+
330
+ const setMode = React.useCallback(function (mode) {
331
+ applyOps([{ op: "set", path: ["defaultMode"], value: mode }], t("toast.modeChanged", { name: mode }));
332
+ }, [applyOps]);
267
333
 
268
- const config = state.config;
269
- // 配置被更高优先级压制时,等级选择器整体禁用,避免用户做无效操作
270
- const levelLocked = Boolean(config.priority && config.priority.chain && config.priority.chain.some(function (s) {
271
- return s.level !== "config" && s.hit && s.level !== "fallback";
334
+ // 技能开关:disabledSkills 是覆盖式数组,每次写入都带上「目标技能的最终启用态」
335
+ const disabled = snapshot.value && Array.isArray(snapshot.value.disabledSkills)
336
+ ? snapshot.value.disabledSkills
337
+ : [];
338
+ const toggleSkill = React.useCallback(function (name, enabled) {
339
+ const next = enabled ? disabled.filter(function (item) { return item !== name; }) : disabled.concat([name]);
340
+ applyOps([{ op: "set", path: ["disabledSkills"], value: next }],
341
+ enabled ? t("skills.enabledToast", { name: name }) : t("skills.hiddenToast", { name: name }));
342
+ }, [applyOps, disabled]);
343
+
344
+ const resetAll = React.useCallback(function () {
345
+ applyOps([
346
+ { op: "unset", path: ["defaultMode"] },
347
+ { op: "unset", path: ["disabledSkills"] },
348
+ { op: "unset", path: ["skillDescriptionLang"] },
349
+ ], t("toast.resetDone"));
350
+ }, [applyOps]);
351
+
352
+ const config = snapshot.value || {};
353
+ const defaultMode = typeof config.defaultMode === "string" ? config.defaultMode : null;
354
+ // 配置值的有无,而不是运行时推导值的有无:defaultMode 无 Schema 默认值
355
+ // (src/ponytail.ts),所以快照里读得到它,就说明用户的 profile 补丁里写了。
356
+ const modeConfigured = defaultMode !== null;
357
+ // 只有环境变量命中才锁:env 压过一切,此时改 patch 确实无效。
358
+ // fallback 命中必须放行——那是「补丁与 env 都没写」的兜底,不是更高优先级的配置,
359
+ // 用户改 patch 立刻生效(此前把 fallback 也当压制源,提示文案在说谎)。
360
+ const levelLocked = Boolean(remote && Array.isArray(remote.priority && remote.priority.chain) && remote.priority.chain.some(function (s) {
361
+ return s.level === "env" && s.hit;
272
362
  }));
273
363
 
364
+ // 描述语言:带 Schema 默认值(src/ponytail.ts),读得到即生效值,无需区分「配过没配过」。
365
+ var descLang = typeof config.skillDescriptionLang === "string" ? config.skillDescriptionLang : "zh";
366
+ var setDescLang = React.useCallback(function (lang) {
367
+ applyOps([{ op: "set", path: ["skillDescriptionLang"], value: lang }],
368
+ t("lang.changed", { name: t("lang." + lang) }));
369
+ }, [applyOps]);
370
+
274
371
  return e(
275
372
  "div",
276
373
  { style: { display: "flex", flexDirection: "column", gap: "16px", maxWidth: "720px" } },
277
374
  e("h3", { style: { fontSize: "15px", fontWeight: "600", margin: "0", color: "var(--dsw-alias-label-primary)" } },
278
375
  t("panel.title")),
279
- e(PrioritySection, { priority: config.priority }),
376
+ e(PrioritySection, { priority: remote ? remote.priority : null }),
280
377
  e(
281
378
  "div",
282
379
  { style: L.section },
283
380
  e("h4", { style: L.title }, t("mode.title")),
284
- e("p", { style: L.hint },
285
- t("mode.hint")),
381
+ e("p", { style: L.hint }, t("mode.hint")),
286
382
  e(P.SegmentedControl, {
287
383
  id: "ponytail-mode",
288
384
  label: t("mode.title"),
289
- value: config.defaultMode,
290
- options: MODE_OPTIONS,
291
- disabled: state.saving || levelLocked,
292
- onChange: function (next) {
293
- post({ mode: next }, t("toast.modeChanged", { name: next }));
294
- },
385
+ // 控件只显示配置值,绝不回退到 priority.effective:那是运行时推导档
386
+ // (env / 补丁 / 兜底合并的结果),回退会让刚安装的用户看到「标准」被高亮,
387
+ // 像亲手选过一样。未配置就如实显示「未设置」段。
388
+ // 「当前生效」由上方优先级链表独占呈现,这里不重复。
389
+ value: modeConfigured ? defaultMode : "unset",
390
+ options: modeOptions(modeConfigured),
391
+ disabled: busy || !writable || levelLocked,
392
+ onChange: setMode,
295
393
  }),
394
+ !modeConfigured
395
+ ? e("p", { style: L.hint }, t("mode.unsetHint"))
396
+ : null,
296
397
  levelLocked
297
- ? e("p", { style: Object.assign({}, L.hint, { color: "var(--dsw-alias-state-warn-primary)" }) },
298
- t("mode.lockedHint"))
398
+ ? e("p", { style: Object.assign({}, L.hint, { color: "var(--dsw-alias-state-warn-primary)" }) }, t("mode.lockedHint"))
399
+ : null,
400
+ !writable && snapshot.status === "ready"
401
+ ? e("p", { style: Object.assign({}, L.hint, { color: "var(--dsw-alias-state-warn-primary)" }) }, t("state.readOnly"))
299
402
  : null
300
403
  ),
301
404
  e(
302
405
  "div",
303
406
  { style: L.section },
304
407
  e("h4", { style: L.title }, t("skills.title")),
305
- e("p", { style: L.hint },
306
- t("skills.hint")),
408
+ e("p", { style: L.hint }, t("skills.hint")),
409
+ e(P.SegmentedControl, {
410
+ id: "ponytail-desc-lang",
411
+ label: t("lang.title"),
412
+ value: descLang,
413
+ options: [
414
+ { value: "zh", label: t("lang.zh") },
415
+ { value: "en", label: t("lang.en") },
416
+ ],
417
+ disabled: busy || !writable,
418
+ onChange: setDescLang,
419
+ }),
307
420
  e(
308
421
  "div",
309
422
  { style: { display: "flex", flexDirection: "column", gap: "6px" } },
310
- config.skills.map(function (skill) {
423
+ SKILL_IDS.map(function (id) {
424
+ const enabled = disabled.indexOf(id) < 0;
425
+ var meta = (remote && Array.isArray(remote.skills)
426
+ ? remote.skills.find(function (s) { return s.id === id; })
427
+ : null);
311
428
  return e(
312
429
  "div",
313
- { key: skill.id, style: L.row },
430
+ { key: id, style: L.row },
314
431
  e(
315
432
  "div",
316
433
  { style: L.rowText },
317
- e("span", { style: L.rowName }, "/" + skill.name),
318
- e("span", { style: L.rowDesc }, skill.description)
434
+ e("span", { style: L.rowName }, "/" + id),
435
+ e("span", { style: L.rowDesc }, meta ? meta.description : id)
319
436
  ),
320
437
  e(P.Switch, {
321
- checked: skill.enabled,
322
- disabled: state.saving,
323
- label: t("skills.toggleOn", { name: skill.name }),
324
- onChange: function (next) {
325
- post(
326
- { toggleSkill: { name: skill.id, enabled: next } },
327
- t(next ? "skills.enabledToast" : "skills.hiddenToast", { name: skill.name })
328
- );
329
- },
438
+ checked: enabled,
439
+ disabled: busy || !writable,
440
+ label: t("skills.toggleOn", { name: id }),
441
+ onChange: function (next) { toggleSkill(id, next); },
330
442
  })
331
443
  );
332
444
  })
@@ -336,27 +448,40 @@ window.__ModuleLoader__.load({
336
448
  "div",
337
449
  { style: Object.assign({}, L.footer, L.last) },
338
450
  e("span", { style: L.hint },
339
- state.error
340
- ? t("error.operation") + state.error
341
- : state.toast
342
- ? state.toast
343
- : state.loading
344
- ? t("state.loading")
345
- : t("state.saved")),
451
+ note !== null
452
+ ? note
453
+ : snapshot.status === "loading"
454
+ ? t("state.loading")
455
+ : t("state.saved")),
346
456
  e(P.Button, {
347
457
  variant: "outline",
348
458
  size: "sm",
349
- disabled: state.saving,
459
+ disabled: busy || !writable,
350
460
  onClick: function () {
351
461
  if (!confirm(t("confirm.reset"))) return;
352
- post({ action: "reset" }, t("toast.resetDone"));
462
+ resetAll();
353
463
  },
354
464
  }, t("button.reset"))
355
465
  )
356
466
  );
357
467
  }
358
468
 
359
- function apply(ctx) {
469
+ async function apply(ctx) {
470
+ // 先把只读推导值的端点挂到官方网关上:命名空间不会自动出现,
471
+ // 必须由客户端显式声明并 $mount。$mount 返回的 disposer 即 effect 的清理函数。
472
+ // 挂载失败(部署没有 remote 服务)不阻断其余能力,卡片照常出现,只是优先级段不显示。
473
+ var mounted = null;
474
+ try {
475
+ mounted = await ctx.remote.$mount(PONYTAIL_REMOTE);
476
+ } catch (error) {
477
+ if (ctx.logger) ctx.logger.warn("[ponytail] remote 贡献挂载失败(优先级诊断段将不显示): " + String(error));
478
+ }
479
+ ctx.effect(function () {
480
+ return function () {
481
+ if (mounted) void mounted();
482
+ };
483
+ }, "ponytail: remote contribution");
484
+
360
485
  // 官方 locale:注册 ponytail 命名空间双语字典并绑定 t(真源 locale/*.json 构建期内嵌)
361
486
  ctx.effect(function () {
362
487
  return ctx.locale.register(NS, { zh: ZH, en: EN });
@@ -364,17 +489,55 @@ window.__ModuleLoader__.load({
364
489
  localeCtx = ctx.locale;
365
490
  t = ctx.locale.bind(NS);
366
491
 
367
- // 插件卡片详情页:内联配置面板(keyed by 包名,已核实宿主 listBundles 的 name 即包名)
368
- ctx.slots.inject("plugins.bundle.config", function () {
369
- return ctx.slots.register(
370
- { name: "plugins.bundle.config", key: "@wenaixi/dsh-ponytail" },
371
- PonytailConfigPanel
372
- );
373
- });
492
+ // 官方配置表单:宿主 settings 服务投影出的 ponytail 命名空间
493
+ var scope = ctx.configForms.get("ponytail");
494
+
495
+ // 只读推导值:官方 Typert 通道(ctx.remote.ponytail.snapshot())。
496
+ // 远程方法返回的是 { ok, value } / { ok:false, error } 信封(网关 client 侧 lib/client.js:1795-1802),
497
+ // 这里自己拆包并折成一个最小快照 store:命名空间不可用或调用失败时停在 unavailable,
498
+ // 优先级段降级为不显示,不影响其余控件。此处已在上面的 $mount 完成后才读,无需重试。
499
+ var remoteStore = mounted === null
500
+ ? createUnavailableStore()
501
+ : createRemoteStore(ctx, "ponytail");
502
+
503
+ // 插槽注册沿用 whileServed:命名空间没被宿主服务时卡片整体不出现,
504
+ // 部署若从未组合 settings 服务,页面上不留本插件的痕迹
505
+ // (与官方 dsh-client-ui-settings-shell 同一生命周期协议,ui-settings-shell/lib/client.js:182)。
506
+ //
507
+ // 数据面经 inject 提供:组件只从 props 消费,不触碰 ctx(渲染器铁律)。
508
+ // usePonytailConfig 读官方配置表单快照;usePonytailSnapshot 读远程快照;
509
+ // reloadRemote 在写入后让面板重新拉服务端真值。
510
+ var formStore = {
511
+ getSnapshot: function () { return scope.getSnapshot(); },
512
+ subscribe: function (listener) { return scope.subscribe(listener); },
513
+ };
514
+ var face = function () {
515
+ return {
516
+ hooks: {
517
+ ponytailConfig: formStore,
518
+ ponytailSnapshot: remoteStore,
519
+ },
520
+ // 直接透传官方表单控制器的方法:它自带写队列串行化、revision 冲突恢复
521
+ // 与镜像折入(dsh-client-ui-settings 的 ConfigFormController.mutate,lib/client.js:1177-1194)
522
+ mutate: function (ops, revision) { return scope.mutate(ops, revision); },
523
+ reloadRemote: function () { remoteStore.reload(); },
524
+ };
525
+ };
526
+
527
+ ctx.effect(function () {
528
+ return ctx.configForms.whileServed(["ponytail"], function () {
529
+ return ctx.slots.inject("plugins.bundle.config", function () {
530
+ return ctx.slots.register(
531
+ { name: "plugins.bundle.config", key: "@wenaixi/dsh-ponytail", locale: NS, inject: face },
532
+ PonytailConfigPanel
533
+ );
534
+ });
535
+ });
536
+ }, "ponytail: config card");
374
537
  }
375
538
 
376
539
  exports.apply = apply;
377
- exports.inject = ["slots", "locale"];
540
+ exports.inject = ["slots", "locale", "configForms", "remote"];
378
541
  return module.exports;
379
542
  }
380
543
  });