dsh-paoding 0.2.2 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +27 -32
  2. package/README_en.md +27 -32
  3. package/bin/dsh-paoding.mjs +73 -11
  4. package/docs/architecture.md +41 -34
  5. package/docs/architecture_en.md +65 -42
  6. package/docs/configuration.md +159 -65
  7. package/docs/configuration_en.md +161 -66
  8. package/docs/images/1.png +0 -0
  9. package/docs/installation.md +100 -237
  10. package/docs/installation_en.md +112 -252
  11. package/docs/orchestration.md +34 -38
  12. package/docs/orchestration_en.md +34 -38
  13. package/package.json +8 -5
  14. package/plugins/paoding-config-ui/api-core.mjs +215 -18
  15. package/plugins/paoding-config-ui/index.mjs +414 -154
  16. package/plugins/paoding-config-ui/lib/base.css +1083 -42
  17. package/plugins/paoding-config-ui/lib/client.js +2266 -252
  18. package/plugins/paoding-config-ui/package.json +2 -2
  19. package/plugins/paoding-config-ui/runtime-inventory.mjs +27 -26
  20. package/presets/orchestrator/agent.cordis.yml +19 -9
  21. package/presets/orchestrator/preset.yml +3 -2
  22. package/presets/orchestrator/restrict.mjs +2 -1
  23. package/tools/install.mjs +9 -4
  24. package/tools/lib/alloc.mjs +60 -29
  25. package/tools/lib/cli.mjs +127 -278
  26. package/tools/lib/compose.mjs +261 -95
  27. package/tools/lib/config.mjs +266 -86
  28. package/tools/lib/host.mjs +55 -24
  29. package/tools/lib/skills.mjs +72 -45
  30. package/tools/lib/state.mjs +172 -40
  31. package/tools/lib/upgrade.mjs +170 -0
  32. package/tools/lib/util.mjs +7 -0
  33. package/tools/lib/version.mjs +233 -0
  34. package/tools/lib/workspaces.mjs +74 -0
  35. package/tools/lib/yaml.mjs +92 -15
  36. package/docs/images/2.png +0 -0
  37. package/docs/images/3.png +0 -0
  38. package/docs/images/4.png +0 -0
  39. package/docs/images/5.png +0 -0
  40. package/docs/images/6.png +0 -0
  41. package/install.sh +0 -31
  42. package/tools/lib/wizard.mjs +0 -516
@@ -10,9 +10,11 @@
10
10
  * - factory 内 require() 只解析平台 seed 模块(react / cordis /
11
11
  * ui-slots / ui-primitives 与已物化模块);本 bundle 依赖 react 与
12
12
  * @deepseek-ai/dsh-client-ui-primitives。
13
- * - 挂载点:设置页 settings.section 槽位(list slot,id: "paoding"),
14
- * ctx.slots.inject() 等待设置壳声明后 register;id/order/label 驱动导航,
15
- * 与 ui-settings-general 的 general 分区同款模式(参见该包 client.js)。
13
+ * - 挂载点:左侧栏底部动作位入口(sidebar.footer.action 键控槽,与插件
14
+ * 广场 cordis 徽章同槽,设置行上方)+ 会话中栏全页 overlay
15
+ * (data-dsh-paoding-view / html[data-dsh-paoding-active]);
16
+ * 与 mnemon / taskboard / ssh 全页面板互斥(dsh-panel-activate 事件 +
17
+ * html active 属性观察),点侧栏会话行等上下文自动关闭。
16
18
  * - UI:按 DSH 设置壳原生观感重绘(dsh-better-sidebar 的 SideCardSection
17
19
  * 配方)——分组容器卡 + 设计令牌(--dsw-alias-*,运行时注入,浅/深色
18
20
  * 主题经 body[data-ds-dark-theme] 自动跟随)。样式正文独立在 lib/base.css,
@@ -71,7 +73,9 @@ window.__ModuleLoader__.load({
71
73
  h("span", { className: "omd-statusText" }, text));
72
74
  }
73
75
 
74
- function toolItem(tool, tag, checked, disabled, onChange, key) {
76
+ // projBadge(可选):项目级技能角标文案(如「项目」),仅技能项在
77
+ // 工作区视角下、该技能来自工作区项目根时传入;其余网格不传不渲染。
78
+ function toolItem(tool, tag, checked, disabled, onChange, key, projBadge) {
75
79
  return h("label", { key: key, className: "omd-toolItem", title: tool },
76
80
  h("input", {
77
81
  type: "checkbox",
@@ -82,7 +86,8 @@ window.__ModuleLoader__.load({
82
86
  onChange: onChange,
83
87
  }),
84
88
  h("span", { className: "omd-toolName" }, tool),
85
- tag ? h("span", { className: tag === "host" ? "omd-toolTag omd-toolTagHost" : "omd-toolTag" }, tag) : null);
89
+ tag ? h("span", { className: "omd-toolTag" + (tag === "mcp" || tag === "plugin" ? " omd-toolTagHost" : "") }, tag) : null,
90
+ projBadge ? h("span", { className: "omd-skillProj" }, projBadge) : null);
86
91
  }
87
92
 
88
93
  var DEFAULT_ROLES = ["search_external", "design", "implement"];
@@ -107,77 +112,235 @@ window.__ModuleLoader__.load({
107
112
  return roleStringRef(baseRoles, n, "name");
108
113
  }
109
114
 
110
- var INTRO = "可视化编排 dsh-paoding(庖丁解牛)预设:为主 agent 与子 agent 分配 Host 工具与技能,预览并应用生成的 agent.cordis.yml(修改后先预览,确认无误再应用)。";
115
+ // ── 会话模式(roles.<toolName>.background_mode)─────────────────────────
116
+ // 定稿键名 'one-shot' | 'continuable',缺省 one-shot。与后端序列化形状
117
+ // 一致:config 层 normalizeBackgroundMode 恒收成这两个字面量(缺省/非法
118
+ // 回落 'one-shot',落盘恒写出),assignments 与回填照搬同款取值。
119
+ var ROLE_BG_CONTINUABLE = "continuable";
120
+ var ROLE_BG_ONESHOT = "one-shot";
111
121
 
112
- function hostTool(name) {
113
- // host 工具 = MCP 暴露的工具(mcp__<server>__<tool>)。memory_search
114
- // 旧归类已删:magic-memory 停用、mnemon 现在注册 mnemon_*(本函数只做
115
- // 展示分组,不影响识别/分配)。
116
- return name.indexOf("mcp__") === 0;
122
+ // 角色条目 background_mode 透传归一:恰好等于 'continuable' 才认,其余
123
+ //(缺省 / null / 手编误值)一律 'one-shot'(与 normalizeBackgroundMode 同口径)。
124
+ function roleBgModeOf(baseRoles, n) {
125
+ var r = (baseRoles || {})[n];
126
+ return r && r.background_mode === ROLE_BG_CONTINUABLE ? ROLE_BG_CONTINUABLE : ROLE_BG_ONESHOT;
117
127
  }
118
128
 
119
- // 插件 chips:识别保持通用(pluginReports + 未覆盖的 runtimePlugins 全量并入),
120
- // 这里只做【纯展示】分组,绝不反向过滤识别结果:
121
- // agent → 主行(工具型/agent 插件:dsh-mnemon、@hyzyn/dsh-codegraph,以及
122
- // 文件白名单命中的工具插件)
123
- // other → 次要行(市场/纯界面插件:dshmarket、dsh-better-sidebar)
124
- // internal → 不展示(预设内部行,如 ./restrict.mjs)
125
- // self → 不展示(本插件自己)
126
- // 未知名默认归 agent;新装插件一律能识别并在主/次行出现,不会漏。
127
- function pluginKindOf(p) {
128
- var label = ((p && (p.label || p.moduleName)) || "");
129
- if (label.charAt(0) === "/" || label.indexOf("./") === 0) return "internal";
130
- if (label === "paoding-config-ui") return "self";
131
- if (label === "dshmarket" || label === "dsh-better-sidebar") return "other";
132
- return "agent";
129
+ // 一组角色里是否有人配成可续(供面板顶警告横幅判定,与 roleBgModeOf 同口径)。
130
+ function hasContinuableRole(roles) {
131
+ var keys = Object.keys(roles || {});
132
+ for (var i = 0; i < keys.length; i++) {
133
+ if (roles[keys[i]] && roles[keys[i]].background_mode === ROLE_BG_CONTINUABLE) return true;
134
+ }
135
+ return false;
133
136
  }
134
137
 
135
- function pluginChipsOf(d) {
136
- var primary = [];
137
- var secondary = [];
138
- var seen = {};
139
- function push(p) {
140
- var label = p && (p.label || p.moduleName);
141
- if (!label || seen[label]) return;
142
- seen[label] = true;
143
- var kind = pluginKindOf(p);
144
- if (kind === "agent") primary.push(p);
145
- else if (kind === "other") secondary.push(p);
146
- // internal / self:不展示
147
- }
148
- ((d && d.pluginReports) || []).forEach(push);
149
- ((d && d.runtimePlugins) || []).forEach(function (e) {
150
- var label = e && e.moduleName;
151
- if (!label || seen[label]) return;
152
- // UI 双保险:只展示启用且运行中的条目(与服务端过滤口径一致)
153
- if (e.enabled === false) return;
154
- if (e.fiberPhase != null && e.fiberPhase !== "active") return;
155
- if (label.indexOf("@deepseek-ai/") === 0) return;
156
- if (label.indexOf("dsh-client") !== -1) return;
157
- if (label.indexOf("cordis") !== -1) return;
158
- push({ label: label, tools: [], runtime: true, moduleName: label });
159
- });
160
- return { primary: primary, secondary: secondary };
138
+ // 「可续」选中时的行内成本提示(定稿文案)。
139
+ var ROLE_BG_CONTINUABLE_HINT =
140
+ "可续会话跨轮保留子代理上下文:产出不满可用 send_message 就地续修;" +
141
+ "无自动清理,落盘与 token 成本随轮次累积,建议仅多轮打磨类角色开启。";
142
+
143
+ // persistence 后端缺失时的警告(定稿文案;只警示不拦截,预览 / 应用照常)。
144
+ var ROLE_BG_PERSISTENCE_WARN =
145
+ "未检测到会话持久化后端(sessionPersistence)。可续模式在委派时会直接报错 " +
146
+ "PERSISTENCE_UNAVAILABLE:需在 ~/.dsh/cordis.patch.yml 挂 " +
147
+ "@deepseek-ai/dsh-session-persistence-jsonl(host 层、机器级改动,详见 docs/orchestration.md)。";
148
+
149
+
150
+ // mcp__ 前缀判定(归「MCP 工具」组的依据,供角色工具网格分组与
151
+ // customCoreToolsOf 使用;只做展示分组,不影响识别/分配)。非 mcp__ 名字
152
+ // 再按是否在检测清单 inventory 里分进「基础工具」/「插件工具」两组。
153
+ function hostTool(name) {
154
+ return name.indexOf("mcp__") === 0;
161
155
  }
162
156
 
163
- // 运行时来源的插件条目没有工具归属信息,不显示计数(避免误读为 “0 工具”);
164
- // 其工具实际已并入 inventory / MCP chips,可在角色卡片里勾选。
165
- function chipTextOf(p) {
166
- if (p && p.runtime) return "插件 " + p.label + "(运行时)";
167
- return "插件 " + (p && p.label) + " · " + ((p && p.tools) || []).length + " 工具";
157
+ // 自定义角色「基础工具」核心候选:自定义角色没有 SRC 基础面(blocks 里没
158
+ // 有它的块),从主 agent 核心 allow(state.restrictBase)派生——去掉四个
159
+ // 委派名(DEFAULT_ROLES 三内置角色 + 静态的 search_internal_deep,委派
160
+ // 工具不走工具网格)与 mcp__ 工具(归「MCP 工具」组),剩下的就是可通过
161
+ // UI 勾给自定义角色的核心工具面;检测清单里的插件工具(mnemon_* 等)不
162
+ // 在此列,由「插件工具」组单独提供。
163
+ function customCoreToolsOf(data) {
164
+ var delegations = DEFAULT_ROLES.concat(DELEGATE_STATIC_TOOLS);
165
+ return ((data && data.restrictBase) || []).filter(function (t) {
166
+ return delegations.indexOf(t) === -1 && !hostTool(t);
167
+ });
168
168
  }
169
169
 
170
- // 运行时识别不可用时的回退提示(小字);字段缺失或正常时不渲染任何内容。
170
+ // 运行时识别不可用时的回退提示:持续状态(不是一次性事件),只在
171
+ // runtimeOk === false 且带原因时渲染一行小字,贴在标签行上方;字段缺失或
172
+ // 正常时返回 null 零占位。错误原文可能很长,只留前 200 字符(排障够用,
173
+ // 不撑爆版面),仅超长才补省略号。
171
174
  function runtimeNoteOf(d) {
172
175
  if (!d || d.runtimeOk !== false || !d.runtimeError) return null;
173
176
  var err = String(d.runtimeError);
174
177
  if (err.length > 200) err = err.slice(0, 200) + "…";
175
- return h("div", { className: "omd-hint" },
178
+ return h("div", { className: "omd-runtimeNote" },
176
179
  "运行时识别不可用,已回退文件扫描(" + err + ")");
177
180
  }
178
181
 
182
+ // 版本号展示归一:tag 缺前缀 v 时补上(服务端原样透传 GitHub tag_name,
183
+ // 通常自带 v,本地版本号则来自 package.json 不带 v,显示口径统一为 vX.Y.Z)。
184
+ function versionTagOf(tag) {
185
+ var s = String(tag || "");
186
+ return /^v/i.test(s) ? s : "v" + s;
187
+ }
188
+
189
+ // 版本提示(渲染在页头标题旁,由 PageShell 持有):常态版本号不进正文;
190
+ // updateAvailable 时这里渲染醒目提示卡:releaseUrl 新标签页打开 release 页、
191
+ // 「升级」按钮走 onUpgrade(一键 dsh plugin update 自升级),并保留当前版本号。
192
+ // latest 为 null(含检测失败 error 的情况)一律不显示新版部分——检测失败
193
+ // 时服务端也只回 200 + error 字段,这里整行静默。
194
+ // opts(可省):busy/busyOp 复用面板级互斥(任何操作进行中都禁用升级,
195
+ // 升级进行中也反过来禁用其他按钮),onUpgrade 为点击回调。
196
+ // ── 服务端 API 助手(PageShell 与 ConfigPanel 共用)──────────────────────
197
+ // GET(无 body)/ POST(有 body)对 /api/paoding/*,非 2xx 抛 Error。
198
+ // 服务端失败响应可能带排障附件(如 upgrade 的子进程输出尾部),Error 只有
199
+ // message 装不下它——挂到 error.output 上,调用方 catch 侧自行取舍(见
200
+ // PageShell doUpgrade 的尾部并入)。
201
+ // 总超时(models 等慢路由同限):请求挂死时按超时报错,不让按钮永久转圈。
202
+ var PAODING_API_TIMEOUT_MS = 60000;
203
+
204
+ function paodingApi(path, body) {
205
+ var opts = body === undefined ? {} : {
206
+ method: "POST",
207
+ headers: { "content-type": "application/json" },
208
+ body: JSON.stringify(body),
209
+ };
210
+ // AbortController 超时;老环境没有该构造器就降级为无超时(功能不变)。
211
+ var ctrl = typeof AbortController === "function" ? new AbortController() : null;
212
+ var timer = null;
213
+ if (ctrl) {
214
+ opts.signal = ctrl.signal;
215
+ timer = setTimeout(function () { ctrl.abort(); }, PAODING_API_TIMEOUT_MS);
216
+ }
217
+ return fetch("/api/paoding/" + path, opts).then(function (r) {
218
+ // 先取全文再 JSON.parse:非 JSON 响应(代理错误页 / 空体)不再抛
219
+ // SyntaxError,回落 HTTP <status> 的可读错误。
220
+ return r.text().then(function (raw) {
221
+ var d = null;
222
+ try { d = raw ? JSON.parse(raw) : null; } catch (err) { d = null; }
223
+ if (!r.ok) {
224
+ var err = new Error((d && d.error) || ("HTTP " + r.status));
225
+ if (d && typeof d.output === "string" && d.output) err.output = d.output;
226
+ throw err;
227
+ }
228
+ if (!d) throw new Error("HTTP " + r.status + "(响应不是 JSON)");
229
+ return d;
230
+ });
231
+ }).catch(function (err) {
232
+ if (ctrl && err && err.name === "AbortError") throw new Error("请求超时(" + (PAODING_API_TIMEOUT_MS / 1000) + "s 无响应)");
233
+ throw err;
234
+ }).finally(function () {
235
+ if (timer !== null) clearTimeout(timer);
236
+ });
237
+ }
238
+
239
+ // ── Toast 反馈控制器(模块级单例,PageShell 的 ToastHost 订阅渲染)──────
240
+ // 三档:ok(成功,4s 自动消失)/ warn(校验提示,6s)/ err(错误,粘性——
241
+ // 不自动消失,手动 × 关;错误文本可能很长(子进程输出尾部等),限高滚动。
242
+ // 栈上限 4 条,超出丢最旧。id 自增;定时器只挂自动消失档。
243
+ function createToastCtrl() {
244
+ var seq = 0;
245
+ var items = [];
246
+ var listeners = [];
247
+ function notify() { listeners.slice().forEach(function (fn) { fn(); }); }
248
+ function dismiss(id) {
249
+ items = items.filter(function (it) { return it.id !== id; });
250
+ notify();
251
+ }
252
+ function push(kind, text) {
253
+ var it = { id: ++seq, kind: kind, text: String(text || "") };
254
+ items = items.concat([it]);
255
+ if (items.length > 4) items = items.slice(items.length - 4);
256
+ if (kind !== "err") {
257
+ it.timer = setTimeout(function () { dismiss(it.id); }, kind === "warn" ? 6000 : 4000);
258
+ }
259
+ notify();
260
+ return it.id;
261
+ }
262
+ return {
263
+ ok: function (t) { return push("ok", t); },
264
+ warn: function (t) { return push("warn", t); },
265
+ err: function (t) { return push("err", t); },
266
+ dismiss: dismiss,
267
+ subscribe: function (fn) {
268
+ listeners.push(fn);
269
+ return function () { listeners = listeners.filter(function (f) { return f !== fn; }); };
270
+ },
271
+ snapshot: function () { return items.slice(); },
272
+ };
273
+ }
274
+ var toast = createToastCtrl();
275
+
276
+ // 页头版本更新卡:常态版本号常驻标题旁(pd-versionTag),检测到新版本时
277
+ // 在标题右侧并排一张紧凑警示卡(🆙 新版本链接 + 升级按钮)。由 PageShell
278
+ // 渲染并持有升级动作,面板正文不再重复版本信息;当前版本号就在旁边的
279
+ // pd-versionTag 上,卡内不再赘述「(当前 vX)」。note / error 为升级
280
+ // 反馈行(成功提示 / 失败原因,含子进程输出尾部),也收在卡内。
281
+ function versionRowOf(v, opts) {
282
+ if (!v || typeof v.current !== "string" || !v.current) return null;
283
+ if (!(v.updateAvailable && v.latest)) return null;
284
+ var busy = !!opts && !!opts.busy;
285
+ var note = (opts && opts.note) || "";
286
+ var error = (opts && opts.error) || "";
287
+ return h("div", { className: "omd-versionRow omd-versionNew" },
288
+ h("span", { className: "omd-versionIcon" }, "🆙"),
289
+ h("span", { className: "omd-versionText" },
290
+ "新版本 " + versionTagOf(v.latest) + " 可用 · ",
291
+ // 只信 https://github.com/ 的 release 链接;其余取值一律不渲染 <a>。
292
+ v.releaseUrl && /^https:\/\/github\.com\//.test(v.releaseUrl)
293
+ ? h("a", {
294
+ className: "omd-versionLink",
295
+ href: v.releaseUrl,
296
+ target: "_blank",
297
+ rel: "noreferrer",
298
+ }, "查看更新")
299
+ : null),
300
+ typeof (opts && opts.onUpgrade) === "function"
301
+ ? h("button", {
302
+ type: "button",
303
+ className: "omd-upgradeBtn",
304
+ disabled: busy,
305
+ onClick: opts.onUpgrade,
306
+ }, busy ? "升级中…" : "升级")
307
+ : null,
308
+ busy ? h("span", { className: "omd-upgradeHint" }, "下载安装中,最长约 2 分钟") : null,
309
+ error
310
+ ? h("span", { className: "omd-versionNote omd-versionNoteErr" }, error)
311
+ : (note ? h("span", { className: "omd-versionNote" }, note) : null));
312
+ }
313
+
314
+ // 反馈弹窗宿主:fixed 悬浮在操作坞上方,水平居中;host 本身 pointer-events:
315
+ // none,只有 toast 本体接事件(不挡下方按钮)。ok/warn 自动消失,err 带 ×
316
+ // 手动关。aria-live 播报;err 单条 role=alert 更醒目。
317
+ function ToastHost() {
318
+ var _t = useState(function () { return toast.snapshot(); }), items = _t[0], setItems = _t[1];
319
+ useEffect(function () {
320
+ return toast.subscribe(function () { setItems(toast.snapshot()); });
321
+ }, []);
322
+ if (!items.length) return null;
323
+ return h("div", { className: "pd-toastHost", "aria-live": "polite" },
324
+ items.map(function (it) {
325
+ return h("div", {
326
+ key: it.id,
327
+ className: "pd-toast pd-toast" + it.kind.charAt(0).toUpperCase() + it.kind.slice(1),
328
+ role: it.kind === "err" ? "alert" : "status",
329
+ },
330
+ h("span", { className: "pd-toastIcon", "aria-hidden": "true" },
331
+ it.kind === "err" ? "⛔" : it.kind === "warn" ? "⚠️" : "✅"),
332
+ h("div", { className: "pd-toastText" }, it.text),
333
+ h("button", {
334
+ type: "button",
335
+ className: "pd-toastClose",
336
+ "aria-label": "关闭提示",
337
+ onClick: function () { toast.dismiss(it.id); },
338
+ }, h("span", { className: "pd-closeGlyph", "aria-hidden": "true" }, "×")));
339
+ }));
340
+ }
341
+
179
342
  // ── 角色专用模型下拉(目录来自 GET /api/paoding/models,Node 半经
180
- // ctx.apiProxy.llm.models({}) 取 DSH 已注册的模型路由)────────────────
343
+ // ctx.llm 服务(listProviders / listModels)取已注册的模型路由)──────
181
344
 
182
345
  // 选项 value 编码:JSON.stringify([provider, model])。provider 路由与模型
183
346
  // id 都可能含任意字符(/ : . 等),裸拼接有歧义;JSON 数组编码可逆且无
@@ -247,6 +410,216 @@ window.__ModuleLoader__.load({
247
410
  };
248
411
  }
249
412
 
413
+ // ── 工作区选择(配置对象)助手 ───────────────────────────────────────
414
+
415
+ // 剥掉两个非目标字段容器键后的全局配置副本:existing 里现在还带 workspaces /
416
+ // profile,工作区视角回落全局时不能把它们当目标字段读。返回 null 表示无。
417
+ function globalConfigOf(d) {
418
+ var existing = d && d.existing;
419
+ if (!existing || typeof existing !== "object") return null;
420
+ var out = Object.assign({}, existing);
421
+ delete out.workspaces;
422
+ delete out.profile;
423
+ return out;
424
+ }
425
+
426
+ // 工作区标签条收纳个数的初值与兜底:测量未就绪(首帧)或环境不支持
427
+ // ResizeObserver 时按这个数拆可见区 / 隐藏区;正常路径的收纳个数由装填
428
+ // 算法按标签行实宽决定(见 ConfigPanel 的 tabsRowWidth / wsFitCount)。
429
+ var WS_TABS_FALLBACK_COUNT = 5;
430
+
431
+ // 工作区 pill 数据行:宿主注册表列表(GET /api/paoding/workspaces)为主,
432
+ // state.existing.workspaces 里多出的键(宿主列表不可用 / 尚未在 GUI 注册)
433
+ // 补在后面——「配置文件里出现过的工作区」必须永远可见、可切换。
434
+ function workspaceRowsOf(d, list) {
435
+ var rows = [];
436
+ var seen = {};
437
+ (list || []).forEach(function (it) {
438
+ if (!it || typeof it.path !== "string" || it.path === "" || seen[it.path]) return;
439
+ seen[it.path] = true;
440
+ rows.push({
441
+ path: it.path,
442
+ title: typeof it.title === "string" && it.title !== "" ? it.title : null,
443
+ sessionCount: typeof it.sessionCount === "number" ? it.sessionCount : null,
444
+ });
445
+ });
446
+ var wsCfg = (d && d.existing && d.existing.workspaces) || {};
447
+ Object.keys(wsCfg).forEach(function (p) {
448
+ if (seen[p]) return;
449
+ seen[p] = true;
450
+ rows.push({ path: p, title: null, sessionCount: null });
451
+ });
452
+ return rows;
453
+ }
454
+
455
+ // pill 文案:宿主 title 优先,缺省回落路径 basename(宿主列表不可用的降级行)。
456
+ function wsLabelOf(row) {
457
+ if (row.title) return row.title;
458
+ var parts = String(row.path).replace(/[\/\\]+$/, "").split(/[\/\\]/); // Windows 路径按 \ 也能取到 basename
459
+ return parts[parts.length - 1] || row.path;
460
+ }
461
+
462
+ // 工作区配置状态三态(tab / 菜单项共用的小标签):
463
+ // project —— 专属预设已生成(orchestrator-<slug> 已落盘,项目配置生效)
464
+ // pending —— 配置里存了工作区条目、但预设还没生成(编辑过没应用)
465
+ // global —— 没有任何条目(新会话跟随全局 orchestrator 预设)
466
+ // 判定优先级 installed > existing 条目 > 无。existing.workspaces 是磁盘
467
+ // 配置原文透传,meta 的 installed 对账 installedPresets。
468
+ function wsTagOf(existingWs, meta, wsPath) {
469
+ var m = meta && meta[wsPath];
470
+ if (m && m.installed) return "project";
471
+ if (existingWs && Object.prototype.hasOwnProperty.call(existingWs, wsPath)) return "pending";
472
+ return "global";
473
+ }
474
+
475
+ // 三态 → 标签文案(中文,与 tooltip 口径一致)
476
+ function wsTagTextOf(kind) {
477
+ return kind === "project" ? "项目" : kind === "pending" ? "待应用" : "全局";
478
+ }
479
+
480
+ // 角色卡「默认展开」判定单一来源(toggleRole / roleCard 共用):主 agent
481
+ // 与外部检索卡默认展开,其余默认收起;openMap 显式开关优先。
482
+ function defaultOpenOf(name) {
483
+ return name === "__main__" || name === "search_external";
484
+ }
485
+
486
+ // 工作区 tab / 「更多」菜单项共用的 title 提示串(两处渲染同款拼法,抽
487
+ // 一处防漂移):路径 + 会话数 + 配置状态(项目 / 待应用 / 全局)。
488
+ function wsTabTipOf(row, meta, existingWs) {
489
+ var m = meta && meta[row.path];
490
+ var kind = wsTagOf(existingWs, meta, row.path);
491
+ return row.path +
492
+ (row.sessionCount != null ? " · " + row.sessionCount + " 会话" : "") +
493
+ (kind === "project"
494
+ ? " · 项目配置(预设 " + (m && m.presetId) + ")"
495
+ : kind === "pending"
496
+ ? " · 已保存项目配置、预设未生成(点「保存并应用」生效)"
497
+ : " · 跟随全局配置");
498
+ }
499
+
500
+ // 工作区 tab 的显示顺序:按会话数降序(缺失沉底、同分保持注册表顺序),
501
+ // 激活项再恒置顶(紧跟「全局默认」之后)。收纳装填按这个顺序进行,激活
502
+ // tab 自然永远留在可见区;「更多」按钮上的 omd-tabMoreActive 兜底分支
503
+ // 保留(万一首帧测量未就绪)。
504
+ function wsDisplayOrderOf(rows, activeWs) {
505
+ // 会话数从多到少:常用工作区靠前,冷门的自然沉进「更多」菜单。缺失
506
+ //(null)按 -1 处理沉底;同分保持注册表顺序(V8 的 sort 稳定,不抖)。
507
+ var order = rows.slice().sort(function (a, b) {
508
+ var ca = a && a.sessionCount != null ? a.sessionCount : -1;
509
+ var cb = b && b.sessionCount != null ? b.sessionCount : -1;
510
+ return cb - ca;
511
+ });
512
+ if (activeWs === null) return order;
513
+ // 激活项恒置顶(紧跟「全局默认」):装填按此顺序进行,正在操作的 tab
514
+ // 永远不会被收进「更多」。
515
+ for (var i = 0; i < order.length; i++) {
516
+ if (order[i].path === activeWs && i > 0) {
517
+ var active = order.splice(i, 1)[0];
518
+ order.unshift(active);
519
+ break;
520
+ }
521
+ }
522
+ return order;
523
+ }
524
+
525
+ // 「添加工作区」组合框建议行:服务端候选(GET workspaces?discover=1 的
526
+ // items)按当前输入做子串过滤——完整路径或 basename 命中即可,不分大小写;
527
+ // 空输入 = 全部候选。没拉到候选(null)或无匹配时返回空数组,调用方据此
528
+ // 整块不渲染下拉。
529
+ function wsSuggestionsOf(cands, query) {
530
+ if (!Array.isArray(cands) || !cands.length) return [];
531
+ var q = String(query || "").trim().toLowerCase();
532
+ if (q === "") return cands.slice();
533
+ return cands.filter(function (c) {
534
+ return (typeof c.path === "string" && c.path.toLowerCase().indexOf(q) !== -1) ||
535
+ (typeof c.basename === "string" && c.basename.toLowerCase().indexOf(q) !== -1);
536
+ });
537
+ }
538
+
539
+ // ── 通用确认弹窗(confirm 型模态框)────────────────────────────────────
540
+ // 照 80-mount 的 createWorkspaceController 做模块级小状态器 + 单例:发起方
541
+ // (ConfigPanel 的删除 / 保存并应用、86 页头的升级)与渲染方(86 的
542
+ // ConfirmLayer)分属两棵 React 子树,中间隔着懒挂载的 createRoot,props
543
+ // 传不通,靠快照 + subscribe 同步。req 纯 JS 对象直存(含 onConfirm 回调),
544
+ // 不经 JSON 序列化。本片段拼接序在 86 之前,confirmCtrl / confirmModalOf
545
+ // 由 86 直接共用,不得重复声明。
546
+ function createConfirmController() {
547
+ var snapshot = { req: null };
548
+ var listeners = [];
549
+ function notify() {
550
+ listeners.slice().forEach(function (listener) { listener(); });
551
+ }
552
+ return {
553
+ getSnapshot: function () { return snapshot; },
554
+ subscribe: function (listener) {
555
+ listeners.push(listener);
556
+ return function () {
557
+ var i = listeners.indexOf(listener);
558
+ if (i !== -1) listeners.splice(i, 1);
559
+ };
560
+ },
561
+ // 弹出确认框:opts = { title, body, confirmText, cancelText, danger, onConfirm }。
562
+ // 不在这里执行 onConfirm——确认键点击时(confirmModalOf)先 close 再调,
563
+ // req 先行清空,异步 onConfirm 期间弹窗已卸载,双击 / Enter 不会二触发。
564
+ ask: function (opts) {
565
+ snapshot = {
566
+ req: {
567
+ title: opts.title,
568
+ body: opts.body,
569
+ confirmText: opts.confirmText || "确认",
570
+ cancelText: opts.cancelText || "取消",
571
+ danger: !!opts.danger,
572
+ onConfirm: opts.onConfirm,
573
+ },
574
+ };
575
+ notify();
576
+ },
577
+ close: function () {
578
+ if (snapshot.req === null) return;
579
+ snapshot = { req: null };
580
+ notify();
581
+ },
582
+ };
583
+ }
584
+ var confirmCtrl = createConfirmController();
585
+
586
+ // 确认弹窗纯渲染函数:只依赖模块级 h / P,与控制器同址声明。结构对齐
587
+ // wsModalOf / roleAddModalOf(pd-modalMask/Card/Header/Title/Close/Body/
588
+ // Footer),正文用 pd-modalText(多行文案 pre-line),danger 时确认钮加
589
+ // pd-dangerBtn(红底,删除类操作专用)。close 由 ConfirmLayer 传入。
590
+ function confirmModalOf(req, close) {
591
+ if (!req) return null;
592
+ return h("div", { className: "pd-modalMask",
593
+ // 只点遮罩空白处才收(mousedown 真正落在遮罩上),在卡片内按下拖选不误关
594
+ onMouseDown: function (e) { if (e.target === e.currentTarget) close(); } },
595
+ h("div", { className: "pd-modalCard pd-confirmCard", role: "alertdialog", "aria-modal": "true",
596
+ "aria-label": req.title, "aria-describedby": "pd-confirmBody" },
597
+ h("div", { className: "pd-modalHeader" },
598
+ h("h2", { className: "pd-modalTitle" }, req.title),
599
+ h("button", { type: "button", className: "pd-modalClose", "aria-label": "关闭", onClick: close },
600
+ h("span", { className: "pd-closeGlyph", "aria-hidden": "true" }, "×"))),
601
+ h("div", { className: "pd-modalBody" },
602
+ h("div", { className: "pd-modalText", id: "pd-confirmBody" }, req.body)),
603
+ h("div", { className: "pd-modalFooter" },
604
+ // 焦点默认落在确认钮(危险操作落在取消钮):开窗即可 Enter 确认 /
605
+ // 取消,与原生 confirm 的键盘习惯对齐;danger 反向落焦是让破坏性
606
+ // 操作多一道显式点击的门槛。弹窗关闭即整体卸载,重开重新自动聚焦。
607
+ h(P.Button, { variant: "outline", size: "md", autoFocus: !!req.danger, onClick: close },
608
+ req.cancelText || "取消"),
609
+ h(P.Button, {
610
+ variant: "primary",
611
+ size: "md",
612
+ className: req.danger ? "pd-dangerBtn" : undefined,
613
+ autoFocus: !req.danger,
614
+ onClick: function () {
615
+ // 先 close 再执行:req 立即清空、按钮随弹窗卸载,onConfirm
616
+ // 异步期间的重复点击 / Enter 都不会再命中确认路径。
617
+ close();
618
+ if (typeof req.onConfirm === "function") req.onConfirm();
619
+ },
620
+ }, req.confirmText || "确认"))));
621
+ }
622
+
250
623
  // ── 主面板 ─────────────────────────────────────────────────────────────
251
624
 
252
625
  function ConfigPanel(props) {
@@ -256,37 +629,102 @@ window.__ModuleLoader__.load({
256
629
  var _b = useState(false), busy = _b[0], setBusy = _b[1];
257
630
  var _op = useState(null), busyOp = _op[0], setBusyOp = _op[1];
258
631
  var _p = useState(null), preview = _p[0], setPreview = _p[1];
259
- var _n = useState(""), note = _n[0], setNote = _n[1];
260
632
  var _o = useState({}), openMap = _o[0], setOpenMap = _o[1];
261
633
  var _m = useState({}), personaMap = _m[0], setPersonaMap = _m[1];
262
634
  var _x = useState(false), extraOpen = _x[0], setExtraOpen = _x[1];
263
635
  var _r = useState(0), extraRev = _r[0], setExtraRev = _r[1];
264
- var _nr = useState(0), nameRev = _nr[0], setNameRev = _nr[1]; // 主 agent 名称 input 重挂(key 技巧)
636
+ var _dnr = useState(0), displayNameRev = _dnr[0], setDisplayNameRev = _dnr[1]; // 主 agent 显示名 input 重挂(key 技巧)
265
637
  var _rn = useState({}), roleNameRev = _rn[0], setRoleNameRev = _rn[1]; // 内置角色显示名 input 重挂(每角色独立 key 计数)
266
638
  var _rm = useState({}), roleModelRev = _rm[0], setRoleModelRev = _rm[1]; // 角色专用模型/provider input 重挂(仅降级手输模式用;每角色独立 key 计数)
267
639
  var _mg = useState(null), modelGroups = _mg[0], setModelGroups = _mg[1]; // 模型目录 groups(/api/paoding/models)
268
640
  var _mgok = useState(false), modelGroupsOk = _mgok[0], setModelGroupsOk = _mgok[1]; // 目录可用 = 下拉模式;false = 降级双 input
269
641
  var _mgerr = useState(""), modelGroupsErr = _mgerr[0], setModelGroupsErr = _mgerr[1]; // 目录拉取失败原因(降级 hint 一句话展示)
642
+ // 版本信息(/api/paoding/version;null = 未取到/检测失败,静默不显示)。
643
+ // 状态挂在页头 PageShell(版本号展示在标题旁),经 props 下发:
644
+ // setVerInfo 供 loadVersion 回写页头(版本号与更新卡都在 PageShell)。
645
+ var setVerInfo = props && props.setVerInfo;
646
+ var _ws = useState(null), activeWs = _ws[0], setActiveWs = _ws[1]; // 当前配置对象:null = 全局默认;否则工作区绝对路径
647
+ // 视角切换计数:switchWorkspace 每次自增,拼进全部未受控(defaultValue)
648
+ // 编辑器的 key——否则切 tab 不重挂,旧工作区输入框里未提交的文本会在
649
+ // blur 时写进新工作区的 assign(串台)。现有各 rev 只覆盖「校验失败 /
650
+ // 恢复默认」重挂,不覆盖切视角,故统一再加这一维。
651
+ var _swr = useState(0), switchRev = _swr[0], setSwitchRev = _swr[1];
652
+ var _wsl = useState([]), wsList = _wsl[0], setWsList = _wsl[1]; // 宿主工作区列表(GET /api/paoding/workspaces;失败降级为空数组,pill 由配置文件键兜底)
653
+ var _wslt = useState(false), wsListTried = _wslt[0], setWsListTried = _wslt[1]; // 列表请求尘埃落定标记(成败均置位):pendingOpenWs 消费条件用
654
+ var _wsao = useState(false), wsAddOpen = _wsao[0], setWsAddOpen = _wsao[1]; // 「添加工作区」弹窗开合态(旧版是 tab 条下方的内联展开行)
655
+ var _wsap = useState(""), wsAddPath = _wsap[0], setWsAddPath = _wsap[1];
656
+ var _wsae = useState(""), wsAddErr = _wsae[0], setWsAddErr = _wsae[1];
657
+ var _rao = useState(false), roleAddOpen = _rao[0], setRoleAddOpen = _rao[1]; // 「新建自定义 agent 角色」弹窗开合态(原原生 prompt 改模态框)
658
+ var _ran = useState(""), roleAddName = _ran[0], setRoleAddName = _ran[1];
659
+ var _radn = useState(""), roleAddDispName = _radn[0], setRoleAddDispName = _radn[1]; // 弹窗第二字段「显示名」(可选;残值语义同 roleAddName,成功提交才清)
660
+ var _rabm = useState("one-shot"), roleAddBgMode = _rabm[0], setRoleAddBgMode = _rabm[1]; // 弹窗第三字段「会话模式」:'one-shot' | 'continuable'(残值语义同上,成功提交才复位)
661
+ var _rae = useState(""), roleAddErr = _rae[0], setRoleAddErr = _rae[1];
662
+ // 「重命名自定义角色」弹窗三件套(原原生 prompt 改模态框):For 为
663
+ // null = 关闭,非空 = 待改名的旧 toolName(身份键,提交时迁移 skills 引用)。
664
+ var _rrf = useState(null), roleRenameFor = _rrf[0], setRoleRenameFor = _rrf[1];
665
+ var _rrv = useState(""), roleRenameVal = _rrv[0], setRoleRenameVal = _rrv[1]; // 弹窗输入值(开窗预填旧名)
666
+ var _rre = useState(""), roleRenameErr = _rre[0], setRoleRenameErr = _rre[1];
667
+ var roleAddDispRef = useRef(null); // 显示名 input 引用:toolName 框 Enter 的移焦落点(两字段分工见 roleAddModalOf 注释)
668
+ var _wsc = useState(null), wsCands = _wsc[0], setWsCands = _wsc[1]; // 服务端候选目录(?discover=1);null = 未拉到(含失败),纯手输兜底
669
+ var _wsso = useState(false), wsSuggestOpen = _wsso[0], setWsSuggestOpen = _wsso[1]; // 建议下拉展开态(跟随输入框焦点 / Esc)
670
+ var _wsmo = useState(false), wsMenuOpen = _wsmo[0], setWsMenuOpen = _wsmo[1]; // 「更多」下拉展开态(放不下收进来的那批工作区)
671
+ var _wssk = useState(null), wsSkills = _wssk[0], setWsSkills = _wssk[1]; // 当前工作区技能合并列表(用户级 + 该工作区项目级;null = 全局视角 / 未拉到)
672
+ var _wspk = useState(null), wsProjSkills = _wspk[0], setWsProjSkills = _wspk[1]; // 项目级技能子集(Set,与 wsSkills 同源同步;项目角标据此渲染)
673
+ var _wsse = useState(""), wsSkillsErr = _wsse[0], setWsSkillsErr = _wsse[1]; // 工作区技能拉取失败的降级一句话提示
674
+ var wsSkillsCacheRef = useRef({}); // 工作区技能缓存:每路径只拉一次(面板生命周期;失败也记 null 不重试)
675
+ var wsCandsTriedRef = useRef(false); // 候选只拉一次的面板级闸门(失败的尝试也算数,防聚焦一次请求一次)
676
+ var activeWsRef = useRef(activeWs); // load / rescan / preview / apply 等异步回调里读最新值,不依赖闭包里的旧 activeWs
677
+ activeWsRef.current = activeWs; // 渲染期同步(回调只在这些 setter 之后触发,安全)
270
678
  var rootRef = useRef(null); // 面板根节点(重新检测后回顶用)
271
679
  var previewRef = useRef(null); // 预览生成组根节点(条件渲染,预览成功后定位用)
680
+ var assignRef = useRef(assign); // doPreview 完成回调里比对请求时快照:生成期间 assign 变过则结果作废
681
+ assignRef.current = assign; // 渲染期同步(同 activeWsRef 的做法)
272
682
  var _sr = useState(null), scrollReq = _sr[0], setScrollReq = _sr[1]; // 滚动请求:"preview" | "top" | null
273
-
274
- var api = useCallback(function (path, body) {
275
- var opts = body === undefined ? {} : {
276
- method: "POST",
277
- headers: { "content-type": "application/json" },
278
- body: JSON.stringify(body),
279
- };
280
- return fetch("/api/paoding/" + path, opts).then(function (r) {
281
- return r.json().then(function (d) {
282
- if (!r.ok) throw new Error(d.error || ("HTTP " + r.status));
283
- return d;
284
- });
285
- });
286
- }, []);
287
-
288
- var buildAssignments = useCallback(function (d) {
289
- var base = d.existing || d.suggested || { roles: {}, roles_remove: [], main_agent_name: null, main_agent_extra: [], main_agent_remove: [], main_agent_skills: [], main_agent_skills_inline: [], skills: {} };
683
+ // 工作区标签行自动收纳:量出标签行实宽后由装填算法决定直接露出几个
684
+ // tab(wsFitCount),放不下的收进「更多」。tabWidthsRef 缓存测量层量到
685
+ // 的每个 tab 自然宽度(data-ws-key → offsetWidth),跨渲染复用。
686
+ var tabsRowRef = useRef(null); // .omd-tabs 标签行(宽度来源)
687
+ var tabsMeasureRef = useRef(null); // 隐藏测量层容器
688
+ var tabWidthsRef = useRef({});
689
+ var _rw = useState(0), tabsRowWidth = _rw[0], setTabsRowWidth = _rw[1];
690
+ var _fc = useState(WS_TABS_FALLBACK_COUNT), wsFitCount = _fc[0], setWsFitCount = _fc[1]; // 初值与兜底 = 测量未就绪时的固定 5 个
691
+
692
+ var ready = !!(data && assign); // 面板数据就绪:宽度观察 / 装填计算只在就绪后跑
693
+ // 装填计算的依赖指纹:工作区集合、显示顺序、文案或安装标记任一变化都会
694
+ // 改变这个串,effect 据此重算收纳个数(行宽变化经 tabsRowWidth 触达)。
695
+ // 必须先于下方用到它的 useEffect 求值,依赖数组才能拿到当次渲染的值。
696
+ var wsMetaNow = (data && data.workspaceMeta) || {};
697
+ var existingWsKey = (data && data.existing && data.existing.workspaces) || null;
698
+ var wsWidthKey = "";
699
+ (function buildKey() {
700
+ var rowsFor = workspaceRowsOf(data, wsList);
701
+ var ord = wsDisplayOrderOf(rowsFor, activeWs);
702
+ for (var i = 0; i < ord.length; i++) {
703
+ var m = wsMetaNow[ord[i].path];
704
+ wsWidthKey += (i ? "|" : "") + ord[i].path + ":" + wsLabelOf(ord[i]) + ":"
705
+ + (ord[i].sessionCount != null ? ord[i].sessionCount : "-") + ":"
706
+ + (m && m.installed ? "2" : existingWsKey && Object.prototype.hasOwnProperty.call(existingWsKey, ord[i].path) ? "1" : "0");
707
+ }
708
+ })();
709
+
710
+ // 实现在模块级 paodingApi(PageShell 的页头升级卡也用它),这里薄包一层
711
+ // 保持既有调用点与 useCallback 依赖口径不变。
712
+ var api = useCallback(function (path, body) { return paodingApi(path, body); }, []);
713
+
714
+ // 目标配置基座选取:工作区视角(wsPath 非空)优先取该工作区已保存条目
715
+ // (existing.workspaces[wsPath],没应用过则回落剥掉 workspaces/profile 的
716
+ // 全局 existing),全局视角直接用 existing(同样剥污染键)、再回落建议值。
717
+ // 返回的 assignments 只由下方显式字段拼出,workspaces / profile 两个容器
718
+ // 键天然混不进 POST 体。
719
+ var buildTargetAssignments = useCallback(function (d, wsPath) {
720
+ var base = null;
721
+ if (wsPath) {
722
+ var wsCfg = d.existing && d.existing.workspaces && d.existing.workspaces[wsPath];
723
+ if (wsCfg) base = wsCfg;
724
+ }
725
+ if (!base) {
726
+ base = globalConfigOf(d) || d.suggested || { roles: {}, roles_remove: [], main_agent_display_name: null, main_agent_extra: [], main_agent_remove: [], main_agent_skills: [], main_agent_skills_inline: [], main_agent_persona_extra: null, skills: {} };
727
+ }
290
728
  // roles_remove:被删除的内置角色名单(只含 DEFAULT_ROLES 名)。名单内的内置角色
291
729
  // 不重建进 roles —— 删除状态跨刷新 / 重新检测保留,卡片消失,仅留在恢复区。
292
730
  var removed = (base.roles_remove || []).filter(function (n) { return DEFAULT_ROLES.indexOf(n) !== -1; });
@@ -301,6 +739,9 @@ window.__ModuleLoader__.load({
301
739
  //(null = 跟随主 agent 当前模型;provider 只配不配 model 时后端 warn 并忽略)
302
740
  model: roleStringRef(base.roles, name, "model"),
303
741
  provider: roleStringRef(base.roles, name, "provider"),
742
+ // 会话模式透传:仅 'continuable' 认可,其余归 'one-shot'(字面量,
743
+ // 与 config 层 normalizeBackgroundMode 的序列化形状一致)
744
+ background_mode: roleBgModeOf(base.roles, name),
304
745
  tools: (base.roles && base.roles[name] && base.roles[name].tools) ||
305
746
  (d.blocks && d.blocks[name]) || [],
306
747
  };
@@ -313,14 +754,17 @@ window.__ModuleLoader__.load({
313
754
  // 自定义角色同样携带 model / provider(null 或 string),形状与内置一致
314
755
  model: roleStringRef(base.roles, name, "model"),
315
756
  provider: roleStringRef(base.roles, name, "provider"),
757
+ // 会话模式与内置同款收法:'one-shot' = 缺省一次性,'continuable' = 可续
758
+ background_mode: roleBgModeOf(base.roles, name),
316
759
  tools: base.roles[name].tools || [],
317
760
  };
318
761
  });
319
762
  return {
320
763
  roles: roles,
321
764
  roles_remove: removed,
322
- // 语义:null / '' / 缺省一律归一为 null('' 等于没配 = 默认显示名)
323
- main_agent_name: base.main_agent_name == null || base.main_agent_name === "" ? null : base.main_agent_name,
765
+ // 显示名键(仅 preset 显示名,persona 身份行不可改名):null / '' / 缺省
766
+ // 一律归一为 null(= 默认「编排模式 (Orchestrator)」,工作区预设用派生名)
767
+ main_agent_display_name: base.main_agent_display_name == null || base.main_agent_display_name === "" ? null : base.main_agent_display_name,
324
768
  main_agent_extra: base.main_agent_extra || [],
325
769
  main_agent_remove: base.main_agent_remove || [],
326
770
  main_agent_skills: (base.main_agent_skills || []).slice(),
@@ -332,16 +776,252 @@ window.__ModuleLoader__.load({
332
776
  }, []);
333
777
 
334
778
  var load = useCallback(function () {
335
- setError(null);
336
779
  api("state").then(function (d) {
337
780
  setData(d);
338
- setAssign(buildAssignments(d));
339
- setNote("");
781
+ setAssign(buildTargetAssignments(d, activeWsRef.current));
340
782
  }).catch(function (err) { setError(err.message); });
341
- }, [api, buildAssignments]);
783
+ }, [api, buildTargetAssignments]);
342
784
 
343
785
  useEffect(load, [load]);
344
786
 
787
+ // 工作区列表:面板加载时拉一次宿主注册表(与 state 并行)。失败降级为
788
+ // 空列表——workspaceRowsOf 会用 state.existing.workspaces 的键兜底出 pill,
789
+ // 绝不阻塞面板。
790
+ var loadWorkspaces = useCallback(function () {
791
+ api("workspaces").then(function (d) {
792
+ if (!d || d.ok !== true || !Array.isArray(d.items)) throw new Error("工作区列表响应格式异常");
793
+ setWsList(d.items);
794
+ setWsListTried(true);
795
+ }).catch(function () {
796
+ setWsList([]); // 静默降级:只用配置文件里已有的工作区键
797
+ setWsListTried(true);
798
+ });
799
+ }, [api]);
800
+
801
+ useEffect(loadWorkspaces, [loadWorkspaces]);
802
+
803
+ // 宿主菜单「庖丁配置」入口的落地:state 与工作区列表都就绪(列表成败
804
+ // 均算就绪,wsListTried 见 loadWorkspaces)后,把 pendingOpenWs 指着的
805
+ // 工作区切为激活 tab。找不到(未注册 / 列表拉失败)给 warn、留在当前
806
+ // tab,一次性消费不再重试。
807
+ useEffect(function () {
808
+ if (pendingOpenWs === null || !data || !wsListTried) return;
809
+ var target = pendingOpenWs;
810
+ pendingOpenWs = null;
811
+ var rows = workspaceRowsOf(data, wsList);
812
+ var hit = null;
813
+ for (var i = 0; i < rows.length; i++) {
814
+ if (rows[i].path.replace(/[\/\\]+$/, "") === target) { hit = rows[i].path; break; }
815
+ }
816
+ if (hit) switchWorkspace(hit);
817
+ else toast.warn("工作区 " + target + " 不在列表里,未切换 tab");
818
+ }, [data, wsListTried]);
819
+
820
+ // 「添加工作区」候选:输入框首次聚焦时拉一次服务端扫描
821
+ //(GET workspaces?discover=1),面板生命周期内缓存不重拉(含失败的
822
+ // 尝试——不该每次聚焦都打一遍接口)。请求错误 / 响应异常一律静默:
823
+ // 候选保持 null,输入框退化成现状的纯手输,不弹错不打扰。
824
+ function loadWsCands() {
825
+ if (wsCandsTriedRef.current) return;
826
+ wsCandsTriedRef.current = true;
827
+ api("workspaces?discover=1").then(function (d) {
828
+ if (d && d.ok === true && Array.isArray(d.items)) setWsCands(d.items);
829
+ }).catch(function () { /* 静默降级:纯手输 */ });
830
+ }
831
+
832
+ // 按工作区拉技能(GET workspaces?skills=1&path=…):切到某工作区时拉取
833
+ // 该目录的技能全集(用户级 + 该工作区项目级)与项目级子集。面板生命周期
834
+ // 内每路径只拉一次(结果存 wsSkillsCacheRef,失败也记 null 不重试)。
835
+ // 失败降级:wsSkills 置 null(技能网格回落 data.skills 用户级)并留一句
836
+ // 提示;切回全局清空两态。响应回来时若已切走(activeWsRef 对不上)只写
837
+ // 缓存、不再动当前视图,防串台。
838
+ function applyWsSkills(wsPath) {
839
+ if (!wsPath) {
840
+ setWsSkills(null);
841
+ setWsProjSkills(null);
842
+ setWsSkillsErr("");
843
+ return;
844
+ }
845
+ var cached = wsSkillsCacheRef.current[wsPath];
846
+ if (cached !== undefined) {
847
+ setWsSkills(cached ? cached.skills : null);
848
+ setWsProjSkills(cached ? new Set(cached.projectSkills) : null);
849
+ setWsSkillsErr(cached ? "" : "该工作区技能列表拉取失败,技能网格已回退用户级技能");
850
+ return;
851
+ }
852
+ api("workspaces?skills=1&path=" + encodeURIComponent(wsPath)).then(function (d) {
853
+ if (!d || d.ok !== true || !Array.isArray(d.skills)) throw new Error("技能列表响应格式异常");
854
+ var proj = Array.isArray(d.projectSkills) ? d.projectSkills : [];
855
+ wsSkillsCacheRef.current[wsPath] = { skills: d.skills, projectSkills: proj };
856
+ if (activeWsRef.current !== wsPath) return; // 期间已切走:只留缓存
857
+ setWsSkills(d.skills);
858
+ setWsProjSkills(new Set(proj));
859
+ setWsSkillsErr("");
860
+ }).catch(function () {
861
+ wsSkillsCacheRef.current[wsPath] = null;
862
+ if (activeWsRef.current !== wsPath) return;
863
+ setWsSkills(null);
864
+ setWsProjSkills(null);
865
+ setWsSkillsErr("该工作区技能列表拉取失败,技能网格已回退用户级技能");
866
+ });
867
+ }
868
+
869
+ // 切换配置对象(全局 ↔ 工作区):按新视角重建 assign,并清掉上一视角的
870
+ // 预览(预览文本对应的是旧目标,留着只会误导);切到工作区时顺带
871
+ // 拉取该工作区的技能列表(带缓存,见 applyWsSkills)。
872
+ function switchWorkspace(wsPath) {
873
+ setActiveWs(wsPath);
874
+ activeWsRef.current = wsPath;
875
+ if (data) setAssign(buildTargetAssignments(data, wsPath));
876
+ setPreview(null);
877
+ setSwitchRev(switchRev + 1); // 未受控编辑器随 key 变化重挂:显示新目标值,blur 不再把旧视角文本写进新视角
878
+ applyWsSkills(wsPath);
879
+ }
880
+
881
+ // 添加工作区:POST /api/paoding/workspaces 注册到宿主(幂等),成功后
882
+ // 刷新工作区列表并切到该工作区(未应用过配置的工作区先回落全局现状,
883
+ // 首次「保存并应用」才生成工作区专属 preset 与 workspaces 条目)。
884
+ // rawPath 仅组合框建议行点击时传选中的候选路径(只认 string,按钮
885
+ // onClick 透传进来的事件对象等一律回落当前输入值,Enter 语义不变)。
886
+ function submitWorkspace(rawPath) {
887
+ var p = String(typeof rawPath === "string" ? rawPath : wsAddPath || "").trim();
888
+ if (p === "") return;
889
+ setBusy(true);
890
+ setBusyOp("wsadd");
891
+ api("workspaces", { path: p }).then(function (r) {
892
+ var ws = (r && r.workspace) || {};
893
+ var wsPath = typeof ws.path === "string" && ws.path !== "" ? ws.path : p;
894
+ setWsAddPath("");
895
+ setWsAddOpen(false);
896
+ setWsSuggestOpen(false);
897
+ setWsAddErr("");
898
+ return api("workspaces").then(function (d) {
899
+ if (d && d.ok === true && Array.isArray(d.items)) setWsList(d.items);
900
+ switchWorkspace(wsPath);
901
+ toast.ok((r.created === false ? "工作区已在列表里:" : "已注册工作区:") + wsPath + ";配置后点「保存并应用」生成专属预设");
902
+ });
903
+ }).catch(function (err) {
904
+ setWsAddErr(err.message);
905
+ }).finally(function () {
906
+ setBusy(false);
907
+ setBusyOp(null);
908
+ });
909
+ }
910
+
911
+ // 收起「添加工作区」弹窗(右上 × / 取消按钮 / 点遮罩空白处共用)。不清
912
+ // wsAddPath / wsAddErr:成功路径由 submitWorkspace 自己清理,取消后重开
913
+ // 保留残值——改两个字就能续填,报错文案也留着,重开即见上次失败原因。
914
+ function closeWsModal() {
915
+ setWsAddOpen(false);
916
+ setWsSuggestOpen(false);
917
+ }
918
+
919
+ // 收起「新建自定义 agent 角色」弹窗(右上 × / 取消按钮 / 点遮罩空白处
920
+ // 共用)。同样不清 roleAddName / roleAddDispName / roleAddErr:取消重开
921
+ // 保留残值续填。
922
+ function closeRoleAddModal() {
923
+ setRoleAddOpen(false);
924
+ }
925
+
926
+ // 收起「重命名自定义角色」弹窗(右上 × / 取消按钮 / 点遮罩空白处 /
927
+ // Esc 共用)。同样不清 roleRenameVal / roleRenameErr:取消重开保留
928
+ // 残值续填,重开时由 renameRole 重置(预填旧名 + 清错误行)。
929
+ function closeRoleRenameModal() {
930
+ setRoleRenameFor(null);
931
+ }
932
+
933
+ // 弹窗 / 「更多」菜单的 Esc 分层拦截:捕获阶段先于 PageShell 的冒泡监听
934
+ // 触发,stopPropagation 之后页面级关页逻辑不再收到该事件。优先级:更多
935
+ // 菜单 > 弹窗;弹窗里建议下拉还开着时先只收下拉,再按一次才收弹窗。
936
+ // 确认弹窗(confirmCtrl)不在本链里——它的 Esc 由 86 的 ConfirmLayer
937
+ // 自带捕获级监听处理(注册在本监听之后,本监听只在自己弹窗开着时才
938
+ // stopPropagation,互不踩踏)。
939
+ useEffect(function () {
940
+ var onKey = function (e) {
941
+ if (!e || e.key !== "Escape") return;
942
+ if (wsMenuOpen) { e.stopPropagation(); setWsMenuOpen(false); return; }
943
+ if (wsAddOpen) {
944
+ e.stopPropagation();
945
+ if (wsSuggestOpen) setWsSuggestOpen(false);
946
+ else setWsAddOpen(false);
947
+ }
948
+ if (roleAddOpen) { e.stopPropagation(); closeRoleAddModal(); }
949
+ if (roleRenameFor) { e.stopPropagation(); closeRoleRenameModal(); }
950
+ };
951
+ document.addEventListener("keydown", onKey, true);
952
+ return function () { document.removeEventListener("keydown", onKey, true); };
953
+ }, [wsAddOpen, wsSuggestOpen, wsMenuOpen, roleAddOpen, roleRenameFor]);
954
+
955
+ // 「更多」菜单的外点关闭:目标不在菜单本体、也不在「更多」按钮内就收起。
956
+ // 不 stopPropagation——正常点击照常生效;菜单项与「更多」按钮自身的
957
+ // onClick 已各自收菜单,这里只兜「点面板其它地方」这一种情况。
958
+ useEffect(function () {
959
+ if (!wsMenuOpen) return;
960
+ var onDocClick = function (e) {
961
+ var t = e && e.target;
962
+ if (t && typeof t.closest === "function" && t.closest(".omd-tabMenu, .omd-tabMore")) return;
963
+ setWsMenuOpen(false);
964
+ };
965
+ document.addEventListener("click", onDocClick, true);
966
+ return function () { document.removeEventListener("click", onDocClick, true); };
967
+ }, [wsMenuOpen]);
968
+
969
+ // 标签行宽度观察:就绪后先量一次行宽,再挂 ResizeObserver 跟随窗口缩放、
970
+ // 侧栏开合等布局变化。老环境没有 ResizeObserver 就止步于这一次测量,
971
+ // 收纳个数停在兜底值(WS_TABS_FALLBACK_COUNT),交互不缺失只是不自适应。
972
+ useEffect(function () {
973
+ if (!ready) return;
974
+ var row = tabsRowRef.current;
975
+ if (!row) return;
976
+ var measure = function () { setTabsRowWidth(row.clientWidth); };
977
+ measure();
978
+ if (typeof ResizeObserver !== "function") return;
979
+ var ro = new ResizeObserver(measure);
980
+ ro.observe(row);
981
+ return function () { ro.disconnect(); };
982
+ }, [ready]);
983
+
984
+ // 装填计算:先从测量层收一轮实测宽度(data-ws-key → offsetWidth),再按
985
+ // 「全局默认必放、工作区按显示顺序逐个尝试」做前缀和装填,算出直接露出
986
+ // 的 tab 个数。行宽为 0 或某行宽度还没量到(首帧)就维持现值等下一轮——
987
+ // 首帧 wsFitCount 仍是兜底 5,宽度到位后这里修正,可能轻微跳变,可接受。
988
+ useEffect(function () {
989
+ if (!ready) return;
990
+ var layer = tabsMeasureRef.current;
991
+ if (layer) {
992
+ var nodes = layer.querySelectorAll("[data-ws-key]");
993
+ for (var i = 0; i < nodes.length; i++) {
994
+ tabWidthsRef.current[nodes[i].getAttribute("data-ws-key")] = nodes[i].offsetWidth;
995
+ }
996
+ }
997
+ var widths = tabWidthsRef.current;
998
+ var globalW = widths.__global__ || 0;
999
+ var addW = widths.__add__ || 0;
1000
+ var moreW = widths.__more__ || 0;
1001
+ if (!globalW || !addW || !tabsRowWidth) return; // 测量未就绪:维持现值
1002
+ var order = wsDisplayOrderOf(workspaceRowsOf(data, wsList), activeWs);
1003
+ // gap 4px、行内边距 2px×2,再留 8px 安全余量抵消「更多」计数徽标的
1004
+ // 位数差与亚像素取整。
1005
+ var GAP = 4, SAFETY = 8;
1006
+ var avail = tabsRowWidth - 4 - SAFETY;
1007
+ var prefix = [0];
1008
+ for (var j = 0; j < order.length; j++) {
1009
+ var w = widths[order[j].path] || 0;
1010
+ if (!w) return; // 某行还没量到(首帧):等下一轮,别算出缩水值
1011
+ prefix.push(prefix[j] + w);
1012
+ }
1013
+ var N = order.length, k = 0;
1014
+ while (k < N) {
1015
+ var next = k + 1;
1016
+ // 放进 next 个工作区时整行要摆:全局默认 + 这 next 个 + 「+」,
1017
+ // 还有装不下的(next < N)就再为「更多」预留一位;间隙数 = 项数 - 1。
1018
+ var need = globalW + prefix[next] + addW + (next < N ? moreW : 0) + GAP * (1 + next + (next < N ? 1 : 0));
1019
+ if (need > avail) break;
1020
+ k = next;
1021
+ }
1022
+ setWsFitCount(k);
1023
+ }, [ready, tabsRowWidth, wsWidthKey]);
1024
+
345
1025
  // 模型目录:与 loadState 并行拉一次(目录发现可能稍慢,属正常;失败不
346
1026
  // 阻塞面板,只把角色专用模型降级为双 input 手输)。请求错误 / ok:false /
347
1027
  // 响应格式异常都归入降级,原因一句话留在 modelGroupsErr。
@@ -360,6 +1040,18 @@ window.__ModuleLoader__.load({
360
1040
 
361
1041
  useEffect(loadModels, [loadModels]);
362
1042
 
1043
+ // 版本提示:面板挂载后异步拉一次 /api/paoding/version(api 助手无 body
1044
+ // 即 GET,与首屏 state/models 并行、不阻塞渲染;服务端带 1 小时缓存且
1045
+ // 绝不 5xx)。失败静默:不设 error 状态——检测只是锦上添花,网络不通、
1046
+ // 接口异常都不该打扰面板;error/latest 皆空时 versionRowOf 也不渲染。
1047
+ var loadVersion = useCallback(function () {
1048
+ api("version").then(function (d) {
1049
+ if (d && typeof d.current === "string" && d.current && typeof setVerInfo === "function") setVerInfo(d);
1050
+ }).catch(function () { /* 静默:版本检测失败不影响面板 */ });
1051
+ }, [api]);
1052
+
1053
+ useEffect(loadVersion, [loadVersion]);
1054
+
363
1055
  // 滚动请求:preview 需等条件渲染挂载后生效(effect 在 commit 后运行),top 立即生效。
364
1056
  useEffect(function () {
365
1057
  if (!scrollReq) return;
@@ -452,51 +1144,162 @@ window.__ModuleLoader__.load({
452
1144
  setExtraRev(extraRev + 1); // 重挂 textarea,defaultValue 重新取默认文本
453
1145
  }
454
1146
 
455
- function addRole() {
456
- var name = window.prompt('新角色 toolName(英文小写,如 "reviewer"):');
457
- if (!name) return;
458
- name = name.trim();
459
- if (!/^[a-z][a-z0-9_-]{1,31}$/.test(name)) { setNote("角色名须为 2-32 位小写字母/数字/下划线/连字符"); return; }
460
- // 内置名不可经「新建」再造:正常存在时由下方 prev.roles 命中提示;已删除
461
- // (不在 roles、在 roles_remove)时在此拦截并引导走恢复区,避免同一内置名
462
- // 同时出现在卡片流与「已删除的内置角色」恢复区。
463
- if (DEFAULT_ROLES.indexOf(name) !== -1) { setNote('内置角色不可新建("' + name + '"如已删除,请在下方「已删除的内置角色」区恢复)'); return; }
1147
+ // 「新建自定义 agent 角色」三件套(原原生 prompt 实现改为模态框,
1148
+ // 弹窗本体见下方 roleAddModalOf):open 开弹窗,close 收起,submit
1149
+ // 做校验 + 创建。校验失败走弹窗内错误行,不再用原生弹窗 / toast。
1150
+ function openRoleAddModal() {
1151
+ setRoleAddErr(""); // 上次失败原因开弹窗即清;roleAddName / roleAddDispName
1152
+ // 故意不清——与 closeWsModal「取消保留残值」同语义,重开接着上次输入续填。
1153
+ setRoleAddOpen(true);
1154
+ }
1155
+
1156
+ function submitRoleAdd() {
1157
+ var name = String(roleAddName || "").trim();
1158
+ // 空名静默吞掉:确认按钮在空值时本来就 disabled,这里只兜输入框直接
1159
+ // 按 Enter 的路径(与旧 prompt 实现的 if (!name) return 同语义)。
1160
+ if (name === "") return;
1161
+ if (!/^[a-z][a-z0-9_-]{1,31}$/.test(name)) { setRoleAddErr("角色名须为 2-32 位小写字母/数字/下划线/连字符"); return; }
1162
+ // 内置名不可经「新建」再造:已删除(不在 roles、在 roles_remove)的
1163
+ // 内置名在此拦截并引导走恢复区,避免同一内置名同时出现在卡片流与
1164
+ // 「已删除的内置角色」恢复区;仍正常存在的内置名由下方「已存在」命中。
1165
+ if (DEFAULT_ROLES.indexOf(name) !== -1) { setRoleAddErr('内置角色不可新建("' + name + '"如已删除,请在下方「已删除的内置角色」区恢复)'); return; }
1166
+ // 已存在检查从 setAssign updater 挪到外层:报错要走弹窗内错误行,
1167
+ // setState 副作用不能塞进 updater;闭包 assign 与点击时刻一致,双击
1168
+ // 极端场景下重复创建也只是同形状覆盖,幂等无害。
1169
+ if (assign.roles[name]) { setRoleAddErr('角色 "' + name + '" 已存在'); return; }
1170
+ // 显示名校验与 roleTitleBlock 的 commitRoleName 同源(不含 \r \n、
1171
+ // trim 后 ≤60、空 = null):新建时就把显示名写进出厂形状,省得建完
1172
+ // 再去卡片里补一枪。报错走弹窗内错误行(不 toast)——此刻弹窗还开着,
1173
+ // 错误行就贴在输入框下面,比 toast 更贴输入现场。
1174
+ var dispRaw = String(roleAddDispName == null ? "" : roleAddDispName);
1175
+ if (/[\r\n]/.test(dispRaw)) { setRoleAddErr("显示名不能包含换行(\\r / \\n)"); return; }
1176
+ var dispTrim = dispRaw.trim();
1177
+ if (dispTrim.length > 60) { setRoleAddErr("显示名过长:trim 后最多 60 字符"); return; }
1178
+ var disp = dispTrim === "" ? null : dispTrim; // 空 / 纯空白 = null:事件流标题回落 toolName
464
1179
  setAssign(function (prev) {
465
- if (prev.roles[name]) { setNote('角色 "' + name + '" 已存在'); return prev; }
466
1180
  var next = JSON.parse(JSON.stringify(prev));
467
1181
  // 新角色全字段出厂形状:与 buildAssignments / restoreBuiltinRole 条目一致
468
- //(model/provider 置 null = 跟随主 agent 当前模型)。
469
- next.roles[name] = { persona: null, name: null, model: null, provider: null, tools: (data && data.inventory) || [] };
1182
+ //(model/provider 置 null = 跟随主 agent 当前模型;background_mode
1183
+ // 用弹窗单选值,字面量 'one-shot' | 'continuable')。name 是可选
1184
+ // 显示名(null = 用 toolName 当标题),不再恒 null。
1185
+ next.roles[name] = {
1186
+ persona: null,
1187
+ name: disp,
1188
+ model: null,
1189
+ provider: null,
1190
+ background_mode: roleAddBgMode,
1191
+ // 出厂 tools 默认全选 = 核心工具面(customCoreToolsOf:restrictBase
1192
+ // 去四个委派名与 mcp__ 工具)+ host 检测库存。两集合不相交,无需去重
1193
+ //(render 侧 roleCandidateTools 拼候选时自带去重兜底)。
1194
+ tools: customCoreToolsOf(data).concat((data && data.inventory) || []),
1195
+ };
470
1196
  return next;
471
1197
  });
1198
+ setRoleAddOpen(false);
1199
+ setRoleAddName("");
1200
+ setRoleAddDispName("");
1201
+ setRoleAddBgMode("one-shot"); // 成功提交即复位出厂默认(取消重开才保留残值)
1202
+ setRoleAddErr("");
1203
+ toast.ok('已新建角色 "' + name + '",记得「保存并应用」');
472
1204
  }
473
1205
 
474
- function removeRole(name) {
475
- if (!window.confirm('删除自定义角色 "' + name + '"?')) return;
1206
+ // 自定义角色改名(toolName = 委派调用名):名称是身份键,改它要联动
1207
+ // 迁移 skills 里的角色引用(skills.<技能> = [角色名…])。roles 键按原
1208
+ // 顺序重建(旧名位置原位替换);校验与新建同源(2-32 位小写字母/数字/
1209
+ // 下划线/连字符),内置名 / 已存在的名字一律拦。只改当前视角的
1210
+ // assignments——工作区视角改的是该工作区条目,别的视角不动(与编辑
1211
+ // 语义一致)。应用后生成侧换 delegation-<新名> 委派块,旧调用名消失,
1212
+ // 需重启 / 新会话生效。
1213
+ // 原原生 prompt 实现改为模态框三件套(弹窗本体见 72 片段的
1214
+ // roleRenameModalOf):open 开窗预填旧名,close 收起,submit 走校验链。
1215
+ // 校验失败走弹窗内错误行,不再 toast。
1216
+ function renameRole(name) {
1217
+ setRoleRenameVal(name); // 预填当前名:多数场景只改一两个字
1218
+ setRoleRenameErr("");
1219
+ setRoleRenameFor(name);
1220
+ }
1221
+
1222
+ function submitRoleRename() {
1223
+ var name = roleRenameFor; // 旧名(身份键)
1224
+ var v = String(roleRenameVal || "").trim();
1225
+ // 未改动:原 prompt 下按 Enter 即关窗(只是没改名),这里对齐——直接收起。
1226
+ if (v === name) { closeRoleRenameModal(); return; }
1227
+ if (!/^[a-z][a-z0-9_-]{1,31}$/.test(v)) { setRoleRenameErr("角色名须为 2-32 位小写字母/数字/下划线/连字符"); return; }
1228
+ if (DEFAULT_ROLES.indexOf(v) !== -1) { setRoleRenameErr('不可改成内置角色名 "' + v + '"'); return; }
1229
+ // 已存在检查从 setAssign updater 挪到外层(与 submitRoleAdd 同款):
1230
+ // 报错要走弹窗内错误行,setState 副作用不能塞进 updater;闭包 assign
1231
+ // 与点击时刻一致。
1232
+ if (assign.roles[v]) { setRoleRenameErr('角色 "' + v + '" 已存在'); return; }
476
1233
  setAssign(function (prev) {
477
1234
  var next = JSON.parse(JSON.stringify(prev));
478
- delete next.roles[name];
479
- Object.keys(next.skills).forEach(function (s) {
480
- next.skills[s] = next.skills[s].filter(function (r) { return r !== name; });
1235
+ var roles = {};
1236
+ Object.keys(next.roles).forEach(function (k) { roles[k === name ? v : k] = next.roles[k]; });
1237
+ next.roles = roles;
1238
+ Object.keys(next.skills || {}).forEach(function (sk) {
1239
+ var arr = next.skills[sk];
1240
+ if (Array.isArray(arr) && arr.indexOf(name) !== -1) {
1241
+ next.skills[sk] = arr.map(function (r) { return r === name ? v : r; });
1242
+ }
481
1243
  });
482
1244
  return next;
483
1245
  });
1246
+ // 卡片开合状态跟着换键(旧键遗留无碍,顺手清干净)
1247
+ setOpenMap(function (m) {
1248
+ if (!(name in m)) return m;
1249
+ var c = Object.assign({}, m);
1250
+ c[v] = c[name];
1251
+ delete c[name];
1252
+ return c;
1253
+ });
1254
+ closeRoleRenameModal();
1255
+ setRoleRenameVal("");
1256
+ setRoleRenameErr("");
1257
+ toast.ok('已重命名 "' + name + '" → "' + v + '",记得「保存并应用」');
1258
+ }
1259
+
1260
+ function removeRole(name) {
1261
+ // 原原生 confirm 改通用确认弹窗:删除属破坏性操作,danger 红底确认钮。
1262
+ confirmCtrl.ask({
1263
+ title: "删除自定义角色",
1264
+ body: "删除自定义角色 \"" + name + "\"?",
1265
+ danger: true,
1266
+ confirmText: "确认删除",
1267
+ onConfirm: function () {
1268
+ setAssign(function (prev) {
1269
+ var next = JSON.parse(JSON.stringify(prev));
1270
+ delete next.roles[name];
1271
+ Object.keys(next.skills).forEach(function (s) {
1272
+ next.skills[s] = next.skills[s].filter(function (r) { return r !== name; });
1273
+ });
1274
+ return next;
1275
+ });
1276
+ },
1277
+ });
484
1278
  }
485
1279
 
486
1280
  // 内置角色删除:roles 删 key + skills 引用剔除 + roles_remove 记名(去重)。
487
1281
  // 后端语义:roles_remove 里的内置名 = 已删除(卡片消失、生成不再委派),
488
1282
  // 可经 restoreBuiltinRole 恢复;自定义角色删除语义不变(只删 roles key,不进 roles_remove)。
489
1283
  function removeBuiltinRole(name) {
490
- if (!window.confirm('删除内置角色 "' + name + '"?该角色的委派工具、工具面、persona 与主 agent 的委派指引将一并移除;可在下方「已删除的内置角色」区一键恢复。')) return;
491
- setAssign(function (prev) {
492
- var next = JSON.parse(JSON.stringify(prev));
493
- delete next.roles[name];
494
- Object.keys(next.skills).forEach(function (s) {
495
- next.skills[s] = next.skills[s].filter(function (r) { return r !== name; });
496
- });
497
- next.roles_remove = next.roles_remove || [];
498
- if (next.roles_remove.indexOf(name) === -1) next.roles_remove.push(name);
499
- return next;
1284
+ // 原原生 confirm 改通用确认弹窗;文案含恢复引导(多行由
1285
+ // pd-modalText 的 pre-line 保留换行),danger 红底确认钮。
1286
+ confirmCtrl.ask({
1287
+ title: "删除内置角色",
1288
+ body: "删除内置角色 \"" + name + "\"?\n该角色的委派工具、工具面、persona 与主 agent 的委派指引将一并移除;\n可在下方「已删除的内置角色」区一键恢复。",
1289
+ danger: true,
1290
+ confirmText: "确认删除",
1291
+ onConfirm: function () {
1292
+ setAssign(function (prev) {
1293
+ var next = JSON.parse(JSON.stringify(prev));
1294
+ delete next.roles[name];
1295
+ Object.keys(next.skills).forEach(function (s) {
1296
+ next.skills[s] = next.skills[s].filter(function (r) { return r !== name; });
1297
+ });
1298
+ next.roles_remove = next.roles_remove || [];
1299
+ if (next.roles_remove.indexOf(name) === -1) next.roles_remove.push(name);
1300
+ return next;
1301
+ });
1302
+ },
500
1303
  });
501
1304
  }
502
1305
 
@@ -504,57 +1307,98 @@ window.__ModuleLoader__.load({
504
1307
  // (data.blocks[name]),不沿用删前自定义配置——恢复即出厂默认,简化处理。
505
1308
  // 显示名同样置 null(出厂默认),重建条目与 buildAssignments 形状一致。
506
1309
  // 专用模型 / provider 同理置 null:恢复 = 回到「跟随主 agent 当前模型」,
507
- // 不保留删前的模型固定配置。
1310
+ // 不保留删前的模型固定配置。background_mode 置 'one-shot'(出厂缺省)。
508
1311
  function restoreBuiltinRole(name) {
509
1312
  setAssign(function (prev) {
510
1313
  var next = JSON.parse(JSON.stringify(prev));
511
1314
  next.roles_remove = (next.roles_remove || []).filter(function (r) { return r !== name; });
512
1315
  var defTools = (data && data.blocks && data.blocks[name]) || [];
513
- next.roles[name] = { persona: null, name: null, model: null, provider: null, tools: defTools.slice() };
1316
+ next.roles[name] = { persona: null, name: null, model: null, provider: null, background_mode: "one-shot", tools: defTools.slice() };
514
1317
  return next;
515
1318
  });
516
1319
  }
517
1320
 
518
1321
  function doPreview() {
519
1322
  if (!assign) return;
1323
+ var reqAssign = assign;
520
1324
  setBusy(true);
521
1325
  setBusyOp("preview");
522
1326
  setPreview(null);
523
- api("preview", { assignments: assign }).then(function (r) {
1327
+ // workspace 透传当前配置对象(null = 全局默认),服务端据此选生成落点。
1328
+ api("preview", { assignments: reqAssign, workspace: activeWsRef.current }).then(function (r) {
1329
+ if (assignRef.current !== reqAssign) return; // 生成期间配置已改:结果过期,不挂旧预览
524
1330
  setPreview(r);
525
1331
  setScrollReq("preview"); // 预览成功后自动滚动定位到预览生成区
526
- setNote("");
527
- }).catch(function (err) { setError(err.message); }).finally(function () {
1332
+ }).catch(function (err) { toast.err(err.message); }).finally(function () {
528
1333
  setBusy(false);
529
1334
  setBusyOp(null);
530
1335
  });
531
1336
  }
532
1337
 
1338
+ // 预览时效:assign 一变即作废已挂出的预览——预览文本对应生成时刻的目标
1339
+ // 配置,继续挂着「✅ 生成成功」只会误导(switchWorkspace 自带
1340
+ // setPreview(null) 只覆盖切视角,这里兜住其余一切编辑路径;生成中的
1341
+ // 请求由 doPreview 里的 assignRef 快照比对作废)。
1342
+ useEffect(function () {
1343
+ setPreview(null);
1344
+ }, [assign]);
1345
+
533
1346
  function doApply() {
534
1347
  if (!assign) return;
535
- if (!window.confirm("确认应用当前配置?将写入 preset 并覆盖 ~/.dsh/.agent-presets/orchestrator,且保存到 dsh-paoding.config.yml。")) return;
536
- setBusy(true);
537
- setBusyOp("apply");
538
- api("apply", { assignments: assign }).then(function (r) {
539
- var notes = (r.roleResults || []).map(function (x) {
540
- return x.role + ": " + x.intent.length + " → " + x.kept.length;
541
- });
542
- setNote("✅ 已安装:" + notes.join(";") + "。" + (r.staleNote ? " ⚠️ patch 较新,重启后生效。" : " 重启 DSH 或新建会话后生效。"));
543
- }).catch(function (err) { setError(err.message); }).finally(function () {
544
- setBusy(false);
545
- setBusyOp(null);
1348
+ var wsPath = activeWsRef.current;
1349
+ // 确认文案按配置对象分流(语义原样保留):全局覆盖共享的 orchestrator
1350
+ // 预设(现状文案);工作区落独立预设、只并进 workspaces 段,全局预设
1351
+ // 不受影响。写预设非破坏性操作,danger 不置位(主色确认钮)。
1352
+ var applyMsg = wsPath
1353
+ ? "确认应用当前配置?将为该工作区生成专属预设(orchestrator-<slug>)并保存到 dsh-paoding.config.yml 的 workspaces 段,全局 orchestrator 预设不受影响。"
1354
+ : "确认应用当前配置?将写入 preset 并覆盖 ~/.dsh/.agent-presets/orchestrator,且保存到 dsh-paoding.config.yml。";
1355
+ confirmCtrl.ask({
1356
+ title: "保存并应用",
1357
+ body: applyMsg,
1358
+ danger: false,
1359
+ confirmText: "确认应用",
1360
+ onConfirm: function () {
1361
+ setBusy(true);
1362
+ setBusyOp("apply");
1363
+ api("apply", { assignments: assign, workspace: wsPath }).then(function (r) {
1364
+ var notes = (r.roleResults || []).map(function (x) {
1365
+ return x.role + ": " + x.intent.length + " → " + x.kept.length;
1366
+ });
1367
+ var summary = notes.join(";") + "。";
1368
+ if (wsPath) {
1369
+ // 工作区应用:点明生成的 preset id,提示去预设选择器选用(工作区
1370
+ // 预设不会自动成为默认,须在新建会话时显式选择)。
1371
+ toast.ok("已生成预设 " + (r.presetId || "orchestrator-<slug>") + ":" + summary + "新建会话时在预设选择器选它。" + (r.staleNote ? " ⚠️ patch 较新,重新应用后生效。" : ""));
1372
+ // 服务端 apply 后已重刷状态缓存:静默拉一次,让 workspaceMeta /
1373
+ // existing.workspaces 跟上(全局路径保持旧交互,不额外拉取)。
1374
+ api("state").then(function (d) {
1375
+ setData(d);
1376
+ setAssign(buildTargetAssignments(d, activeWsRef.current));
1377
+ }).catch(function () { /* 静默:落盘已成功,仅展示层暂旧 */ });
1378
+ } else {
1379
+ toast.ok("已安装:" + summary + (r.staleNote ? " ⚠️ patch 较新,重启后生效。" : " 重启 DSH 或新建会话后生效。"));
1380
+ }
1381
+ // 显示名 / 角色名单可能变了:事件流委派行键控注册跟着刷新(行组件
1382
+ // 标题读 delegateMeta,刷新即可见,无需重开会话)。
1383
+ refreshDelegateRows();
1384
+ }).catch(function (err) { toast.err(err.message); }).finally(function () {
1385
+ setBusy(false);
1386
+ setBusyOp(null);
1387
+ });
1388
+ },
546
1389
  });
547
1390
  }
548
1391
 
549
1392
  function doRescan() {
550
1393
  setBusy(true);
551
1394
  setBusyOp("rescan");
552
- setScrollReq("top"); // 立即滚动回面板顶部看状态说明(不等接口返回)
1395
+ setScrollReq("top"); // 立即滚动回面板顶部(不等接口返回,结果经 toast 播报)
553
1396
  api("rescan", {}).then(function (d) {
554
1397
  setData(d);
555
- setAssign(buildAssignments(d));
556
- setNote("↻ 已重新检测");
557
- }).catch(function (err) { setError(err.message); }).finally(function () {
1398
+ setAssign(buildTargetAssignments(d, activeWsRef.current));
1399
+ // 回调参数 d 即重扫后的新 state,直接报数字,补回原徽标条的信息量
1400
+ toast.ok("已重新检测:" + (d.inventory || []).length + " 个 MCP / 插件工具 · " + (d.skills || []).length + " 个技能");
1401
+ }).catch(function (err) { toast.err(err.message); }).finally(function () {
558
1402
  setBusy(false);
559
1403
  setBusyOp(null);
560
1404
  });
@@ -563,7 +1407,7 @@ window.__ModuleLoader__.load({
563
1407
  function toggleRole(name) {
564
1408
  setOpenMap(function (prev) {
565
1409
  var next = Object.assign({}, prev);
566
- next[name] = !(name in prev ? prev[name] : name === "__main__" || name === "search_external");
1410
+ next[name] = !(name in prev ? prev[name] : defaultOpenOf(name));
567
1411
  return next;
568
1412
  });
569
1413
  }
@@ -578,29 +1422,387 @@ window.__ModuleLoader__.load({
578
1422
 
579
1423
  // ── 渲染 ───────────────────────────────────────────────────────────────
580
1424
 
581
- function statusBarOf() {
582
- if (error) return statusRow("omd-statusError", ic(P.IconWarningOutline16, 16), error);
583
- if (note.indexOf("✅") === 0) return statusRow("omd-statusOk", null, note);
584
- if (note) return statusRow("omd-statusWarn", null, note);
585
- if (data) return statusRow("omd-status", ic(P.IconDataOutline16, 14),
586
- "检测到 " + (data.inventory || []).length + " 个 host 工具 · " +
587
- (data.skills || []).length + " 个技能 · config: " + (data.configFile || "默认"));
588
- return null;
1425
+ // 配置对象标签页(tablist):「全局默认」+ 各工作区(preset 已生成的
1426
+ // 前缀小圆点标记,元数据来自 state.workspaceMeta)+「更多」下拉 + 末尾
1427
+ // 「+ 添加工作区」按钮(点击打开输入弹窗,弹窗由 wsModalOf 渲染)。露出
1428
+ // 几个 tab 不再是固定数:ConfigPanel 的装填算法按标签行实宽算出
1429
+ // wsFitCount,本函数照它拆可见 / 隐藏两段;行内另挂一个常驻隐藏测量层
1430
+ // (omd-tabsMeasure)供算法读每个 tab 的自然宽度,测量未就绪时
1431
+ // wsFitCount 停在兜底值 5(WS_TABS_FALLBACK_COUNT)。tab 点击语义与旧
1432
+ // pill 行一致 = switchWorkspace(逻辑不动,只换了皮)。
1433
+ function workspaceBarOf() {
1434
+ var rows = workspaceRowsOf(data, wsList);
1435
+ var meta = (data && data.workspaceMeta) || {};
1436
+ var existingWs = (data && data.existing && data.existing.workspaces) || {};
1437
+ // 可见 / 隐藏拆分:显示顺序里激活工作区恒排最前,它天然留在可见区;
1438
+ // 下标落在 wsFitCount 之后的收进「更多」菜单。
1439
+ var order = wsDisplayOrderOf(rows, activeWs);
1440
+ var visible = order.slice(0, Math.min(order.length, wsFitCount));
1441
+ var hidden = order.slice(visible.length);
1442
+ var tabs = [h("button", {
1443
+ key: "__global__",
1444
+ type: "button",
1445
+ role: "tab",
1446
+ "aria-selected": activeWs === null ? "true" : "false",
1447
+ className: "omd-tab" + (activeWs === null ? " omd-tabActive" : ""),
1448
+ title: "全局默认:写入共享的 orchestrator 预设",
1449
+ onClick: function () { switchWorkspace(null); },
1450
+ }, h("span", { className: "omd-tabText" }, "全局默认"))];
1451
+ tabs = tabs.concat(visible.map(function (row) {
1452
+ var kind = wsTagOf(existingWs, meta, row.path);
1453
+ var tip = wsTabTipOf(row, meta, existingWs);
1454
+ return h("button", {
1455
+ key: row.path,
1456
+ type: "button",
1457
+ role: "tab",
1458
+ "aria-selected": activeWs === row.path ? "true" : "false",
1459
+ className: "omd-tab" + (activeWs === row.path ? " omd-tabActive" : ""),
1460
+ title: tip,
1461
+ onClick: function () { switchWorkspace(row.path); },
1462
+ },
1463
+ h("span", { className: "omd-tabText" }, wsLabelOf(row)),
1464
+ h("span", { className: "omd-tabTag omd-tabTag" + kind.charAt(0).toUpperCase() + kind.slice(1) },
1465
+ wsTagTextOf(kind)),
1466
+ row.sessionCount != null ? h("span", { className: "omd-tabCount" }, row.sessionCount + " 会话") : null);
1467
+ }));
1468
+ // 「更多」入口:仅在确有收进来的工作区时出现。正常路径激活项按显示
1469
+ // 顺序排最前、必然可见,这里仍兜底判一次——首帧 wsFitCount 还是兜底
1470
+ // 值时激活项可能暂居 hidden,就双挂 omd-tabMoreActive + omd-tabActive
1471
+ // 借用激活视觉(主色 + 激活条,CSS 不另画一份)。
1472
+ var hiddenActive = false;
1473
+ for (var k = 0; k < hidden.length; k++) {
1474
+ if (hidden[k].path === activeWs) { hiddenActive = true; break; }
1475
+ }
1476
+ if (hidden.length > 0) {
1477
+ tabs.push(h("button", {
1478
+ key: "__more__",
1479
+ type: "button",
1480
+ className: "omd-tab omd-tabMore" + (hiddenActive ? " omd-tabMoreActive omd-tabActive" : ""),
1481
+ title: "其余 " + hidden.length + " 个工作区",
1482
+ "aria-haspopup": "listbox",
1483
+ "aria-expanded": wsMenuOpen ? "true" : "false",
1484
+ onClick: function () { setWsMenuOpen(!wsMenuOpen); },
1485
+ },
1486
+ h("span", { className: "omd-tabText" }, "更多"),
1487
+ h("span", { className: "omd-tabMoreCount" }, String(hidden.length))));
1488
+ }
1489
+ // 末尾「+ 添加工作区」:与 tab 同尺寸的弱化按钮(ghost),点击打开输入弹窗。
1490
+ tabs.push(h("button", {
1491
+ key: "__add__",
1492
+ type: "button",
1493
+ className: "omd-tab omd-tabAdd",
1494
+ title: "注册新的工作区目录",
1495
+ disabled: busy,
1496
+ onClick: function () { setWsAddOpen(true); setWsAddErr(""); },
1497
+ },
1498
+ h("span", { className: "omd-groupIcon" }, ic(P.IconPlusOutline16, 14)),
1499
+ h("span", { className: "omd-tabText" }, "添加工作区")));
1500
+ // 测量层单件:与真实 tab 同款 .omd-tab 类,自然宽度因此一致;不进
1501
+ // a11y 树、不可聚焦,只负责被装填算法 offsetWidth 量。
1502
+ function measureTabOf(key, label) {
1503
+ return h("button", { type: "button", "data-ws-key": key, className: "omd-tab", tabIndex: -1 },
1504
+ h("span", { className: "omd-tabText" }, label));
1505
+ }
1506
+ // 行包裹层 omd-tabsRow:给「更多」下拉做锚点——挂在整列容器上的话
1507
+ // top:100% 会落到提示行下面,视觉上和标签行脱节;锚在行包裹层上,
1508
+ // 下拉恰好贴着标签行的下边框展开。
1509
+ return h("div", { className: "omd-wsTabs" },
1510
+ h("div", { className: "omd-tabsRow" },
1511
+ h("div", { className: "omd-tabs", role: "tablist", "aria-label": "配置对象", ref: tabsRowRef }, tabs),
1512
+ // 宽度测量层:常驻渲染(不随 wsFitCount 变),挪出视口且不可见,只把
1513
+ // 各 tab 的自然宽度喂给装填算法。「更多」原型计数固定写 99 按两位数
1514
+ // 算宽,与装填里的 SAFETY 余量搭配。
1515
+ h("div", { className: "omd-tabsMeasure", ref: tabsMeasureRef, "aria-hidden": "true" },
1516
+ measureTabOf("__global__", "全局默认"),
1517
+ order.map(function (row) {
1518
+ var m = meta[row.path];
1519
+ var mk = wsTagOf(existingWs, meta, row.path);
1520
+ return h("button", { key: row.path, type: "button", "data-ws-key": row.path, className: "omd-tab", tabIndex: -1 },
1521
+ h("span", { className: "omd-tabText" }, wsLabelOf(row)),
1522
+ h("span", { className: "omd-tabTag omd-tabTag" + mk.charAt(0).toUpperCase() + mk.slice(1) },
1523
+ wsTagTextOf(mk)),
1524
+ row.sessionCount != null ? h("span", { className: "omd-tabCount" }, row.sessionCount + " 会话") : null);
1525
+ }),
1526
+ h("button", { type: "button", "data-ws-key": "__more__", className: "omd-tab omd-tabMore", tabIndex: -1 },
1527
+ h("span", { className: "omd-tabText" }, "更多"),
1528
+ h("span", { className: "omd-tabMoreCount" }, "99")),
1529
+ h("button", { type: "button", "data-ws-key": "__add__", className: "omd-tab omd-tabAdd", tabIndex: -1 },
1530
+ h("span", { className: "omd-groupIcon" }, ic(P.IconPlusOutline16, 14)),
1531
+ h("span", { className: "omd-tabText" }, "添加工作区"))),
1532
+ // 「更多」下拉:菜单项信息结构与 tab 同款(小圆点 / 名称 / 会话数,
1533
+ // title 拼法也一致),点击直接切过去。role=listbox/option 表达
1534
+ // 「从清单里选一个配置对象」的语义,aria-selected 标当前项。
1535
+ wsMenuOpen && hidden.length > 0
1536
+ ? h("div", { className: "omd-tabMenu", role: "listbox", "aria-label": "更多工作区" },
1537
+ hidden.map(function (row) {
1538
+ var kind = wsTagOf(existingWs, meta, row.path);
1539
+ var tip = wsTabTipOf(row, meta, existingWs);
1540
+ return h("button", {
1541
+ key: row.path,
1542
+ type: "button",
1543
+ role: "option",
1544
+ "aria-selected": activeWs === row.path ? "true" : "false",
1545
+ className: "omd-tabMenuItem" + (activeWs === row.path ? " omd-tabMenuItemActive" : ""),
1546
+ title: tip,
1547
+ onClick: function () { setWsMenuOpen(false); switchWorkspace(row.path); },
1548
+ },
1549
+ h("span", { className: "omd-tabText" }, wsLabelOf(row)),
1550
+ h("span", { className: "omd-tabTag omd-tabTag" + kind.charAt(0).toUpperCase() + kind.slice(1) },
1551
+ wsTagTextOf(kind)),
1552
+ row.sessionCount != null ? h("span", { className: "omd-tabCount" }, row.sessionCount + " 会话") : null);
1553
+ }))
1554
+ : null),
1555
+ h("div", { className: "omd-hint" },
1556
+ "「全局默认」写入共享的 orchestrator 预设;选中某个工作区后,预览 / 应用会生成该工作区专属预设(orchestrator-<目录名>),两者互不影响。"));
1557
+ }
1558
+
1559
+ // 「添加工作区」弹窗:旧版是 tab 条下方的内联展开行,改为模态框输入。
1560
+ // 右上 × / 取消 / 点遮罩空白处 / Esc 都只收弹窗(Esc 分层见上方捕获级
1561
+ // useEffect);确认按钮与输入框 Enter 走 submitWorkspace——成功路径自己
1562
+ // 关弹窗、刷新列表并切到新工作区,这里不重复收尾。
1563
+ function wsModalOf() {
1564
+ if (!wsAddOpen) return null;
1565
+ // 建议行:候选按当前输入子串过滤(下拉展开才算);没拉到候选或无匹配
1566
+ // 时是空数组,下拉整块不渲染。(随旧内联行整体迁入弹窗)
1567
+ var wsSuggestRows = wsSuggestOpen ? wsSuggestionsOf(wsCands, wsAddPath) : [];
1568
+ return h("div", { className: "pd-modalMask",
1569
+ // 只点遮罩空白处才收(mousedown 真正落在遮罩上),在卡片内按下拖选不误关
1570
+ onMouseDown: function (e) { if (e.target === e.currentTarget) closeWsModal(); } },
1571
+ h("div", { className: "pd-modalCard", role: "dialog", "aria-modal": "true", "aria-label": "添加工作区" },
1572
+ h("div", { className: "pd-modalHeader" },
1573
+ h("h2", { className: "pd-modalTitle" }, "添加工作区"),
1574
+ h("button", { type: "button", className: "pd-modalClose", "aria-label": "关闭", onClick: closeWsModal },
1575
+ h("span", { className: "pd-closeGlyph", "aria-hidden": "true" }, "×"))),
1576
+ h("div", { className: "pd-modalBody" },
1577
+ h("label", { className: "pd-modalLabel" }, "工作区目录绝对路径"),
1578
+ h("div", { className: "omd-wsSuggestWrap" },
1579
+ h("input", {
1580
+ type: "text",
1581
+ className: "omd-nameInput omd-wsAddInput",
1582
+ value: wsAddPath,
1583
+ placeholder: "输入工作区目录绝对路径,如 /Users/me/code/project-a",
1584
+ spellCheck: false,
1585
+ autoFocus: true,
1586
+ onFocus: function () { loadWsCands(); setWsSuggestOpen(true); },
1587
+ onBlur: function () { setWsSuggestOpen(false); },
1588
+ onChange: function (e) { setWsAddPath(e.target.value); setWsSuggestOpen(true); },
1589
+ // 只认 Enter;Esc 不在这里拦——捕获级监听(见上方 useEffect)
1590
+ // 在 document 上先截停并 stopPropagation,事件到不了 React
1591
+ // 合成层,这里写 Escape 分支永远不会触发。
1592
+ onKeyDown: function (e) {
1593
+ if (e.key === "Enter") { setWsSuggestOpen(false); submitWorkspace(); }
1594
+ },
1595
+ }),
1596
+ wsSuggestRows.length
1597
+ ? h("div", { className: "omd-wsSuggest", role: "listbox" },
1598
+ wsSuggestRows.map(function (c) {
1599
+ return h("button", {
1600
+ key: c.path,
1601
+ type: "button",
1602
+ className: "omd-wsSuggestItem",
1603
+ title: c.path,
1604
+ disabled: busy,
1605
+ // mousedown 的默认行为会把焦点从输入框夺走,blur 先于
1606
+ // click 把下拉卸载、点击就落空——preventDefault 掐掉它,
1607
+ // input 保持聚焦,click 才能安稳到达建议行。
1608
+ onMouseDown: function (e) { e.preventDefault(); },
1609
+ onClick: function () {
1610
+ setWsSuggestOpen(false);
1611
+ setWsAddPath(c.path);
1612
+ submitWorkspace(c.path);
1613
+ },
1614
+ },
1615
+ h("span", { className: "omd-wsSuggestName" }, c.basename || c.path),
1616
+ h("span", { className: "omd-wsSuggestPath" }, c.path));
1617
+ }))
1618
+ : null),
1619
+ wsAddErr ? statusRow("omd-statusError", ic(P.IconWarningOutline16, 16), wsAddErr) : null,
1620
+ h("div", { className: "omd-hint" }, "目录需已存在;已注册的目录重复提交幂等返回。")),
1621
+ h("div", { className: "pd-modalFooter" },
1622
+ h(P.Button, { variant: "outline", size: "md", disabled: busy, onClick: closeWsModal }, "取消"),
1623
+ h(P.Button, {
1624
+ variant: "primary",
1625
+ size: "md",
1626
+ disabled: busy || String(wsAddPath).trim() === "",
1627
+ onClick: submitWorkspace,
1628
+ }, busyOp === "wsadd" ? "添加中…" : "确认添加"))));
1629
+ }
1630
+
1631
+ // 「新建自定义 agent 角色」弹窗:结构 / 关闭路径与「添加工作区」弹窗
1632
+ // (wsModalOf)完全一致——右上 × / 取消 / 点遮罩空白处 / Esc 都只收弹窗
1633
+ //(Esc 分层见上方捕获级 useEffect)。两字段表单的 Enter 分工:toolName
1634
+ // 框 Enter 不提交(用户多半还想填显示名),只把焦点挪到显示名框;显示
1635
+ // 名框 Enter 才走 submitRoleAdd——成功路径自己关弹窗并清残值,这里不
1636
+ // 重复收尾。新建是纯本地 setAssign 操作,无请求,按钮不挂 busy 态。
1637
+ function roleAddModalOf() {
1638
+ if (!roleAddOpen) return null;
1639
+ return h("div", { className: "pd-modalMask",
1640
+ // 只点遮罩空白处才收(mousedown 真正落在遮罩上),在卡片内按下拖选不误关
1641
+ onMouseDown: function (e) { if (e.target === e.currentTarget) closeRoleAddModal(); } },
1642
+ h("div", { className: "pd-modalCard", role: "dialog", "aria-modal": "true", "aria-label": "新建自定义 agent 角色" },
1643
+ h("div", { className: "pd-modalHeader" },
1644
+ h("h2", { className: "pd-modalTitle" }, "新建自定义 agent 角色"),
1645
+ h("button", { type: "button", className: "pd-modalClose", "aria-label": "关闭", onClick: closeRoleAddModal },
1646
+ h("span", { className: "pd-closeGlyph", "aria-hidden": "true" }, "×"))),
1647
+ h("div", { className: "pd-modalBody" },
1648
+ h("label", { className: "pd-modalLabel" }, "角色 toolName(主 agent 的委派调用名)"),
1649
+ h("input", {
1650
+ type: "text",
1651
+ className: "omd-nameInput",
1652
+ value: roleAddName,
1653
+ placeholder: "输入英文小写角色名,如 reviewer",
1654
+ spellCheck: false,
1655
+ autoFocus: true,
1656
+ onChange: function (e) { setRoleAddName(e.target.value); },
1657
+ // Enter 只移焦不提交:这是两字段表单的第一段,直接提交会跳过
1658
+ // 还没填的显示名。Esc 仍不在这里拦——捕获级监听(见上方
1659
+ // useEffect)在 document 上先截停并 stopPropagation,事件到不了
1660
+ // React 合成层,这里写 Escape 分支永远不会触发。
1661
+ onKeyDown: function (e) {
1662
+ if (e.key === "Enter") {
1663
+ var inp = roleAddDispRef.current; // 弹窗条件渲染,此刻必已挂载;仍判空兜极端时序
1664
+ if (inp && typeof inp.focus === "function") inp.focus();
1665
+ }
1666
+ },
1667
+ }),
1668
+ h("label", { className: "pd-modalLabel" }, "显示名(可选)"),
1669
+ h("input", {
1670
+ ref: roleAddDispRef,
1671
+ type: "text",
1672
+ className: "omd-nameInput",
1673
+ value: roleAddDispName,
1674
+ // 占位语义与 roleTitleBlock 的 subject=toolName 一致:留空 = 事件流标题回落 toolName
1675
+ placeholder: "如 代码评审,留空 = 用 toolName",
1676
+ spellCheck: false,
1677
+ onChange: function (e) { setRoleAddDispName(e.target.value); },
1678
+ // 第二段的 Enter 才是提交:第一段已把焦点送到这里,用户在此按
1679
+ // Enter 即视为整个表单填完。
1680
+ onKeyDown: function (e) {
1681
+ if (e.key === "Enter") submitRoleAdd();
1682
+ },
1683
+ }),
1684
+ // 「会话模式」单选(与角色卡 roleBgModeBlock 同款语义):缺省一次性,
1685
+ // 选「可续」才写 background_mode: 'continuable';提示 / 警告行内联。
1686
+ h("label", { className: "pd-modalLabel" }, "会话模式"),
1687
+ h("div", { className: "omd-nameRow" },
1688
+ h("label", { className: "omd-toolItem" },
1689
+ h("input", {
1690
+ type: "radio",
1691
+ className: "omd-check",
1692
+ name: "pd-bgm-roleadd",
1693
+ checked: roleAddBgMode !== "continuable",
1694
+ onChange: function () { setRoleAddBgMode("one-shot"); },
1695
+ }),
1696
+ h("span", { className: "omd-toolName" }, "一次性(one-shot)")),
1697
+ h("label", { className: "omd-toolItem" },
1698
+ h("input", {
1699
+ type: "radio",
1700
+ className: "omd-check",
1701
+ name: "pd-bgm-roleadd",
1702
+ checked: roleAddBgMode === "continuable",
1703
+ onChange: function () { setRoleAddBgMode("continuable"); },
1704
+ }),
1705
+ h("span", { className: "omd-toolName" }, "可续(continuable)"))),
1706
+ roleAddBgMode === "continuable"
1707
+ ? h("div", { className: "omd-hint" }, ROLE_BG_CONTINUABLE_HINT)
1708
+ : null,
1709
+ roleAddBgMode === "continuable" && data && data.persistenceAvailable === false
1710
+ ? statusRow("omd-statusWarn", ic(P.IconWarningOutline16, 16), ROLE_BG_PERSISTENCE_WARN)
1711
+ : null,
1712
+ roleAddErr ? statusRow("omd-statusError", ic(P.IconWarningOutline16, 16), roleAddErr) : null,
1713
+ h("div", { className: "omd-hint" },
1714
+ "角色名须为 2-32 位小写字母/数字/下划线/连字符,小写字母开头;内置名不可新建。显示名可选,不能换行,trim 后最多 60 字符,留空用 toolName 当标题。新角色工具面默认放开全部检测到的工具,可在卡片里再收窄。会话模式缺省一次性(one-shot),选「可续」后仍可在角色卡里改。")),
1715
+ h("div", { className: "pd-modalFooter" },
1716
+ h(P.Button, { variant: "outline", size: "md", onClick: closeRoleAddModal }, "取消"),
1717
+ h(P.Button, {
1718
+ variant: "primary",
1719
+ size: "md",
1720
+ disabled: String(roleAddName).trim() === "",
1721
+ onClick: submitRoleAdd,
1722
+ }, "确认新建"))));
1723
+ }
1724
+
1725
+ // 「重命名自定义角色」弹窗:结构 / 关闭路径与 roleAddModalOf 一致——
1726
+ // 右上 × / 取消 / 点遮罩空白处 / Esc 都只收弹窗(Esc 分层见 70 片段的
1727
+ // 捕获级 useEffect)。单字段表单:开窗预填旧名,Enter 直接提交(重命名
1728
+ // 没有第二段);校验失败走弹窗内错误行,成功路径由 submitRoleRename 关
1729
+ // 弹窗并清残值。改的是 toolName(身份键),显示名不在此处。
1730
+ function roleRenameModalOf() {
1731
+ if (!roleRenameFor) return null;
1732
+ return h("div", { className: "pd-modalMask",
1733
+ // 只点遮罩空白处才收(mousedown 真正落在遮罩上),在卡片内按下拖选不误关
1734
+ onMouseDown: function (e) { if (e.target === e.currentTarget) closeRoleRenameModal(); } },
1735
+ h("div", { className: "pd-modalCard", role: "dialog", "aria-modal": "true", "aria-label": "重命名自定义角色" },
1736
+ h("div", { className: "pd-modalHeader" },
1737
+ h("h2", { className: "pd-modalTitle" }, "重命名自定义角色"),
1738
+ h("button", { type: "button", className: "pd-modalClose", "aria-label": "关闭", onClick: closeRoleRenameModal },
1739
+ h("span", { className: "pd-closeGlyph", "aria-hidden": "true" }, "×"))),
1740
+ h("div", { className: "pd-modalBody" },
1741
+ h("label", { className: "pd-modalLabel" }, "新 toolName(主 agent 的委派调用名)"),
1742
+ h("input", {
1743
+ type: "text",
1744
+ className: "omd-nameInput",
1745
+ value: roleRenameVal,
1746
+ placeholder: roleRenameFor || "", // 占位即旧名(输入框已预填,占位只在清空后可见)
1747
+ spellCheck: false,
1748
+ autoFocus: true,
1749
+ onChange: function (e) { setRoleRenameVal(e.target.value); },
1750
+ // Esc 不在这里拦——捕获级监听(见 70 片段 useEffect)在 document
1751
+ // 上先截停并 stopPropagation,事件到不了 React 合成层,这里写
1752
+ // Escape 分支永远不会触发。
1753
+ onKeyDown: function (e) {
1754
+ if (e.key === "Enter") submitRoleRename();
1755
+ },
1756
+ }),
1757
+ roleRenameErr ? statusRow("omd-statusError", ic(P.IconWarningOutline16, 16), roleRenameErr) : null,
1758
+ h("div", { className: "omd-hint" },
1759
+ "2-32 位小写字母/数字/下划线/连字符,小写字母开头;不可改成内置名或已存在的名字。skills 里的引用自动迁移;改名后需重启 / 新会话生效。")),
1760
+ h("div", { className: "pd-modalFooter" },
1761
+ h(P.Button, { variant: "outline", size: "md", onClick: closeRoleRenameModal }, "取消"),
1762
+ h(P.Button, {
1763
+ variant: "primary",
1764
+ size: "md",
1765
+ // 空值 / 与旧名相同都禁用确认:后者点确认只会空转收窗,禁用
1766
+ // 让按钮状态与「确实改了名」对齐。
1767
+ disabled: String(roleRenameVal).trim() === "" || String(roleRenameVal).trim() === roleRenameFor,
1768
+ onClick: submitRoleRename,
1769
+ }, "确认重命名"))));
589
1770
  }
590
1771
 
591
1772
  if (!data || !assign) {
592
- return h("div", { className: "omd-section" },
593
- h("p", { className: "omd-intro" }, INTRO),
594
- h("div", { className: "omd-group" },
595
- h("div", { className: "omd-groupHeading" }, h("span", null, "状态与操作")),
596
- statusBarOf(),
597
- h("div", { className: "omd-loading" },
598
- h("span", { className: "omd-spin" }, ic(P.IconLoadingOutline16, 16)),
599
- h("span", null, error ? "加载失败" : "加载检测状态…"))));
1773
+ // 面板没起来(含 state 加载失败)才走这条整页分支:失败渲染常驻错误行,
1774
+ // 不进 toast——此刻页面还没有内容可依托,错误必须一直可见。
1775
+ return h("div", { className: "pd-page" },
1776
+ error
1777
+ ? statusRow("omd-statusError", ic(P.IconWarningOutline16, 16), error)
1778
+ : h("div", { className: "omd-loading" },
1779
+ h("span", { className: "omd-spin" }, ic(P.IconLoadingOutline16, 16)),
1780
+ h("span", null, "加载检测状态…")));
1781
+ }
1782
+
1783
+ // 技能网格数据源(主 agent 软引导/内联 + 下方角色 skills 分组共用):
1784
+ // 工作区视角(activeWs 非空)且已拉到 wsSkills 时用「用户级 + 该工作区
1785
+ // 项目级」合并列表;全局视角 / 拉取中 / 拉取失败降级一律回落 state 的
1786
+ // 用户级技能 data.skills。gridProj 仅在合并生效时非空(项目角标据此渲染)。
1787
+ var gridSkills = activeWs !== null && Array.isArray(wsSkills) ? wsSkills : (data.skills || []);
1788
+ var gridProj = activeWs !== null && wsProjSkills ? wsProjSkills : null;
1789
+
1790
+ // 技能网格作用域小字(分组标题旁):说明当前网格覆盖的技能来源——
1791
+ // 全局 = 仅用户级技能;工作区 = 用户级 + 该工作区项目级技能。
1792
+ function skillScopeNote() {
1793
+ return h("span", { className: "omd-skillScope" },
1794
+ activeWs !== null ? "用户级 + 该工作区项目级技能" : "仅用户级技能");
600
1795
  }
601
1796
 
1797
+ // 角色工具候选(三组网格共用数据源):所有角色(内置 + 自定义)= SRC
1798
+ // allow 面 ∪ 核心工具清单 ∪ host 检测库存。内置角色在 SRC 面上再并核心
1799
+ // 工具候选——勾选才生效,默认勾选仍来自 suggested/staticBase,默认面不
1800
+ // 变;自定义角色无 SRC 基础面,自然回落核心清单(customCoreToolsOf:去
1801
+ // 四个委派名与 mcp__ 工具)。分组口径:mcp__ 前缀 → 「MCP 工具」;其余
1802
+ // 名字里在 inventory 中的 → 「插件工具」(mnemon_* 等直注册工具);两者
1803
+ // 皆非 → 「基础工具」。全部拼接后按首次出现去重。
602
1804
  function roleCandidateTools(name) {
603
- var base = (data.blocks && data.blocks[name]) || [];
1805
+ var base = ((data.blocks && data.blocks[name]) || []).concat(customCoreToolsOf(data));
604
1806
  return base.concat(data.inventory || []).filter(function (t, i, arr) { return arr.indexOf(t) === i; });
605
1807
  }
606
1808
 
@@ -618,10 +1820,12 @@ window.__ModuleLoader__.load({
618
1820
  h("span", { className: "omd-personaEdit" }, ic(P.IconEditOutline16, 14))),
619
1821
  open ? h("div", { className: "omd-personaBody" },
620
1822
  h("textarea", {
1823
+ key: "omd-persona-" + name + "-" + switchRev, // 切视角重挂:defaultValue 显示新目标的 persona
621
1824
  className: "omd-textarea",
622
1825
  defaultValue: assign.roles[name].persona || "",
623
1826
  placeholder: "输入该角色的 persona 说明…",
624
1827
  spellCheck: false,
1828
+ disabled: busy,
625
1829
  onBlur: function (e) {
626
1830
  setAssign(function (prev) {
627
1831
  var next = JSON.parse(JSON.stringify(prev));
@@ -652,17 +1856,19 @@ window.__ModuleLoader__.load({
652
1856
  h("span", { className: "omd-personaEdit" }, ic(P.IconEditOutline16, 14))),
653
1857
  extraOpen ? h("div", { className: "omd-personaBody" },
654
1858
  h("textarea", {
655
- key: "omd-extra-" + extraRev,
1859
+ key: "omd-extra-" + extraRev + "-" + switchRev,
656
1860
  className: "omd-textarea omd-extraArea",
657
1861
  defaultValue: val,
658
1862
  placeholder: "留空并保存 = 不追加任何内容(默认 codegraph 规则将被移除)…",
659
1863
  spellCheck: false,
1864
+ disabled: busy,
660
1865
  onBlur: function (e) { setMainPersonaExtra(e.target.value); },
661
1866
  }),
662
1867
  h("div", { className: "omd-extraActions" },
663
1868
  h("button", {
664
1869
  type: "button",
665
1870
  className: "omd-extraRestore",
1871
+ disabled: busy,
666
1872
  onClick: restoreMainPersonaExtra,
667
1873
  }, "恢复默认"),
668
1874
  h("span", { className: "omd-extraHint" },
@@ -671,61 +1877,65 @@ window.__ModuleLoader__.load({
671
1877
  : null);
672
1878
  }
673
1879
 
674
- // 主 agent「名称」行:preset 显示名 + persona 身份行 label。默认文案
1880
+ // 主 agent「显示名」行(main_agent_display_name):仅替换 preset 显示名
1881
+ //(GUI 预设选择器里的名字),不动 persona 身份行——编排主 agent 的身份
1882
+ // 语义不属于可配置项,UI 不提供改名入口。默认文案
675
1883
  // 「编排模式 (Orchestrator)」为后端默认常量,UI 侧硬编码做占位(后端
676
1884
  // state 无该默认字段就不引,与 mainPersonaExtraDefault 处理思路一致)。
677
- // 写回校验与后端一致:trim 后 1-60 字符、不含 \r \n;真空字段 = 默认;
678
- // 非空纯空白视为非法(防误触把已有名称静默清回默认)。
1885
+ // 写回校验与后端一致:trim 后 1-60 字符、不含 \r \n;空 = null(默认显示
1886
+ // 名,工作区预设用派生名);非空纯空白视为非法(防误触把已有显示名静默
1887
+ // 清回默认)。
679
1888
  function mainNameBlock() {
680
1889
  var DEFAULT_NAME = "编排模式 (Orchestrator)";
681
- function commitName(raw) {
1890
+ function commitDisplayName(raw) {
682
1891
  var s = String(raw == null ? "" : raw);
683
1892
  if (/[\r\n]/.test(s)) {
684
- setNote("主 agent 名称不能包含换行(\\r / \\n),未保存");
685
- setNameRev(nameRev + 1); // 重挂 input,回到上次生效值
1893
+ toast.warn("主 agent 显示名(仅 preset 显示名)不能包含换行(\\r / \\n),未保存");
1894
+ setDisplayNameRev(displayNameRev + 1); // 重挂 input,回到上次生效值
686
1895
  return;
687
1896
  }
688
1897
  var v = s.trim();
689
1898
  if (s !== "" && v === "") {
690
- setNote("主 agent 名称不能为纯空白(清空字段或点「恢复默认」可回到默认名称),未保存");
691
- setNameRev(nameRev + 1);
1899
+ toast.warn("主 agent 显示名(仅 preset 显示名)不能为纯空白(清空字段或点「恢复默认」= 默认显示名),未保存");
1900
+ setDisplayNameRev(displayNameRev + 1);
692
1901
  return;
693
1902
  }
694
1903
  if (v.length > 60) {
695
- setNote("主 agent 名称过长:trim 后最多 60 字符,未保存");
696
- setNameRev(nameRev + 1);
1904
+ toast.warn("主 agent 显示名(仅 preset 显示名)过长:trim 后最多 60 字符,未保存");
1905
+ setDisplayNameRev(displayNameRev + 1);
697
1906
  return;
698
1907
  }
699
1908
  setAssign(function (prev) {
700
1909
  var next = JSON.parse(JSON.stringify(prev));
701
- next.main_agent_name = v === "" ? null : v;
1910
+ next.main_agent_display_name = v === "" ? null : v; // null = 默认显示名
702
1911
  return next;
703
1912
  });
704
- if (v === "") setNameRev(nameRev + 1); // 清空 = 默认:重挂显示占位
1913
+ if (v === "") setDisplayNameRev(displayNameRev + 1); // 清空 = 默认:重挂显示占位
705
1914
  }
706
- function restoreMainName() {
1915
+ function restoreMainDisplayName() {
707
1916
  setAssign(function (prev) {
708
1917
  var next = JSON.parse(JSON.stringify(prev));
709
- next.main_agent_name = null; // null = 默认显示名
1918
+ next.main_agent_display_name = null; // null = 默认显示名
710
1919
  return next;
711
1920
  });
712
- setNameRev(nameRev + 1);
1921
+ setDisplayNameRev(displayNameRev + 1);
713
1922
  }
714
1923
  return h("div", { className: "omd-mainName" },
715
- h("div", { className: "omd-glabel" }, "主 agent 名称(preset 显示名 + persona 身份行)"),
1924
+ h("div", { className: "omd-glabel" }, "主 agent 显示名(仅 preset 显示名)"),
716
1925
  h("div", { className: "omd-nameRow" },
717
1926
  h("input", {
718
- key: "omd-name-" + nameRev,
1927
+ key: "omd-displayName-" + displayNameRev + "-" + switchRev,
719
1928
  type: "text",
720
1929
  className: "omd-nameInput",
721
- defaultValue: assign.main_agent_name || "",
1930
+ defaultValue: assign.main_agent_display_name || "",
722
1931
  placeholder: DEFAULT_NAME,
723
1932
  spellCheck: false,
724
- onBlur: function (e) { commitName(e.target.value); },
1933
+ disabled: busy,
1934
+ onBlur: function (e) { commitDisplayName(e.target.value); },
725
1935
  }),
726
- h("button", { type: "button", className: "omd-extraRestore", onClick: restoreMainName }, "恢复默认")),
1936
+ h("button", { type: "button", className: "omd-extraRestore", disabled: busy, onClick: restoreMainDisplayName }, "恢复默认")),
727
1937
  h("div", { className: "omd-hint" },
728
- "保存并应用后 persona 首行与预设显示名一并替换;留空 = 默认「编排模式 (Orchestrator)」。改动需重启 GUI / 新会话生效。"));
1938
+ "仅替换预设显示名,persona 身份行(You are the …)不动;留空 = 默认「编排模式 (Orchestrator)」(工作区预设用派生名)。改动需重启 GUI / 新会话生效。"));
729
1939
  }
730
1940
 
731
1941
  // 内置角色「显示名」input 重挂(key 技巧):写回校验失败 / 清空回默认时
@@ -751,23 +1961,25 @@ window.__ModuleLoader__.load({
751
1961
  });
752
1962
  }
753
1963
 
754
- // 内置角色「显示名」行:仅替换该角色默认 persona 首行身份句的主语
755
- // (toolName / 调用名 / 工具面 / restrict 全不变);已自定义该角色
756
- // persona 时此项后端忽略(此处仅提示)。校验与主 agent 名同源约束:
757
- // trim 后 1-60 字符、不含 \r \n;空 / 纯空白 = 默认(写入 null)。
758
- // 自定义角色卡不加此输入(其 persona 首行由用户自写),不入此函数。
759
- function builtinNameBlock(name) {
760
- var subject = (ROLE_DEFAULT_SUBJECT[name] || name);
1964
+ // 角色「显示名」行(内置与自定义通用):
1965
+ // - 内置:替换默认 persona 首行身份句主语(persona 已自定义时后端
1966
+ // 忽略,仅提示),同时是事件流委派行的标题;
1967
+ // - 自定义:后端不消费此字段(其 persona 全文自写),纯粹用作事件流
1968
+ // 委派行标题——不动 toolName / 调用名 / persona。
1969
+ // 校验与主 agent 名同源约束:trim 后 1-60 字符、不含 \r \n;
1970
+ // 空 / 纯空白 = 默认(写入 null = 事件流回落缺省标题)。
1971
+ function roleTitleBlock(name, isCustom) {
1972
+ var subject = isCustom ? name : (ROLE_DEFAULT_SUBJECT[name] || name);
761
1973
  function commitRoleName(raw) {
762
1974
  var s = String(raw == null ? "" : raw);
763
1975
  if (/[\r\n]/.test(s)) {
764
- setNote('内置角色 "' + name + '" 显示名不能包含换行(\\r / \\n),未保存');
1976
+ toast.warn('角色 "' + name + '" 显示名不能包含换行(\\r / \\n),未保存');
765
1977
  bumpRoleNameRev(name); // 重挂 input,回到上次生效值
766
1978
  return;
767
1979
  }
768
1980
  var v = s.trim();
769
1981
  if (v.length > 60) {
770
- setNote('内置角色 "' + name + '" 显示名过长:trim 后最多 60 字符,未保存');
1982
+ toast.warn('角色 "' + name + '" 显示名过长:trim 后最多 60 字符,未保存');
771
1983
  bumpRoleNameRev(name);
772
1984
  return;
773
1985
  }
@@ -779,19 +1991,23 @@ window.__ModuleLoader__.load({
779
1991
  if (v === "") bumpRoleNameRev(name); // 空 / 纯空白 = 默认:重挂显示占位
780
1992
  }
781
1993
  return h("div", { className: "omd-mainName" },
782
- h("div", { className: "omd-glabel" }, "角色显示名(默认 persona 身份句主语;不动工具名/调用名)"),
1994
+ h("div", { className: "omd-glabel" },
1995
+ isCustom ? "角色显示名(事件流委派行标题;不动 toolName / persona)" : "角色显示名(persona 身份句主语 + 事件流委派行标题;不动工具名/调用名)"),
783
1996
  h("div", { className: "omd-nameRow" },
784
1997
  h("input", {
785
- key: "omd-roleName-" + name + "-" + ((roleNameRev && roleNameRev[name]) || 0),
1998
+ key: "omd-roleName-" + name + "-" + ((roleNameRev && roleNameRev[name]) || 0) + "-" + switchRev,
786
1999
  type: "text",
787
2000
  className: "omd-nameInput",
788
2001
  defaultValue: (assign.roles[name] && assign.roles[name].name) || "",
789
2002
  placeholder: subject,
790
2003
  spellCheck: false,
2004
+ disabled: busy,
791
2005
  onBlur: function (e) { commitRoleName(e.target.value); },
792
2006
  })),
793
2007
  h("div", { className: "omd-hint" },
794
- '留空 = 默认身份(如 "You are the ' + subject + ' agent.");已为该角色自定义 persona 时此项不生效。'));
2008
+ isCustom
2009
+ ? '留空 = 事件流显示 toolName(' + name + ');保存并应用后生效(仅 GUI 展示,不影响生成产物与调用名)。'
2010
+ : '留空 = 默认身份(如 "You are the ' + subject + ' agent.");已为该角色自定义 persona 时仅作事件流标题。'));
795
2011
  }
796
2012
 
797
2013
  // 角色「专用模型」行(内置与自定义角色通用同一组件,签名不变):
@@ -814,13 +2030,13 @@ window.__ModuleLoader__.load({
814
2030
  function commitRoleModel(key, raw) {
815
2031
  var s = String(raw == null ? "" : raw);
816
2032
  if (/[\r\n]/.test(s)) {
817
- setNote('角色 "' + name + '" ' + labelOf(key) + "不能包含换行(\\r / \\n),未保存");
2033
+ toast.warn('角色 "' + name + '" ' + labelOf(key) + "不能包含换行(\\r / \\n),未保存");
818
2034
  bumpRoleModelRev(name); // 重挂 input,回到上次生效值
819
2035
  return;
820
2036
  }
821
2037
  var v = s.trim();
822
2038
  if (v.length > 120) {
823
- setNote('角色 "' + name + '" ' + labelOf(key) + "过长:trim 后最多 120 字符,未保存");
2039
+ toast.warn('角色 "' + name + '" ' + labelOf(key) + "过长:trim 后最多 120 字符,未保存");
824
2040
  bumpRoleModelRev(name);
825
2041
  return;
826
2042
  }
@@ -836,7 +2052,7 @@ window.__ModuleLoader__.load({
836
2052
  });
837
2053
  // provider 非空而 model 为空 → 允许保存(后端原样落盘),但明确提示会被忽略
838
2054
  if (provider && !model) {
839
- setNote('角色 "' + name + '":provider 仅在同时配置模型时生效(后端将忽略),已保存');
2055
+ toast.warn('角色 "' + name + '":provider 仅在同时配置模型时生效(后端将忽略),已保存');
840
2056
  }
841
2057
  if (v === "") bumpRoleModelRev(name); // 清空 = 跟随主 agent:重挂显示占位
842
2058
  }
@@ -844,21 +2060,23 @@ window.__ModuleLoader__.load({
844
2060
  h("div", { className: "omd-glabel" }, "专用模型(可选;留空 = 跟随主 agent 当前模型)"),
845
2061
  h("div", { className: "omd-nameRow" },
846
2062
  h("input", {
847
- key: "omd-roleModel-" + name + "-" + rev,
2063
+ key: "omd-roleModel-" + name + "-" + rev + "-" + switchRev,
848
2064
  type: "text",
849
2065
  className: "omd-nameInput",
850
2066
  defaultValue: role.model || "",
851
2067
  placeholder: "deepseek-v4-pro",
852
2068
  spellCheck: false,
2069
+ disabled: busy,
853
2070
  onBlur: function (e) { commitRoleModel("model", e.target.value); },
854
2071
  }),
855
2072
  h("input", {
856
- key: "omd-roleProvider-" + name + "-" + rev,
2073
+ key: "omd-roleProvider-" + name + "-" + rev + "-" + switchRev,
857
2074
  type: "text",
858
2075
  className: "omd-nameInput",
859
2076
  defaultValue: role.provider || "",
860
2077
  placeholder: "provider(留空=跟随主 agent)",
861
2078
  spellCheck: false,
2079
+ disabled: busy,
862
2080
  onBlur: function (e) { commitRoleModel("provider", e.target.value); },
863
2081
  })),
864
2082
  h("div", { className: "omd-hint" },
@@ -900,6 +2118,7 @@ window.__ModuleLoader__.load({
900
2118
  h("select", {
901
2119
  key: "omd-roleModel-" + name, // 每角色独立实例;模式 / 选项集切换可整体重挂
902
2120
  className: "omd-nameInput",
2121
+ disabled: busy,
903
2122
  value: sel.value,
904
2123
  onChange: function (e) { commitRoleModelOption(e.target.value); },
905
2124
  }, children)),
@@ -907,11 +2126,50 @@ window.__ModuleLoader__.load({
907
2126
  "选项来自 DSH 已注册的模型路由;留空 = 跟随主 agent,主 agent 换模型时未固定的角色跟着变。"));
908
2127
  }
909
2128
 
2129
+ // 角色「会话模式」行(内置与自定义角色通用,同 roleModelBlock 的定位):
2130
+ // 单选 roles.<toolName>.background_mode ——「一次性(one-shot)」为缺省
2131
+ //(字面量 'one-shot',与 config 层 normalizeBackgroundMode 恒收两个合法
2132
+ // 值的序列化形状一致),「可续(continuable)」写 'continuable'。选中
2133
+ // 可续时行内展示成本提示;host 层 persistence 后端缺失
2134
+ //(state.persistenceAvailable === false,字段缺失的老服务端视为可用不
2135
+ // 误报)时再叠加委派将报错的警告——只警示,不拦截预览 / 应用。
2136
+ function roleBgModeBlock(name) {
2137
+ var continuable = !!(assign.roles[name] && assign.roles[name].background_mode === "continuable");
2138
+ function commitBgMode(on) {
2139
+ setAssign(function (prev) {
2140
+ var next = JSON.parse(JSON.stringify(prev));
2141
+ next.roles[name].background_mode = on ? "continuable" : "one-shot";
2142
+ return next;
2143
+ });
2144
+ }
2145
+ function bgOption(key, label, checked, on) {
2146
+ return h("label", { key: key, className: "omd-toolItem" },
2147
+ h("input", {
2148
+ type: "radio",
2149
+ className: "omd-check",
2150
+ name: "pd-bgm-" + name, // 每角色独立单选组;name 含 toolName 不串台
2151
+ checked: checked,
2152
+ disabled: busy,
2153
+ onChange: function () { commitBgMode(on); },
2154
+ }),
2155
+ h("span", { className: "omd-toolName" }, label));
2156
+ }
2157
+ return h("div", { className: "omd-mainName" },
2158
+ h("div", { className: "omd-glabel" }, "会话模式"),
2159
+ h("div", { className: "omd-nameRow" },
2160
+ bgOption("one-shot", "一次性(one-shot)", !continuable, false),
2161
+ bgOption("continuable", "可续(continuable)", continuable, true)),
2162
+ continuable ? h("div", { className: "omd-hint" }, ROLE_BG_CONTINUABLE_HINT) : null,
2163
+ continuable && data.persistenceAvailable === false
2164
+ ? statusRow("omd-statusWarn", ic(P.IconWarningOutline16, 16), ROLE_BG_PERSISTENCE_WARN)
2165
+ : null);
2166
+ }
2167
+
910
2168
  function roleCard(name) {
911
2169
  var isMain = name === "__main__";
912
2170
  var display = isMain ? "主 agent" : name;
913
2171
  var isCustom = !isMain && DEFAULT_ROLES.indexOf(name) === -1;
914
- var open = name in openMap ? openMap[name] : (isMain || name === "search_external");
2172
+ var open = name in openMap ? openMap[name] : defaultOpenOf(name);
915
2173
 
916
2174
  var d = data;
917
2175
  var a = assign;
@@ -928,6 +2186,19 @@ window.__ModuleLoader__.load({
928
2186
  h("span", { className: "omd-roleBadge" }, isMain ? "固定" : (isCustom ? "自定义" : "内置")),
929
2187
  h("span", { className: "omd-roleCount" },
930
2188
  (isMain ? ((data.restrictBase || []).length + a.main_agent_extra.length - (a.main_agent_remove || []).length) : a.roles[name].tools.length) + " 个工具")),
2189
+ // 自定义角色改名按钮:toolName 是身份键(委派调用名 / skills 引用),
2190
+ // 与内置角色的「显示名」不是一回事——那个只改 persona 主语。
2191
+ isCustom
2192
+ ? h("button", {
2193
+ type: "button",
2194
+ className: "omd-roleEdit",
2195
+ title: '重命名角色 "' + name + '"(toolName = 委派调用名)',
2196
+ onClick: function (e) {
2197
+ e.stopPropagation();
2198
+ renameRole(name);
2199
+ },
2200
+ }, ic(P.IconEditOutline16, 16))
2201
+ : null,
931
2202
  // 删除按钮:自定义角色走 removeRole(仅删 roles key);内置角色走
932
2203
  // removeBuiltinRole(删 key + roles_remove 记名 → 卡片消失、可恢复)。
933
2204
  // 主 agent(__main__)卡不可删。内置卡只渲染存在于 assign.roles 的,删后即隐。
@@ -950,28 +2221,44 @@ window.__ModuleLoader__.load({
950
2221
  body.push(h("div", { className: "omd-glabel" }, "基础工具(取消勾选即从主 agent 移除——写入 main_agent_remove)"));
951
2222
  body.push(h("div", { className: "omd-toolGrid" },
952
2223
  (data.restrictBase || []).map(function (t) {
953
- return toolItem(t, "base", (a.main_agent_remove || []).indexOf(t) === -1, false,
2224
+ return toolItem(t, "base", (a.main_agent_remove || []).indexOf(t) === -1, busy,
954
2225
  function (e) { setMainRemove(t, e.target.checked); }, t);
955
2226
  })));
956
- body.push(h("div", { className: "omd-glabel" }, "Host 工具(可加入主 agent)"));
2227
+ // host 检测库存按角色卡同口径拆两组:mcp__ 前缀 → 「MCP 工具」,
2228
+ // 其余(mnemon_* 等插件直注册工具)→ 「插件工具」。勾选逻辑不变,
2229
+ // 都写入 main_agent_extra。
2230
+ var mainInv = data.inventory || [];
2231
+ var mainMcpTools = mainInv.filter(function (t) { return hostTool(t); });
2232
+ var mainPluginTools = mainInv.filter(function (t) { return !hostTool(t); });
2233
+ body.push(h("div", { className: "omd-glabel" }, "MCP 工具(可加入主 agent)"));
2234
+ body.push(h("div", { className: "omd-toolGrid" },
2235
+ mainMcpTools.length
2236
+ ? mainMcpTools.map(function (t) {
2237
+ return toolItem(t, "mcp", a.main_agent_extra.indexOf(t) !== -1, busy,
2238
+ function (e) { setMainExtra(t, e.target.checked); }, t);
2239
+ })
2240
+ : h("div", { className: "omd-empty" }, "未检测到 MCP 工具")));
2241
+ body.push(h("div", { className: "omd-glabel" }, "插件工具(可加入主 agent)"));
957
2242
  body.push(h("div", { className: "omd-toolGrid" },
958
- (data.inventory || []).length
959
- ? (data.inventory || []).map(function (t) {
960
- return toolItem(t, "host", a.main_agent_extra.indexOf(t) !== -1, false,
2243
+ mainPluginTools.length
2244
+ ? mainPluginTools.map(function (t) {
2245
+ return toolItem(t, "plugin", a.main_agent_extra.indexOf(t) !== -1, busy,
961
2246
  function (e) { setMainExtra(t, e.target.checked); }, t);
962
2247
  })
963
- : h("div", { className: "omd-empty" }, "未检测到 host 工具")));
964
- body.push(h("div", { className: "omd-glabel" }, "技能分配(read 按需:勾选 = persona 写入技能行,主 agent 用 read 加载正文)"));
965
- body.push((data.skills || []).length
2248
+ : h("div", { className: "omd-empty" }, "未检测到插件工具")));
2249
+ body.push(h("div", { className: "omd-glabel" }, "技能分配(read 按需:勾选 = persona 写入技能行,主 agent 用 read 加载正文)", skillScopeNote()));
2250
+ body.push(gridSkills.length
966
2251
  ? h("div", null,
967
- h("div", { className: "omd-toolGrid" }, (data.skills || []).map(function (s) {
968
- return toolItem(s, "read按需", a.main_agent_skills.indexOf(s) !== -1, false,
969
- function (e) { setMainSkill(s, e.target.checked); }, s + "s");
2252
+ h("div", { className: "omd-toolGrid" }, gridSkills.map(function (s) {
2253
+ return toolItem(s, "read按需", a.main_agent_skills.indexOf(s) !== -1, busy,
2254
+ function (e) { setMainSkill(s, e.target.checked); }, s + "s",
2255
+ gridProj && gridProj.has(s) ? "项目" : null);
970
2256
  })),
971
2257
  h("div", { className: "omd-glabel" }, "内联全文(硬生效,每轮固定 persona 开销,适合 caveman 等风格技能)"),
972
- h("div", { className: "omd-toolGrid" }, (data.skills || []).map(function (s) {
973
- return toolItem(s, "内联全文", a.main_agent_skills_inline.indexOf(s) !== -1, false,
974
- function (e) { setMainSkillInline(s, e.target.checked); }, s + "i");
2258
+ h("div", { className: "omd-toolGrid" }, gridSkills.map(function (s) {
2259
+ return toolItem(s, "内联全文", a.main_agent_skills_inline.indexOf(s) !== -1, busy,
2260
+ function (e) { setMainSkillInline(s, e.target.checked); }, s + "i",
2261
+ gridProj && gridProj.has(s) ? "项目" : null);
975
2262
  })))
976
2263
  : h("div", { className: "omd-empty" }, "未检测到技能目录"));
977
2264
  body.push(mainPersonaExtraBlock());
@@ -981,27 +2268,46 @@ window.__ModuleLoader__.load({
981
2268
  // 通用同一 roleModelBlock——下拉模式成对写 provider + model(不存在
982
2269
  // 「只配 provider」);目录不可用降级双 input 时才可能出现该状态(后端
983
2270
  // warn 并忽略)。留空 = 跟随主 agent 当前模型。
984
- if (!isCustom) body.push(builtinNameBlock(name));
2271
+ // 「会话模式」单选紧随模型行(内置 / 自定义通用):缺省一次性,
2272
+ // 选可续时行内给成本提示 + persistence 缺失警告(如有)。
2273
+ body.push(roleTitleBlock(name, isCustom));
985
2274
  body.push(roleModelBlock(name));
986
- var baseTools = roleCandidateTools(name).filter(function (t) { return !hostTool(t); });
987
- var hostTools = roleCandidateTools(name).filter(function (t) { return hostTool(t); });
2275
+ body.push(roleBgModeBlock(name));
2276
+ // 三组网格(同 roleCandidateTools 注释的分组口径):「基础工具」=
2277
+ // 非 mcp__ 且不在检测库存(blocks ∪ customCoreToolsOf 减 inventory);
2278
+ // 「MCP 工具」= mcp__ 前缀(含 SRC 自带行,如 tavily);「插件工具」
2279
+ // = 检测库存中非 mcp__ 的直注册工具(mnemon_* 等)。三组勾选逻辑
2280
+ // 一致(setRoleTools),分组只影响展示。
2281
+ var candidates = roleCandidateTools(name);
2282
+ var inv = data.inventory || [];
2283
+ var mcpTools = candidates.filter(function (t) { return hostTool(t); });
2284
+ var pluginTools = candidates.filter(function (t) { return !hostTool(t) && inv.indexOf(t) !== -1; });
2285
+ var baseTools = candidates.filter(function (t) { return !hostTool(t) && inv.indexOf(t) === -1; });
988
2286
  var checked = a.roles[name].tools;
989
2287
  body.push(h("div", { className: "omd-glabel" }, "基础工具"));
990
2288
  body.push(h("div", { className: "omd-toolGrid" },
991
2289
  baseTools.length
992
2290
  ? baseTools.map(function (t) {
993
- return toolItem(t, "base", checked.indexOf(t) !== -1, false,
2291
+ return toolItem(t, "base", checked.indexOf(t) !== -1, busy,
994
2292
  function (e) { setRoleTools(name, t, e.target.checked); }, t);
995
2293
  })
996
2294
  : h("div", { className: "omd-empty" }, "无")));
997
- body.push(h("div", { className: "omd-glabel" }, "Host 工具"));
2295
+ body.push(h("div", { className: "omd-glabel" }, "MCP 工具"));
998
2296
  body.push(h("div", { className: "omd-toolGrid" },
999
- hostTools.length
1000
- ? hostTools.map(function (t) {
1001
- return toolItem(t, "host", checked.indexOf(t) !== -1, false,
2297
+ mcpTools.length
2298
+ ? mcpTools.map(function (t) {
2299
+ return toolItem(t, "mcp", checked.indexOf(t) !== -1, busy,
1002
2300
  function (e) { setRoleTools(name, t, e.target.checked); }, t);
1003
2301
  })
1004
- : h("div", { className: "omd-empty" }, "未检测到 host 工具")));
2302
+ : h("div", { className: "omd-empty" }, "未检测到 MCP 工具")));
2303
+ body.push(h("div", { className: "omd-glabel" }, "插件工具"));
2304
+ body.push(h("div", { className: "omd-toolGrid" },
2305
+ pluginTools.length
2306
+ ? pluginTools.map(function (t) {
2307
+ return toolItem(t, "plugin", checked.indexOf(t) !== -1, busy,
2308
+ function (e) { setRoleTools(name, t, e.target.checked); }, t);
2309
+ })
2310
+ : h("div", { className: "omd-empty" }, "未检测到插件工具")));
1005
2311
  body.push(personaBlock(name));
1006
2312
  }
1007
2313
 
@@ -1011,17 +2317,21 @@ window.__ModuleLoader__.load({
1011
2317
  }
1012
2318
 
1013
2319
  function skillsBlock() {
1014
- var rows = (data.skills || []).map(function (s) {
2320
+ // 数据源与主 agent 技能网格一致(工作区视角 = 合并列表,见 gridSkills);
2321
+ // 项目级技能在名字旁加「项目」角标。
2322
+ var rows = gridSkills.map(function (s) {
1015
2323
  var roleNames = Object.keys(assign.roles);
1016
2324
  var picked = assign.skills[s] || [];
1017
2325
  return h("div", { className: "omd-skillRow", key: s },
1018
2326
  h("span", { className: "omd-skillName", title: s }, s),
2327
+ gridProj && gridProj.has(s) ? h("span", { className: "omd-skillProj" }, "项目") : null,
1019
2328
  h("div", { className: "omd-skillRoles" }, roleNames.map(function (r) {
1020
2329
  return h("label", { className: "omd-skillRole", key: r },
1021
2330
  h("input", {
1022
2331
  type: "checkbox",
1023
2332
  className: "omd-check",
1024
2333
  checked: picked.indexOf(r) !== -1,
2334
+ disabled: busy,
1025
2335
  onChange: function (e) { setSkill(s, r, e.target.checked); },
1026
2336
  }),
1027
2337
  h("span", null, r));
@@ -1032,6 +2342,11 @@ window.__ModuleLoader__.load({
1032
2342
 
1033
2343
  var customRoles = Object.keys(assign.roles).filter(function (n) { return DEFAULT_ROLES.indexOf(n) === -1; });
1034
2344
 
2345
+ // persistence 警告横幅:host 层会话持久化后端缺失(state.persistenceAvailable
2346
+ // === false;字段缺失的老版服务端视为可用,不误报)且当前视角的任意角色
2347
+ // 配了可续(continuable)时,面板顶部警示——只提示不拦截,预览 / 应用照常。
2348
+ var persistenceWarn = data.persistenceAvailable === false && hasContinuableRole(assign.roles);
2349
+
1035
2350
  var applyBtn = h(P.Button, {
1036
2351
  variant: "primary",
1037
2352
  size: "md",
@@ -1062,32 +2377,30 @@ window.__ModuleLoader__.load({
1062
2377
  : ic(P.IconRefreshOutline16, 16),
1063
2378
  }, busyOp === "rescan" ? "检测中…" : "重新检测");
1064
2379
 
1065
- var pluginChips = data ? pluginChipsOf(data) : { primary: [], secondary: [] };
2380
+ return h("div", { className: "pd-page", ref: rootRef },
2381
+ // 运行时识别降级:持续状态(不是事件),内联一行小字贴标签行上方,
2382
+ // 平时(runtimeOk 正常)零占位
2383
+ runtimeNoteOf(data),
1066
2384
 
1067
- return h("div", { className: "omd-section", ref: rootRef },
1068
- h("p", { className: "omd-intro" }, INTRO),
2385
+ // persistence 警告横幅:仅当前视角确有可续角色且 host 层持久化后端
2386
+ // 缺失时出现(判定见上方 persistenceWarn),其余场合零占位
2387
+ persistenceWarn
2388
+ ? statusRow("omd-statusWarn", ic(P.IconWarningOutline16, 16), ROLE_BG_PERSISTENCE_WARN)
2389
+ : null,
1069
2390
 
1070
- // 状态与操作
1071
- h("div", { className: "omd-group" },
1072
- h("div", { className: "omd-groupHeading" },
1073
- h("span", { className: "omd-groupIcon" }, ic(P.IconSettingsOutline16, 14)),
1074
- h("span", null, "状态与操作")),
1075
- statusBarOf(),
1076
- runtimeNoteOf(data),
1077
- h("div", { className: "omd-chips" },
1078
- h(P.Pill, null, "profile " + (data.effectiveProfile || "web")),
1079
- (data.mcpReports || []).map(function (m) {
1080
- return h(P.Pill, { key: m.serverName },
1081
- "MCP " + m.serverName + " · " + m.count + " 工具",
1082
- m.error ? h("span", { className: "omd-chipErrText" }, "(" + m.error + ")") : null);
1083
- }),
1084
- pluginChips.primary.map(function (p) {
1085
- return h(P.Pill, { key: p.label }, chipTextOf(p));
1086
- }),
1087
- pluginChips.secondary.length
1088
- ? h("span", { className: "omd-chipSecondary" },
1089
- "其他运行时插件:" + pluginChips.secondary.map(function (p) { return p.label; }).join(" · "))
1090
- : null)),
2391
+ // 配置对象(全局默认 / 各工作区,标签页)
2392
+ workspaceBarOf(),
2393
+
2394
+ // 「添加工作区」弹窗:fixed 全屏遮罩不占布局流,挂在根数组末尾即可;
2395
+ // 未打开时返回 null,零开销
2396
+ wsModalOf(),
2397
+
2398
+ // 「新建自定义 agent 角色」弹窗:同为 fixed 全屏遮罩不占布局流,
2399
+ // 未打开时返回 null,零开销
2400
+ roleAddModalOf(),
2401
+
2402
+ // 「重命名自定义角色」弹窗:同上(原原生 prompt 改模态框)
2403
+ roleRenameModalOf(),
1091
2404
 
1092
2405
  // 角色与工具
1093
2406
  h("div", { className: "omd-group" },
@@ -1115,7 +2428,7 @@ window.__ModuleLoader__.load({
1115
2428
  h("div", { className: "omd-hint" },
1116
2429
  "恢复后卡片回到上方内置列表;工具面取内置默认(不沿用删前自定义配置)。"))
1117
2430
  : null,
1118
- h("button", { type: "button", className: "omd-addRole", onClick: addRole },
2431
+ h("button", { type: "button", className: "omd-addRole", onClick: openRoleAddModal },
1119
2432
  h("span", { className: "omd-addRoleIcon" }, ic(P.IconPlusOutline16, 16)),
1120
2433
  h("span", null, "新建自定义 agent 角色"))),
1121
2434
 
@@ -1124,7 +2437,12 @@ window.__ModuleLoader__.load({
1124
2437
  h("div", { className: "omd-groupHeading" },
1125
2438
  h("span", { className: "omd-groupIcon" }, ic(P.IconSkillOutline16, 14)),
1126
2439
  h("span", null, "技能分配(软约束:写入对应 agent 的 persona 引导)"),
1127
- h("span", { className: "omd-count" }, (data.skills || []).length)),
2440
+ skillScopeNote(),
2441
+ h("span", { className: "omd-count" }, gridSkills.length)),
2442
+ // 工作区技能拉取失败的降级提示(一句话):此刻网格数据源已回落用户级。
2443
+ activeWs !== null && wsSkillsErr
2444
+ ? statusRow("omd-statusWarn", ic(P.IconWarningOutline16, 16), wsSkillsErr)
2445
+ : null,
1128
2446
  skillsBlock()),
1129
2447
 
1130
2448
  // 预览生成(预览成功后才出现)
@@ -1133,7 +2451,9 @@ window.__ModuleLoader__.load({
1133
2451
  h("span", { className: "omd-groupIcon" }, ic(P.IconDataOutline16, 14)),
1134
2452
  h("span", null, "预览生成")),
1135
2453
  h("div", { className: "omd-previewSummary" },
1136
- "✅ 生成成功(未写盘)" + (preview.roleResults || []).map(function (r) {
2454
+ "✅ 生成成功(未写盘)" +
2455
+ (preview.presetId ? " · 预设 " + preview.presetId : "") +
2456
+ (preview.roleResults || []).map(function (r) {
1137
2457
  return " · " + r.role + ": " + r.intent.length + " → " + r.kept.length;
1138
2458
  }).join("")),
1139
2459
  h("pre", { className: "omd-pre" }, preview.text)) : null,
@@ -1144,23 +2464,717 @@ window.__ModuleLoader__.load({
1144
2464
  h("span", { className: "omd-applyDock" }, applyBtn)));
1145
2465
  }
1146
2466
 
1147
- // ── cordis client 插件:注册到设置页 settings.section 槽位 ──────────────
2467
+ // ── 挂载:底部入口 + 会话中栏全页 overlay ──────────────────────────────
1148
2468
  //
1149
- // register 是 "slots" service(SlotRegistry)的方法,不是 slots 包的静态导出;
1150
- // 必须经 fiber inject 依赖 slots,并在 apply(ctx) 里通过 ctx.slots.inject()
1151
- // 等待 settings.section 被声明后再注册(与 ui-settings-general 同款模式)。
2469
+ // 入口走宿主 slots 键控槽(sidebar.footer.action,见下方 mountFooterEntry
2470
+ // 与 exports.inject 的 slots 声明);全页 overlay 为纯 DOM + React root:
2471
+ // 1) 底部入口:左下角动作条(与插件广场 cordis 徽章同槽位,设置行上方);
2472
+ // 宽栏 42px 圆角行 / 窄栏 36px 圆钮,点击开关 overlay;
2473
+ // 由观察器自动放回。
2474
+ // 2) 全页 overlay:容器 div(data-dsh-paoding-view)append 到会话中栏,
2475
+ // createRoot 渲染整页配置页;打开态在 <html> 上设
2476
+ // data-dsh-paoding-active,CSS 据此隐藏会话内容、显示本页。与
2477
+ // mnemon / taskboard / ssh 互斥(dsh-panel-activate 事件 + active
2478
+ // 属性双通道),点侧栏会话行等上下文自动关闭。
2479
+
2480
+ // react-dom/client:宿主 ModuleLoader seed 提供(mnemon 线上 bundle 同款
2481
+ // 用法)。异常宿主下取不到时 overlay 不挂,侧栏入口仍可用(保底不炸)。
2482
+ var ReactDOMClient = null;
2483
+ try { ReactDOMClient = require("react-dom/client"); } catch (err) { ReactDOMClient = null; }
2484
+
2485
+ // 框架无关的小状态器(照 mnemon 的 MnemonWorkspaceController):侧栏入口
2486
+ // 与全页 overlay 共享同一份 { open } 快照,subscribe 驱动两侧同步。
2487
+ // 「带目标工作区打开」的待办路径:宿主工作区菜单的「庖丁配置」入口点击时
2488
+ // 写入(controller.open(path)),ConfigPanel 在 state + 工作区列表都就绪后
2489
+ // 消费并切到该 tab。放模块级而非 prop 钻透——controller 与 ConfigPanel 分属
2490
+ // DOM 注入层与 React 层,中间隔着懒挂载的 createRoot,props 传不进去。
2491
+ var pendingOpenWs = null;
2492
+
2493
+ function setPendingOpenWs(wsPath) {
2494
+ pendingOpenWs = typeof wsPath === "string" && wsPath.trim() !== ""
2495
+ ? wsPath.trim().replace(/[\/\\]+$/, "")
2496
+ : null;
2497
+ }
2498
+
2499
+ function createWorkspaceController() {
2500
+ var snapshot = { open: false };
2501
+ var listeners = [];
2502
+ function notify() {
2503
+ listeners.slice().forEach(function (listener) { listener(); });
2504
+ }
2505
+ function setOpen(open, reassert) {
2506
+ if (snapshot.open === open && !reassert) return;
2507
+ snapshot = { open: open };
2508
+ notify();
2509
+ }
2510
+ return {
2511
+ getSnapshot: function () { return snapshot; },
2512
+ subscribe: function (listener) {
2513
+ listeners.push(listener);
2514
+ return function () {
2515
+ var i = listeners.indexOf(listener);
2516
+ if (i !== -1) listeners.splice(i, 1);
2517
+ };
2518
+ },
2519
+ // open 带 reassert:重复打开(如属性观察兜底触发)时也把 active 属性
2520
+ // 与互斥广播重新刷一遍,而不是因「已是 true」短路。可选 wsPath = 打开
2521
+ // 后要切到的工作区路径(宿主菜单「庖丁配置」入口用,见 pendingOpenWs)。
2522
+ open: function (wsPath) { setPendingOpenWs(wsPath); setOpen(true, true); },
2523
+ close: function () { setOpen(false); },
2524
+ toggle: function () { setOpen(!snapshot.open); },
2525
+ };
2526
+ }
2527
+
2528
+ // ── 侧栏底部入口(sidebar.footer.action 键控槽)────────────────────────
2529
+ // 入口挂在左下角底部动作条:注册 sidebar.footer.action
2530
+ // 列表槽(与插件广场 cordis 徽章同一槽位,渲染在设置行上方的动作条里)。
2531
+ // 观感复刻 cordis 徽章 / 设置触发钮:宽栏 = 42px 圆角行(图标 + 文案),
2532
+ // 窄栏(56px rail)= 36px 圆形图标钮。点击开 / 关全页 overlay(mountPanel
2533
+ // 一套不动:互斥广播、工作区菜单深链、Esc 层级照旧)。
2534
+ function makeFooterEntry(controller) {
2535
+ return function FooterEntry(props) {
2536
+ var wide = !!(props && props.wide);
2537
+ var _st = useState(controller.getSnapshot().open), open = _st[0], setOpenState = _st[1];
2538
+ useEffect(function () {
2539
+ return controller.subscribe(function () { setOpenState(controller.getSnapshot().open); });
2540
+ }, []);
2541
+ return h("button", {
2542
+ type: "button",
2543
+ className: "pd-footerBtn" + (wide ? "" : " pd-footerRail"),
2544
+ "data-active": open ? "true" : undefined,
2545
+ "aria-label": "庖丁配置",
2546
+ "aria-haspopup": "dialog",
2547
+ "aria-expanded": open ? "true" : "false",
2548
+ title: wide ? undefined : "庖丁配置",
2549
+ onClick: function () { controller.toggle(); },
2550
+ },
2551
+ h(P.IconSkillOutline16, { size: wide ? 16 : 18 }),
2552
+ wide ? h("span", { className: "pd-footerLabel" }, "庖丁配置") : null);
2553
+ };
2554
+ }
2555
+
2556
+ function mountFooterEntry(ctx, controller) {
2557
+ if (typeof document === "undefined") return function () {};
2558
+ if (!ctx || !ctx.slots || typeof ctx.slots.inject !== "function" || typeof ctx.slots.register !== "function") {
2559
+ return function () {}; // 槽口不可用(老宿主):静默不挂,面板仅失去底部入口
2560
+ }
2561
+ var Entry = makeFooterEntry(controller);
2562
+ var dispose = ctx.slots.inject("sidebar.footer.action", function () {
2563
+ return ctx.slots.register({ name: "sidebar.footer.action", id: "paoding-config", order: 10, label: "庖丁配置" }, Entry);
2564
+ });
2565
+ return typeof dispose === "function" ? dispose : function () {};
2566
+ }
2567
+
2568
+
2569
+ // ── 委派事件流行(tool.call.toolview 键控行替换)────────────────────────
2570
+ // 事件流里每个工具调用行由 ToolCallTree 按 toolName 查 tool.call.toolview
2571
+ // 键控插槽,键命中则整行替换成注册组件(bash / read / grep / cordis 等都
2572
+ // 走这条路)。这里为庖丁的委派工具(内置四角色 + 自定义角色名)注册行:
2573
+ // 标题 = 显示名(roles.<toolName>.name,未配走中文缺省映射 / toolName),
2574
+ // 副题 = 调用参数 description(模型写的任务一句话),尾部状态点,点击展开
2575
+ // 子 agent 返回文本——通用行的结果查看能力不能丢。
2576
+ // 边界:只管工具调用行;子 agent 会话树 / 面包屑标题来自服务端会话摘要,
2577
+ // 不在客户端控制面。标题取全局配置的显示名(行渲染时不知道会话属于哪个
2578
+ // 工作区,工作区级显示名够不着)。
2579
+ var DELEGATE_TITLE_FALLBACK = {
2580
+ search_external: "外部检索",
2581
+ design: "设计",
2582
+ implement: "实现",
2583
+ search_internal_deep: "深搜",
2584
+ };
2585
+ // 静态委派工具:不在 roles 键里(无条目、不可删不可改名),恒注册。
2586
+ var DELEGATE_STATIC_TOOLS = ["search_internal_deep"];
2587
+
2588
+ function buildDelegateMeta(d) {
2589
+ var cfg = globalConfigOf(d) || (d && d.suggested) || {};
2590
+ var roles = (cfg && typeof cfg.roles === "object" && cfg.roles) || {};
2591
+ var removed = Array.isArray(cfg.roles_remove) ? cfg.roles_remove : [];
2592
+ var titles = {};
2593
+ var tools = DELEGATE_STATIC_TOOLS.slice();
2594
+ DELEGATE_STATIC_TOOLS.forEach(function (n) { titles[n] = DELEGATE_TITLE_FALLBACK[n] || n; });
2595
+ Object.keys(roles).forEach(function (n) {
2596
+ if (removed.indexOf(n) !== -1) return; // 已删内置角色:委派工具不存在,键命中不了,不注册
2597
+ if (tools.indexOf(n) === -1) tools.push(n);
2598
+ var nm = roles[n] && typeof roles[n].name === "string" ? roles[n].name.trim() : "";
2599
+ titles[n] = nm !== "" ? nm : (DELEGATE_TITLE_FALLBACK[n] || n);
2600
+ });
2601
+ return { titles: titles, tools: tools };
2602
+ }
2603
+
2604
+ var delegateMeta = { titles: {}, tools: [] };
2605
+ var delegateRowsDispose = null;
2606
+ var delegateRowsCtx = null;
2607
+
2608
+ // 调用块是双形态(与宿主 toolRowModel 同源判据):进行中块无 kind、
2609
+ // 参数在 block.argsRaw;结算块有 kind、参数挪进 block.call.argsRaw,
2610
+ // 结果在 block.content,error.code === "interrupted" 表示被停。
2611
+ function delegateArgsRawOf(block) {
2612
+ if (!block || typeof block !== "object") return "";
2613
+ var raw = "kind" in block
2614
+ ? (block.call && typeof block.call.argsRaw === "string" ? block.call.argsRaw : "")
2615
+ : (typeof block.argsRaw === "string" ? block.argsRaw : "");
2616
+ return raw || "";
2617
+ }
2618
+
2619
+ function delegateStatusOf(block) {
2620
+ if (!block || typeof block !== "object" || !("kind" in block)) return "running";
2621
+ if (block.error && block.error.code === "interrupted") return "stopped";
2622
+ if (block.isError || block.error) return "error";
2623
+ return "done";
2624
+ }
2625
+
2626
+ // 展开用的结果文本:文本块原文 + 其余块 pretty JSON;空内容回落错误行。
2627
+ function delegateResultText(block) {
2628
+ var parts = [];
2629
+ var content = block && Array.isArray(block.content) ? block.content : [];
2630
+ for (var i = 0; i < content.length; i++) {
2631
+ var c = content[i];
2632
+ if (c && c.type === "text" && typeof c.text === "string") parts.push(c.text);
2633
+ else if (c) parts.push(JSON.stringify(c, null, 2));
2634
+ }
2635
+ if (parts.length === 0 && block && block.error) {
2636
+ parts.push((block.error.name || "error") + ": " + (block.error.code || ""));
2637
+ }
2638
+ return parts.join("\n");
2639
+ }
2640
+
2641
+ function DelegateRow(props) {
2642
+ var block = props && props.block ? props.block : {};
2643
+ var toolName = (props && props.toolName) || "";
2644
+ var _o = useState(false), open = _o[0], setOpen = _o[1];
2645
+ var args = {};
2646
+ var argsRaw = delegateArgsRawOf(block);
2647
+ if (argsRaw !== "") {
2648
+ try {
2649
+ var parsed = JSON.parse(argsRaw);
2650
+ if (parsed && typeof parsed === "object") args = parsed;
2651
+ } catch (err) { /* 原始参数非 JSON:忽略,副题留空 */ }
2652
+ }
2653
+ var desc = typeof args.description === "string" ? args.description.trim() : "";
2654
+ var prompt = typeof args.prompt === "string" ? args.prompt : "";
2655
+ var status = delegateStatusOf(block);
2656
+ var stateCls = status === "running" ? "pd-delegateRunning" : (status === "error" ? "pd-delegateFailed" : (status === "stopped" ? "pd-delegateStopped" : "pd-delegateDone"));
2657
+ var stateGlyph = status === "running" ? "◌" : (status === "error" ? "✕" : (status === "stopped" ? "⊘" : "✓"));
2658
+ var title = delegateMeta.titles[toolName] || DELEGATE_TITLE_FALLBACK[toolName] || toolName;
2659
+ return h("div", { className: "pd-delegateRow" + (status === "error" ? " pd-delegateErr" : "") },
2660
+ h("button", {
2661
+ type: "button",
2662
+ className: "pd-delegateHead",
2663
+ "aria-expanded": open ? "true" : "false",
2664
+ title: prompt ? prompt.slice(0, 400) : "",
2665
+ onClick: function () { setOpen(!open); },
2666
+ },
2667
+ h("span", { className: "pd-delegateState " + stateCls, "aria-hidden": "true" }, stateGlyph),
2668
+ h("span", { className: "pd-delegateTitle" }, title),
2669
+ desc ? h("span", { className: "pd-delegateDesc" }, desc) : null,
2670
+ h("span", { className: open ? "pd-delegateChev pd-delegateChevOpen" : "pd-delegateChev", "aria-hidden": "true" }, "▸")),
2671
+ open ? h("pre", { className: "pd-delegateBody" }, delegateResultText(block)) : null);
2672
+ }
2673
+
2674
+ // 重注册:先撤旧键(inject 返回处置器,dsh-mnemon 同款用法),再按当前
2675
+ // 名单重建——自定义角色增删 / 显示名改动后由 refreshDelegateRows 触发。
2676
+ function syncDelegateRows() {
2677
+ if (delegateRowsDispose) {
2678
+ try { delegateRowsDispose(); } catch (err) { /* 撤旧失败不阻断重建 */ }
2679
+ delegateRowsDispose = null;
2680
+ }
2681
+ var ctx = delegateRowsCtx;
2682
+ if (!ctx || !ctx.slots || typeof ctx.slots.inject !== "function" || typeof ctx.slots.register !== "function") return;
2683
+ if (delegateMeta.tools.length === 0) return;
2684
+ delegateRowsDispose = ctx.slots.inject("tool.call.toolview", function () {
2685
+ var disposers = [];
2686
+ try {
2687
+ delegateMeta.tools.forEach(function (n) {
2688
+ disposers.push(ctx.slots.register({ name: "tool.call.toolview", key: n }, DelegateRow));
2689
+ });
2690
+ } catch (err) {
2691
+ disposers.forEach(function (d) { try { d(); } catch (e2) { /* 连锁失败静默 */ } });
2692
+ throw err;
2693
+ }
2694
+ return function () { disposers.forEach(function (d) { try { d(); } catch (e2) { /* 静默 */ } }); };
2695
+ });
2696
+ }
2697
+
2698
+ function refreshDelegateRows() {
2699
+ paodingApi("state").then(function (d) {
2700
+ var next = buildDelegateMeta(d);
2701
+ // 比对串必须并进 titles 的值:显示名单独变更时键集合不变,只比键会
2702
+ // 漏判,事件流委派行标题刷不出来。
2703
+ var oldKeys = Object.keys(delegateMeta.titles).sort().map(function (k) { return k + ":" + delegateMeta.titles[k]; }).join("|")
2704
+ + "//" + delegateMeta.tools.slice().sort().join("|");
2705
+ var newKeys = Object.keys(next.titles).sort().map(function (k) { return k + ":" + next.titles[k]; }).join("|")
2706
+ + "//" + next.tools.slice().sort().join("|");
2707
+ if (newKeys !== oldKeys) {
2708
+ delegateMeta = next;
2709
+ syncDelegateRows();
2710
+ }
2711
+ }).catch(function () { /* 静默:行回落通用卡片 */ });
2712
+ }
2713
+
2714
+ function mountDelegateRows(ctx) {
2715
+ if (typeof document === "undefined") return function () {};
2716
+ delegateRowsCtx = ctx;
2717
+ refreshDelegateRows();
2718
+ var offReset = null;
2719
+ if (ctx && typeof ctx.on === "function") {
2720
+ try {
2721
+ var r = ctx.on("connection/reset", function () { refreshDelegateRows(); });
2722
+ if (typeof r === "function") offReset = r;
2723
+ } catch (err) { /* 事件口不可用:仅失去断线重连后的刷新 */ }
2724
+ }
2725
+ return function () {
2726
+ if (delegateRowsDispose) { try { delegateRowsDispose(); } catch (err) { /* 静默 */ } }
2727
+ delegateRowsDispose = null;
2728
+ if (offReset) { try { offReset(); } catch (err) { /* 静默 */ } }
2729
+ delegateRowsCtx = null;
2730
+ };
2731
+ }
2732
+
2733
+ // ── 宿主工作区「...」菜单注入桥 ──────────────────────────────────────────
2734
+ // 宿主侧栏工作区行的「...」菜单只有「重命名 / 删除工作区」两项,onSelect
2735
+ // 硬编码只认这两个 id,没有插件扩展点。这里从 DOM 层补一个「庖丁配置」项:
2736
+ // 1. 捕获级监听 document 点击,按触发钮 aria-label(工作区“<名>”的操作)
2737
+ // 记下最近一次操作的宿主工作区名;
2738
+ // 2. MutationObserver 盯菜单 portal(每次打开都是新建 DOM):认出「重命名
2739
+ // + 删除工作区」两行 = 工作区菜单,克隆重命名行改文案为「庖丁配置」——
2740
+ // 克隆而非自绘,类名跟宿主构建走,观感天然一致,hash 类名升级也不怕;
2741
+ // 3. 点击:先合成 Escape 关掉宿主菜单,再按记下的名字到 /api/paoding/
2742
+ // workspaces 反查路径(title 精确 → basename 兜底),controller.open
2743
+ // (path) 打开本页,ConfigPanel 就绪后自动切到该工作区 tab。
2744
+ // 反查失败(列表拉不到 / 名字对不上)仍打开页面,落在全局默认并给 warn。
2745
+ // 宿主 i18n 的 aria-label 模板是「工作区“{name}”的操作」(zh)/「Workspace
2746
+ // “{name}” actions」类变体(en),这里宽松匹配:引号样式 + 尾缀都容错。
2747
+ function wsTitleFromAriaLabel(label) {
2748
+ if (typeof label !== "string" || label === "") return null;
2749
+ var m = label.match(/^(?:工作区|Workspace)\s*[“"']([^”"']{1,120})[”"']/);
2750
+ return m ? m[1] : null;
2751
+ }
2752
+
2753
+ function mountWorkspaceMenuBridge(controller) {
2754
+ if (typeof document === "undefined" || typeof MutationObserver === "undefined") {
2755
+ return function () {};
2756
+ }
2757
+ var lastWsTitle = null;
2758
+
2759
+ function onClickCapture(e) {
2760
+ var btn = e.target && e.target.closest ? e.target.closest("button[aria-label]") : null;
2761
+ if (!btn) return;
2762
+ var title = wsTitleFromAriaLabel(btn.getAttribute("aria-label"));
2763
+ if (title !== null) lastWsTitle = title;
2764
+ }
2765
+
2766
+ // 在菜单 portal 里找「工作区菜单」:所有文本叶子按已知文案对筛,命中后
2767
+ // 爬回菜单行(列表容器的直接子级),并要求同容器存在「删除」行作证。
2768
+ function findHostWsMenu(rootNode) {
2769
+ var scopes = [rootNode, document.body];
2770
+ for (var si = 0; si < scopes.length; si++) {
2771
+ var scope = scopes[si];
2772
+ if (!scope || !scope.querySelectorAll) continue;
2773
+ var leafs = scope.querySelectorAll("button, [role='menuitem'], li, div, span");
2774
+ for (var i = 0; i < leafs.length; i++) {
2775
+ var leaf = leafs[i];
2776
+ if (leaf.childElementCount !== 0) continue;
2777
+ var txt = (leaf.textContent || "").trim();
2778
+ var isRename = txt === "重命名" || txt === "Rename";
2779
+ if (!isRename) continue;
2780
+ var row = leaf;
2781
+ while (row.parentElement && row.parentElement.childElementCount < 2) row = row.parentElement;
2782
+ var list = row.parentElement;
2783
+ if (!list) continue;
2784
+ var hasDelete = false;
2785
+ for (var c = 0; c < list.children.length; c++) {
2786
+ var t = (list.children[c].textContent || "");
2787
+ if (t.indexOf("删除工作区") !== -1 || t.indexOf("Delete workspace") !== -1) { hasDelete = true; break; }
2788
+ }
2789
+ if (!hasDelete) continue; // 缺「删除工作区」佐证:非工作区菜单(其他重命名入口),继续扫下一候选
2790
+ // 已注入过(自家行在菜单里)就不再动
2791
+ if (list.querySelector("[data-paoding-ws-menu]")) return null;
2792
+ return row;
2793
+ }
2794
+ }
2795
+ return null;
2796
+ }
2797
+
2798
+ // 克隆宿主菜单行:换文案、清 id / aria 引用(防重复),打自家标记。
2799
+ function buildPaodingRow(modelRow, onPick) {
2800
+ var mine = modelRow.cloneNode(true);
2801
+ mine.removeAttribute("id");
2802
+ var inner = mine.querySelectorAll ? mine.querySelectorAll("[id]") : [];
2803
+ for (var i = 0; i < inner.length; i++) inner[i].removeAttribute("id");
2804
+ mine.setAttribute("data-paoding-ws-menu", "1");
2805
+ mine.setAttribute("aria-label", "庖丁配置");
2806
+ mine.setAttribute("title", "打开庖丁配置并切到该工作区");
2807
+ var walker = document.createTreeWalker(mine, NodeFilter.SHOW_TEXT, null);
2808
+ var node;
2809
+ while ((node = walker.nextNode())) {
2810
+ var t = node.nodeValue;
2811
+ if (t.indexOf("重命名") !== -1 || t.toLowerCase().indexOf("rename") !== -1) {
2812
+ node.nodeValue = t.replace(/重命名|Rename/gi, "庖丁配置");
2813
+ }
2814
+ }
2815
+ mine.addEventListener("click", function (e) {
2816
+ e.preventDefault();
2817
+ e.stopPropagation();
2818
+ onPick();
2819
+ });
2820
+ return mine;
2821
+ }
2822
+
2823
+ // 合成 Escape 关宿主菜单(portal 挂 body,宿主 Menu 自行监听关闭)。
2824
+ function closeHostMenu() {
2825
+ try { document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true })); } catch (err) { /* 老内核无构造器:跳过 */ }
2826
+ }
2827
+
2828
+ function pickAndOpen() {
2829
+ var title = lastWsTitle;
2830
+ closeHostMenu();
2831
+ // 等 Escape 落地再开本页(宿主菜单关闭与 overlay 激活互不抢事件)
2832
+ setTimeout(function () {
2833
+ if (!title) { controller.open(); return; }
2834
+ paodingApi("workspaces").then(function (d) {
2835
+ var items = (d && Array.isArray(d.items)) ? d.items : [];
2836
+ var hit = null;
2837
+ for (var i = 0; i < items.length; i++) {
2838
+ var it = items[i];
2839
+ if (!it || typeof it.path !== "string" || it.path === "") continue;
2840
+ if (typeof it.title === "string" && it.title === title) { hit = it.path; break; }
2841
+ }
2842
+ if (!hit) {
2843
+ // 标题对不上:按 basename 兜底(宿主缺省标题常取目录名)
2844
+ var norm = title.replace(/[\/\\]+$/, "");
2845
+ var base = norm.split("/").pop();
2846
+ for (var j = 0; j < items.length; j++) {
2847
+ var p = items[j] && items[j].path;
2848
+ if (typeof p !== "string" || p === "") continue;
2849
+ var pb = p.replace(/[\/\\]+$/, "").split("/").pop();
2850
+ if (pb === base) { hit = p; break; }
2851
+ }
2852
+ }
2853
+ if (hit) controller.open(hit);
2854
+ else {
2855
+ controller.open();
2856
+ toast.warn("宿主工作区「" + title + "」还没在庖丁注册,已打开全局默认");
2857
+ }
2858
+ }).catch(function () {
2859
+ controller.open();
2860
+ toast.warn("工作区列表拉取失败,已打开全局默认");
2861
+ });
2862
+ }, 80);
2863
+ }
2864
+
2865
+ // 闸门:mutation 记录里新增的元素含「重命名 / Rename」才进全量扫描——
2866
+ // 聊天流 DOM 变更频繁,菜单 portal 是 body 直挂的小 subtree,先粗筛再细认。
2867
+ function recordsLookLikeMenu(records) {
2868
+ for (var i = 0; i < records.length; i++) {
2869
+ var added = records[i] && records[i].addedNodes;
2870
+ if (!added) continue;
2871
+ for (var j = 0; j < added.length; j++) {
2872
+ var n = added[j];
2873
+ if (!n || n.nodeType !== 1) continue;
2874
+ var txt = n.textContent || "";
2875
+ if (txt.indexOf("重命名") !== -1 || txt.toLowerCase().indexOf("rename") !== -1) return true;
2876
+ }
2877
+ }
2878
+ return false;
2879
+ }
2880
+
2881
+ var mo = new MutationObserver(function (records) {
2882
+ if (!recordsLookLikeMenu(records)) return;
2883
+ var model = findHostWsMenu(document.body);
2884
+ if (!model || !model.parentElement) return;
2885
+ model.parentElement.appendChild(buildPaodingRow(model, pickAndOpen));
2886
+ });
2887
+ mo.observe(document.body, { childList: true, subtree: true });
2888
+ document.addEventListener("click", onClickCapture, true);
2889
+ return function () {
2890
+ mo.disconnect();
2891
+ document.removeEventListener("click", onClickCapture, true);
2892
+ };
2893
+ }
1152
2894
 
2895
+ // ── 全页 overlay ────────────────────────────────────────────────────────
2896
+
2897
+ var CONVERSATION_COLUMN_SELECTOR = '[data-pane="conversation"], [class*="centerCol"], .dshDesktopConversationSurface';
2898
+ var ACTIVE_ATTR = "data-dsh-paoding-active";
2899
+ var MNEMON_ACTIVE_ATTR = "data-dsh-mnemon-active";
2900
+ var TASKBOARD_ACTIVE_ATTR = "data-dsh-taskboard-active";
2901
+ var SSH_ACTIVE_ATTR = "data-dsh-ssh-active";
2902
+ var ACTIVATE_EVENT = "dsh-panel-activate";
2903
+ // 点侧栏上下文(会话行 / 项目行 / 搜索结果 / 新会话 / 各插件入口)时自动
2904
+ // 关闭本页——用户已离开配置语境。
2905
+ var SIDEBAR_CONTEXT_SELECTOR = "[data-dsh-taskboard-entry], [data-dsh-ssh-entry], [data-dsh-mnemon-entry], [data-dsh-paoding-entry], [class*=\"sessionRow\"], [class*=\"projectRow\"], [class*=\"searchResultRow\"], [class*=\"searchResultWorkspace\"], [class*=\"newSession\"]";
2906
+
2907
+ // 整页骨架:页头(标题 + 副标题 + 右上关闭)+ 内容区(ConfigPanel 整页
2908
+ // 内容,.pd-page 自身即滚动容器,悬浮操作坞的 sticky 语义在其中成立)。
2909
+ // Esc 关闭挂 document keydown(冒泡阶段):「添加工作区」弹窗、「新建 /
2910
+ // 重命名角色」弹窗与「更多」菜单开着时,ConfigPanel 的捕获级监听先截停
2911
+ // Esc(只收菜单 / 建议下拉 / 弹窗,不关页);确认弹窗开着时由 ConfirmLayer
2912
+ // 的捕获级监听截停(只关确认框);事件都到不了这里。其余位置 Esc 一律关页。
2913
+ // 一键升级(页头更新卡「升级」按钮,统一入口):
2914
+ // POST /api/paoding/upgrade,服务端 spawn `dsh plugin --profile <profile>
2915
+ // update dsh-paoding` 原地换版本,分钟级操作。确认弹窗(原原生 confirm
2916
+ // 改通用确认弹窗,confirmCtrl 单例见 70 片段)文案按挂载形态分流:
2917
+ // dev link 直连形态(layout 由服务端 version 路由按包根 .git 判定)无法
2918
+ // 就地升级,把手动切换命令说在前头;installed(registry 版)原地换版本,
2919
+ // 从简。反馈(成功 note / 失败 err,含子进程输出尾部)都收在卡内,不与
2920
+ // 面板的 toast 反馈混流。升级非破坏性操作,danger 不置位;弹窗本体由
2921
+ // PageShell 末尾的 ConfirmLayer 渲染(页头与面板发起的确认共用一层)。
2922
+ function runUpgradeFlow(verInfo, setVerInfo, setUpgBusy, setUpgNote, setUpgErr) {
2923
+ if (!verInfo || !verInfo.latest) return;
2924
+ var isDev = verInfo.layout === "dev";
2925
+ var msg = isDev
2926
+ ? "当前为开发 link 直连形态:无法就地升级,请手动执行 dsh plugin add dsh-paoding@latest 切换到 registry 版本。仍要继续尝试吗?"
2927
+ : "确定升级到 " + versionTagOf(verInfo.latest) + "?升级将通过 dsh plugin update 原地换版本,完成后需重启 DSH 生效。";
2928
+ confirmCtrl.ask({
2929
+ title: "升级 dsh-paoding",
2930
+ body: msg,
2931
+ danger: false,
2932
+ confirmText: "升级",
2933
+ onConfirm: function () {
2934
+ setUpgBusy(true);
2935
+ setUpgNote("");
2936
+ setUpgErr("");
2937
+ paodingApi("upgrade", {}).then(function (r) {
2938
+ if (r && r.upToDate) { setUpgNote("已是最新版本"); return; }
2939
+ var done = versionTagOf((r && r.version) || verInfo.latest);
2940
+ setUpgNote((r && r.message) || ("✅ 已升级到 " + done + ",重启 DSH 后新版本生效(preset 会自动按新版重生成)"));
2941
+ // 重拉版本信息:服务端有 1 小时缓存且本进程挂的还是旧代码,重启前
2942
+ // 更新卡可能仍在(重启后自然消失),以此处的生效提示为准。
2943
+ paodingApi("version").then(function (d) {
2944
+ if (d && typeof d.current === "string" && d.current) setVerInfo(d);
2945
+ }).catch(function () { /* 静默:升级已完成,重拉失败不打扰 */ });
2946
+ }).catch(function (err) {
2947
+ // 失败要醒目:截服务端透传的子进程输出尾部 ~300 字符并入 err,方便
2948
+ // 定位 pnpm 缓存权限(EPERM)之类环境问题,不用翻服务端日志。
2949
+ var tail = typeof err.output === "string" && err.output ? ":" + err.output.slice(-300) : "";
2950
+ setUpgErr(err.message + tail);
2951
+ }).finally(function () {
2952
+ setUpgBusy(false);
2953
+ });
2954
+ },
2955
+ });
2956
+ }
2957
+
2958
+ // 确认弹窗层:订阅 70 片段的 confirmCtrl 单例(照 makeFooterEntry 的
2959
+ // useState + subscribe 写法),渲染 confirmModalOf。挂在 PageShell 根数组
2960
+ // 末尾——fixed 全屏遮罩覆盖整页,面板(删除角色 / 保存并应用)与页头
2961
+ // (升级)发起的确认都罩得住。
2962
+ // Esc 分层:自带 document 捕获级 keydown 监听。ConfigPanel 的捕获级监听
2963
+ // 先注册(ConfigPanel 是 PageShell 的子组件,子 effect 先于本组件 effect
2964
+ // 运行),但它只在自己弹窗 / 菜单开着时才 stopPropagation——确认框开着
2965
+ // 时该监听放行,本监听(注册在后)截停并只关确认框;stopPropagation 之后
2966
+ // PageShell 冒泡层的关页监听收不到事件,确认框开着时 Esc 不会顺带关页。
2967
+ // req 为空时不截停,Esc 照常落到关页监听。确认路径见 confirmModalOf:
2968
+ // 先 close(req 清空、弹窗卸载)再执行 onConfirm,异步期间重复 Enter /
2969
+ // 双击不会再触发。
2970
+ function ConfirmLayer() {
2971
+ var _cq = useState(confirmCtrl.getSnapshot()), confirmSnap = _cq[0], setConfirmSnap = _cq[1];
2972
+ useEffect(function () {
2973
+ return confirmCtrl.subscribe(function () { setConfirmSnap(confirmCtrl.getSnapshot()); });
2974
+ }, []);
2975
+ useEffect(function () {
2976
+ var onKey = function (e) {
2977
+ if (!e || e.key !== "Escape") return;
2978
+ if (!confirmCtrl.getSnapshot().req) return; // 无确认框不截停:放行给关页监听
2979
+ e.stopPropagation();
2980
+ confirmCtrl.close();
2981
+ };
2982
+ document.addEventListener("keydown", onKey, true);
2983
+ return function () { document.removeEventListener("keydown", onKey, true); };
2984
+ }, []);
2985
+ return confirmModalOf(confirmSnap.req, confirmCtrl.close);
2986
+ }
2987
+
2988
+ function PageShell(props) {
2989
+ var onClose = props.onClose;
2990
+ // 版本信息挂页头(拉取逻辑仍在 ConfigPanel 的 loadVersion,经 setVerInfo 回写)
2991
+ var _v = useState(null), verInfo = _v[0], setVerInfo = _v[1];
2992
+ // 升级动作随更新卡留在页头:独立 busy/note/err,不复用 ConfigPanel 的
2993
+ // 面板级互斥——页头与面板各自成区,写盘目标不同(升级换插件载荷、面板
2994
+ // 写预设与配置),升级期间面板照常预览编辑互不干扰。
2995
+ var _ub = useState(false), upgBusy = _ub[0], setUpgBusy = _ub[1];
2996
+ var _un = useState(""), upgNote = _un[0], setUpgNote = _un[1];
2997
+ var _ue = useState(""), upgErr = _ue[0], setUpgErr = _ue[1];
2998
+
2999
+ // 一键升级逻辑在共享的 runUpgradeFlow(页头更新卡统一走这一份)。
3000
+ function doUpgrade() {
3001
+ runUpgradeFlow(verInfo, setVerInfo, setUpgBusy, setUpgNote, setUpgErr);
3002
+ }
3003
+ useEffect(function () {
3004
+ var onKey = function (e) {
3005
+ if (e && e.key === "Escape" && typeof onClose === "function") onClose();
3006
+ };
3007
+ document.addEventListener("keydown", onKey);
3008
+ return function () { document.removeEventListener("keydown", onKey); };
3009
+ }, []);
3010
+ return h("div", { className: "pd-shell" },
3011
+ h("header", { className: "pd-pageHeader" },
3012
+ h("div", { className: "pd-pageTitles" },
3013
+ // 标题行:标题 + 版本号 + (有新版时的)更新卡并排;窄屏放不下时
3014
+ // 换行收纳,副标题独占下一行
3015
+ h("div", { className: "pd-titleRow" },
3016
+ h("h1", { className: "pd-pageTitle" }, "庖丁配置",
3017
+ verInfo && typeof verInfo.current === "string" && verInfo.current
3018
+ ? h("span", { className: "pd-versionTag" }, versionTagOf(verInfo.current))
3019
+ : null),
3020
+ versionRowOf(verInfo, { busy: upgBusy, onUpgrade: doUpgrade, note: upgNote, error: upgErr })),
3021
+ h("p", { className: "pd-pageSubtitle" },
3022
+ "为主 agent 与子 agent 编排工具、技能与角色,预览并应用生成的 agent.cordis.yml。")),
3023
+ h("button", {
3024
+ type: "button",
3025
+ className: "pd-closeBtn",
3026
+ "aria-label": "关闭庖丁配置",
3027
+ title: "关闭(Esc)",
3028
+ onClick: function () { if (typeof onClose === "function") onClose(); },
3029
+ }, h("span", { className: "pd-closeGlyph", "aria-hidden": "true" }, "×"))),
3030
+ h(ConfigPanel, { setVerInfo: setVerInfo }),
3031
+ // 反馈 toast 宿主:订阅模块级 toast 控制器,悬浮于操作坞上方
3032
+ h(ToastHost),
3033
+ // 通用确认弹窗层:订阅 confirmCtrl(发起方见各 confirmCtrl.ask 调用点),
3034
+ // 放根数组末尾——fixed 遮罩不占布局流,页头 / 面板发起的确认都盖得住;
3035
+ // 无确认时渲染 null,仅保留 Esc 监听。
3036
+ h(ConfirmLayer));
3037
+ }
3038
+
3039
+ // 全页 overlay 挂载:容器 div append 到会话中栏,createRoot 渲染 PageShell。
3040
+ // 懒挂载——首次打开才建容器 / 发起面板首屏请求;此后容器常驻(CSS 按
3041
+ // active 属性显隐),被 React 挤掉时由观察器重挂。
3042
+ function mountPanel(controller) {
3043
+ if (typeof document === "undefined" || typeof window === "undefined" || !ReactDOMClient) {
3044
+ return function () {};
3045
+ }
3046
+ var root;
3047
+ var container;
3048
+ var ensure = function () {
3049
+ if (!controller.getSnapshot().open) return; // 懒挂载:只在打开态维护容器
3050
+ if (container && container.isConnected) return;
3051
+ if (container) {
3052
+ if (root) root.unmount();
3053
+ root = undefined;
3054
+ container = undefined;
3055
+ }
3056
+ var column = document.querySelector(CONVERSATION_COLUMN_SELECTOR);
3057
+ if (!column) return;
3058
+ container = document.createElement("div");
3059
+ // dataset 驼峰转数据属性:dshPaodingView → data-dsh-paoding-view
3060
+ container.dataset.dshPaodingView = "";
3061
+ container.className = "pd-panelView";
3062
+ column.appendChild(container);
3063
+ root = ReactDOMClient.createRoot(container);
3064
+ root.render(h(PageShell, {
3065
+ onClose: function () { controller.close(); },
3066
+ }));
3067
+ };
3068
+ // 会话中栏晚于插件挂载 / 容器被挤掉时重挂
3069
+ var waitObserver = new MutationObserver(ensure);
3070
+ waitObserver.observe(document.body, { childList: true, subtree: true });
3071
+
3072
+ var suppressCompatibilityClose = false;
3073
+ var applyActive = function () {
3074
+ if (!controller.getSnapshot().open) {
3075
+ document.documentElement.removeAttribute(ACTIVE_ATTR);
3076
+ return;
3077
+ }
3078
+ // 互斥:先广播请走 taskboard / ssh(兼容旧面板的事件协议),清掉三家
3079
+ // 的 active 属性(它们的属性观察者会自行关闭),再点亮自己、以
3080
+ // detail:"paoding" 对外宣告。
3081
+ suppressCompatibilityClose = true;
3082
+ try {
3083
+ document.dispatchEvent(new CustomEvent(ACTIVATE_EVENT, { detail: "ssh" }));
3084
+ document.dispatchEvent(new CustomEvent(ACTIVATE_EVENT, { detail: "taskboard" }));
3085
+ } finally {
3086
+ suppressCompatibilityClose = false;
3087
+ }
3088
+ document.documentElement.removeAttribute(MNEMON_ACTIVE_ATTR);
3089
+ document.documentElement.removeAttribute(TASKBOARD_ACTIVE_ATTR);
3090
+ document.documentElement.removeAttribute(SSH_ACTIVE_ATTR);
3091
+ document.documentElement.setAttribute(ACTIVE_ATTR, "");
3092
+ ensure();
3093
+ document.dispatchEvent(new CustomEvent(ACTIVATE_EVENT, { detail: "paoding" }));
3094
+ };
3095
+ // 其它全页面板激活(taskboard / ssh / mnemon)→ 关闭自己
3096
+ var onOtherPanelActivate = function (event) {
3097
+ if (suppressCompatibilityClose || !controller.getSnapshot().open) return;
3098
+ var detail = event && event.detail;
3099
+ if (detail === "taskboard" || detail === "ssh" || detail === "mnemon") controller.close();
3100
+ };
3101
+ var onSidebarContextClick = function (event) {
3102
+ if (!controller.getSnapshot().open) return;
3103
+ var target = event.target;
3104
+ if (target && typeof target.closest === "function" && target.closest(SIDEBAR_CONTEXT_SELECTOR)) {
3105
+ controller.close();
3106
+ }
3107
+ };
3108
+ // 属性兜底:发现自己 active 丢了、或别的面板 active 了 → 关闭
3109
+ var activeObserver = new MutationObserver(function () {
3110
+ if (!controller.getSnapshot().open) return;
3111
+ var html = document.documentElement;
3112
+ if (!html.hasAttribute(ACTIVE_ATTR) || html.hasAttribute(MNEMON_ACTIVE_ATTR) ||
3113
+ html.hasAttribute(TASKBOARD_ACTIVE_ATTR) || html.hasAttribute(SSH_ACTIVE_ATTR)) {
3114
+ controller.close();
3115
+ }
3116
+ });
3117
+ activeObserver.observe(document.documentElement, {
3118
+ attributes: true,
3119
+ attributeFilter: [ACTIVE_ATTR, MNEMON_ACTIVE_ATTR, TASKBOARD_ACTIVE_ATTR, SSH_ACTIVE_ATTR],
3120
+ });
3121
+
3122
+ document.addEventListener("click", onSidebarContextClick, true);
3123
+ document.addEventListener(ACTIVATE_EVENT, onOtherPanelActivate);
3124
+ var unsubscribe = controller.subscribe(applyActive);
3125
+ applyActive();
3126
+ return function () {
3127
+ document.removeEventListener("click", onSidebarContextClick, true);
3128
+ document.removeEventListener(ACTIVATE_EVENT, onOtherPanelActivate);
3129
+ activeObserver.disconnect();
3130
+ waitObserver.disconnect();
3131
+ unsubscribe();
3132
+ document.documentElement.removeAttribute(ACTIVE_ATTR);
3133
+ if (root) root.unmount();
3134
+ root = undefined;
3135
+ if (container) container.remove();
3136
+ container = undefined;
3137
+ };
3138
+ }
3139
+
3140
+ // ── cordis client 插件入口 ──────────────────────────────────────────────
3141
+
3142
+ // 依赖声明:底部入口(sidebar.footer.action)与事件流委派行
3143
+ // (tool.call.toolview)都走宿主 slots 键控槽,须声明 slots 服务——
3144
+ // cordis 护栏下未声明服务的 ctx.slots 取不到(旧代码此处曾是空表,
3145
+ // 槽口挂载全部静默折返,入口不显示)。
1153
3146
  var inject = ["slots"];
1154
3147
 
1155
3148
  function apply(ctx) {
1156
- ctx.slots.inject("settings.section", function () {
1157
- return ctx.slots.register({
1158
- name: "settings.section",
1159
- id: "paoding",
1160
- order: 50,
1161
- label: function () { return "庖丁配置"; },
1162
- }, ConfigPanel);
1163
- });
3149
+ var controller = createWorkspaceController();
3150
+ var manual = []; // ctx.effect 不可用时的保底清理函数累计
3151
+ // 优先交给宿主 effect 管生命周期(重载自动清理);ctx.effect 不可用或
3152
+ // 调用失败则直接执行并自行累计清理,保底不炸。
3153
+ function track(mount, label) {
3154
+ if (ctx && typeof ctx.effect === "function") {
3155
+ try {
3156
+ ctx.effect(mount, label);
3157
+ return;
3158
+ } catch (err) { /* 落入手动路径 */ }
3159
+ }
3160
+ try {
3161
+ var dispose = mount();
3162
+ if (typeof dispose === "function") manual.push(dispose);
3163
+ } catch (err) {
3164
+ if (ctx && ctx.logger && typeof ctx.logger.warn === "function") {
3165
+ ctx.logger.warn("paoding-config-ui: " + label + " mount failed: " + String((err && err.message) || err));
3166
+ }
3167
+ }
3168
+ }
3169
+ track(function () { return mountFooterEntry(ctx, controller); }, "paoding-config-ui: footer entry");
3170
+ track(function () { return mountPanel(controller); }, "paoding-config-ui: page overlay");
3171
+ track(function () { return mountWorkspaceMenuBridge(controller); }, "paoding-config-ui: workspace menu bridge");
3172
+ track(function () { return mountDelegateRows(ctx); }, "paoding-config-ui: delegate rows");
3173
+ return function () {
3174
+ manual.forEach(function (dispose) {
3175
+ try { dispose(); } catch (err) { /* 单个清理失败不阻断其余清理 */ }
3176
+ });
3177
+ };
1164
3178
  }
1165
3179
 
1166
3180
  exports.inject = inject;