dsh-plugin-subagent-director 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/LICENSE +21 -0
- package/README.md +135 -0
- package/lib/bridge-contract.d.ts +41 -0
- package/lib/bridge-contract.d.ts.map +1 -0
- package/lib/bridge-contract.js +7 -0
- package/lib/bridge-contract.js.map +1 -0
- package/lib/bridge-entry.d.ts +29 -0
- package/lib/bridge-entry.d.ts.map +1 -0
- package/lib/bridge-entry.js +10 -0
- package/lib/bridge-entry.js.map +1 -0
- package/lib/client/RoleCard.d.ts +23 -0
- package/lib/client/RoleCard.d.ts.map +1 -0
- package/lib/client/RoleCard.js +72 -0
- package/lib/client/RoleCard.js.map +1 -0
- package/lib/client/SubagentModelDock.d.ts +29 -0
- package/lib/client/SubagentModelDock.d.ts.map +1 -0
- package/lib/client/SubagentModelDock.js +32 -0
- package/lib/client/SubagentModelDock.js.map +1 -0
- package/lib/client/SubagentOptionsSection.d.ts +23 -0
- package/lib/client/SubagentOptionsSection.d.ts.map +1 -0
- package/lib/client/SubagentOptionsSection.js +164 -0
- package/lib/client/SubagentOptionsSection.js.map +1 -0
- package/lib/client/index.d.ts +44 -0
- package/lib/client/index.d.ts.map +1 -0
- package/lib/client/index.js +1541 -0
- package/lib/client/index.js.map +1 -0
- package/lib/client/locales.d.ts +13 -0
- package/lib/client/locales.d.ts.map +1 -0
- package/lib/client/locales.js +107 -0
- package/lib/client/locales.js.map +1 -0
- package/lib/client/store-logic.d.ts +105 -0
- package/lib/client/store-logic.d.ts.map +1 -0
- package/lib/client/store-logic.js +143 -0
- package/lib/client/store-logic.js.map +1 -0
- package/lib/client/store.d.ts +104 -0
- package/lib/client/store.d.ts.map +1 -0
- package/lib/client/store.js +223 -0
- package/lib/client/store.js.map +1 -0
- package/lib/client/subagent-model.d.ts +47 -0
- package/lib/client/subagent-model.d.ts.map +1 -0
- package/lib/client/subagent-model.js +81 -0
- package/lib/client/subagent-model.js.map +1 -0
- package/lib/client/ui.d.ts +26 -0
- package/lib/client/ui.d.ts.map +1 -0
- package/lib/client/ui.js +102 -0
- package/lib/client/ui.js.map +1 -0
- package/lib/config.d.ts +63 -0
- package/lib/config.d.ts.map +1 -0
- package/lib/config.js +26 -0
- package/lib/config.js.map +1 -0
- package/lib/delegation-tool.d.ts +169 -0
- package/lib/delegation-tool.d.ts.map +1 -0
- package/lib/delegation-tool.js +404 -0
- package/lib/delegation-tool.js.map +1 -0
- package/lib/envelope.d.ts +97 -0
- package/lib/envelope.d.ts.map +1 -0
- package/lib/envelope.js +136 -0
- package/lib/envelope.js.map +1 -0
- package/lib/guidance.d.ts +37 -0
- package/lib/guidance.d.ts.map +1 -0
- package/lib/guidance.js +46 -0
- package/lib/guidance.js.map +1 -0
- package/lib/index.d.ts +29 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +63 -0
- package/lib/index.js.map +1 -0
- package/lib/remote.d.ts +102 -0
- package/lib/remote.d.ts.map +1 -0
- package/lib/remote.js +254 -0
- package/lib/remote.js.map +1 -0
- package/lib/route-resolver.d.ts +118 -0
- package/lib/route-resolver.d.ts.map +1 -0
- package/lib/route-resolver.js +71 -0
- package/lib/route-resolver.js.map +1 -0
- package/lib/settings.d.ts +157 -0
- package/lib/settings.d.ts.map +1 -0
- package/lib/settings.js +118 -0
- package/lib/settings.js.map +1 -0
- package/package.json +110 -0
|
@@ -0,0 +1,1541 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "dsh-plugin-subagent-director",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let _deepseek_ai_dsh_client_web_react = require("@deepseek-ai/dsh-client-web-react");
|
|
8
|
+
let _deepseek_ai_dsh_client_runtime_client = require("@deepseek-ai/dsh-client-runtime/client");
|
|
9
|
+
let react = require("react");
|
|
10
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
11
|
+
//#region src/client/locales.ts
|
|
12
|
+
/** English dictionary ($subagentDirector). */
|
|
13
|
+
const en = {
|
|
14
|
+
"nav": "Subagent Director",
|
|
15
|
+
"sectionIntro": "Choose the LLM provider and model each subagent uses, and define role templates that bind a model and persona to a delegation.",
|
|
16
|
+
"defaultsHeading": "Default model",
|
|
17
|
+
"defaultsHint": "Used when a call or role does not name a provider/model.",
|
|
18
|
+
"defaultProvider": "Provider",
|
|
19
|
+
"defaultModel": "Model",
|
|
20
|
+
"defaultReasoningEffort": "Reasoning effort",
|
|
21
|
+
"restoreDefaults": "Restore defaults",
|
|
22
|
+
"provider": "Provider",
|
|
23
|
+
"model": "Model",
|
|
24
|
+
"reasoningEffort": "Reasoning effort",
|
|
25
|
+
"persona": "Persona",
|
|
26
|
+
"rolesHeading": "Role templates",
|
|
27
|
+
"rolesHint": "A role binds delegation guidance, a persona, and optionally a provider/model to a subagent.",
|
|
28
|
+
"addRole": "Add role",
|
|
29
|
+
"emptyRoles": "No role templates yet. Add one to start planning subagent responsibilities.",
|
|
30
|
+
"roleId": "Role id",
|
|
31
|
+
"roleDisplayName": "Display name",
|
|
32
|
+
"roleDescription": "Delegation guidance",
|
|
33
|
+
"rolePersona": "Persona",
|
|
34
|
+
"setDefaultRole": "Set as default",
|
|
35
|
+
"defaultRoleBadge": "Default",
|
|
36
|
+
"deleteRole": "Delete",
|
|
37
|
+
"confirmDeleteRole": "Delete role “{id}”? Roles referenced by defaultRole will fall back to the plugin default.",
|
|
38
|
+
"removeRoleDone": "Role removed",
|
|
39
|
+
"addedRole": "Role added",
|
|
40
|
+
"roleUpdated": "Role updated",
|
|
41
|
+
"conflict": "The settings changed on the server. Reloading your edits — please review and retry.",
|
|
42
|
+
"rejected": "The change was rejected by the settings provider.",
|
|
43
|
+
"fatal": "Could not save: {message}",
|
|
44
|
+
"loadError": "Could not load preference settings.",
|
|
45
|
+
"readonly": "Settings are read-only in this deployment.",
|
|
46
|
+
"save": "Save",
|
|
47
|
+
"cancel": "Cancel",
|
|
48
|
+
"fallbackOnInvalid": "Fall back to the parent agent when a role-bound model is invalid",
|
|
49
|
+
"roleIdPlaceholder": "e.g. code-reviewer",
|
|
50
|
+
"displayNamePlaceholder": "e.g. Code Reviewer",
|
|
51
|
+
"descriptionPlaceholder": "When to delegate to this role and what it should do…",
|
|
52
|
+
"personaPlaceholder": "Optional behavior/identity text injected into the subagent…",
|
|
53
|
+
"modelOptional": "Save the card to apply changes",
|
|
54
|
+
"providerMissing": "No provider selected",
|
|
55
|
+
"restoreDone": "Defaults restored",
|
|
56
|
+
"invalidRoleId": "Role id must be kebab-case (lowercase letters, digits, single hyphens).",
|
|
57
|
+
"bridgeUnavailable": "The Subagent Director settings bridge is not available on this server. Please update/restart the web application so it installs the /subagent-director channel.",
|
|
58
|
+
"modelRanOn": "Subagent ran on",
|
|
59
|
+
"modelNotRecorded": "Subagent model not recorded yet",
|
|
60
|
+
"modelRanOnTitle": "Model route: {model}"
|
|
61
|
+
};
|
|
62
|
+
/** Chinese dictionary. */
|
|
63
|
+
const zh = {
|
|
64
|
+
"nav": "子代理导演",
|
|
65
|
+
"sectionIntro": "为每个子代理选择 LLM 供应商与模型,并以角色模板把模型与 persona 绑定到每次委派。",
|
|
66
|
+
"defaultsHeading": "默认模型",
|
|
67
|
+
"defaultsHint": "当某次调用或角色未指定供应商/模型时使用。",
|
|
68
|
+
"defaultProvider": "供应商",
|
|
69
|
+
"defaultModel": "模型",
|
|
70
|
+
"defaultReasoningEffort": "推理强度",
|
|
71
|
+
"restoreDefaults": "恢复默认",
|
|
72
|
+
"provider": "供应商",
|
|
73
|
+
"model": "模型",
|
|
74
|
+
"reasoningEffort": "推理强度",
|
|
75
|
+
"persona": "人设",
|
|
76
|
+
"rolesHeading": "角色模板",
|
|
77
|
+
"rolesHint": "角色把委派指引、persona 与可选的供应商/模型绑定到子代理。",
|
|
78
|
+
"addRole": "添加角色",
|
|
79
|
+
"emptyRoles": "还没有角色模板。添加一个以开始规划子代理职责。",
|
|
80
|
+
"roleId": "角色 id",
|
|
81
|
+
"roleDisplayName": "显示名称",
|
|
82
|
+
"roleDescription": "委派指引",
|
|
83
|
+
"rolePersona": "人设",
|
|
84
|
+
"setDefaultRole": "设为默认",
|
|
85
|
+
"defaultRoleBadge": "默认",
|
|
86
|
+
"deleteRole": "删除",
|
|
87
|
+
"confirmDeleteRole": "删除角色“{id}”?若 defaultRole 引用了它,将回退到插件默认值。",
|
|
88
|
+
"removeRoleDone": "角色已删除",
|
|
89
|
+
"addedRole": "角色已添加",
|
|
90
|
+
"roleUpdated": "角色已更新",
|
|
91
|
+
"conflict": "服务端设置已变更。已重新加载你的编辑——请复核并重试。",
|
|
92
|
+
"rejected": "更改被设置提供方拒绝。",
|
|
93
|
+
"fatal": "无法保存:{message}",
|
|
94
|
+
"loadError": "无法加载偏好设置。",
|
|
95
|
+
"readonly": "当前部署中设置为只读。",
|
|
96
|
+
"save": "保存",
|
|
97
|
+
"cancel": "取消",
|
|
98
|
+
"fallbackOnInvalid": "当角色绑定的模型无效时回退到父代理",
|
|
99
|
+
"roleIdPlaceholder": "例如 code-reviewer",
|
|
100
|
+
"displayNamePlaceholder": "例如 代码审查员",
|
|
101
|
+
"descriptionPlaceholder": "何时委派给此角色、让其做什么……",
|
|
102
|
+
"personaPlaceholder": "注入子代理的可选行为/身份文案……",
|
|
103
|
+
"modelOptional": "保存卡片以应用更改",
|
|
104
|
+
"providerMissing": "未选择供应商",
|
|
105
|
+
"restoreDone": "已恢复默认",
|
|
106
|
+
"invalidRoleId": "角色 id 必须为 kebab-case(小写字母、数字、单个连字符)。",
|
|
107
|
+
"bridgeUnavailable": "此服务器尚未提供 Subagent Director 设置桥接通道(/subagent-director)。请重启 Web 应用以安装该通道后再试。",
|
|
108
|
+
"modelRanOn": "子代理实际运行于",
|
|
109
|
+
"modelNotRecorded": "尚未记录到子代理模型",
|
|
110
|
+
"modelRanOnTitle": "模型路由:{model}"
|
|
111
|
+
};
|
|
112
|
+
//#endregion
|
|
113
|
+
//#region src/bridge-contract.ts
|
|
114
|
+
/** Absolute RPC channel the bridge owns on the Host web server. */
|
|
115
|
+
const SUBAGENT_DIRECTOR_RPC_CHANNEL = "/subagent-director";
|
|
116
|
+
/** Endpoint that returns the namespace's redacted wire view. */
|
|
117
|
+
const SUBAGENT_DIRECTOR_RPC_VIEW = "settingsView";
|
|
118
|
+
/** Endpoint that applies one path-op mutation. */
|
|
119
|
+
const SUBAGENT_DIRECTOR_RPC_MUTATE = "settingsMutate";
|
|
120
|
+
//#endregion
|
|
121
|
+
//#region src/client/store-logic.ts
|
|
122
|
+
/** Path of the roles map from the section root. */
|
|
123
|
+
const ROLES_PATH = ["roles"];
|
|
124
|
+
/** Normalize an optional string: blank/whitespace becomes undefined (removed). */
|
|
125
|
+
function optional(value) {
|
|
126
|
+
if (value === void 0) return void 0;
|
|
127
|
+
const trimmed = value.trim();
|
|
128
|
+
return trimmed.length === 0 ? void 0 : trimmed;
|
|
129
|
+
}
|
|
130
|
+
/** Generate a kebab-case id from a display name; falls back to a prefix + counter. */
|
|
131
|
+
function roleIdFromName(name, existing, prefix = "role") {
|
|
132
|
+
const base = name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
133
|
+
const candidate = base.length > 0 ? base : prefix;
|
|
134
|
+
if (!existing.has(candidate)) return candidate;
|
|
135
|
+
for (let i = 2; i < 1e3; i++) if (!existing.has(candidate + "-" + i)) return candidate + "-" + i;
|
|
136
|
+
return candidate + "-" + Date.now().toString(36);
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Build the path ops that create or fully replace one role. A single set at the
|
|
140
|
+
* role's root writes every field at once (mutate resolves against the stored
|
|
141
|
+
* section, so intermediate objects materialize).
|
|
142
|
+
*/
|
|
143
|
+
function addRoleOps(id, role) {
|
|
144
|
+
return [{
|
|
145
|
+
op: "set",
|
|
146
|
+
path: [ROLES_PATH[0], id],
|
|
147
|
+
value: {
|
|
148
|
+
displayName: role.displayName,
|
|
149
|
+
description: role.description,
|
|
150
|
+
...optional(role.persona) !== void 0 ? { persona: optional(role.persona) } : {},
|
|
151
|
+
...optional(role.provider) !== void 0 ? { provider: optional(role.provider) } : {},
|
|
152
|
+
...optional(role.model) !== void 0 ? { model: optional(role.model) } : {},
|
|
153
|
+
...optional(role.reasoningEffort) !== void 0 ? { reasoningEffort: optional(role.reasoningEffort) } : {}
|
|
154
|
+
}
|
|
155
|
+
}];
|
|
156
|
+
}
|
|
157
|
+
/** Field-by-field set-edits when a value changed from the stored role; unset when cleared. */
|
|
158
|
+
function fieldEdit(path, stored, next) {
|
|
159
|
+
const store = typeof stored === "string" ? stored : void 0;
|
|
160
|
+
const normalized = optional(next);
|
|
161
|
+
if (normalized === store) return void 0;
|
|
162
|
+
if (normalized === void 0) return {
|
|
163
|
+
op: "unset",
|
|
164
|
+
path: [...path]
|
|
165
|
+
};
|
|
166
|
+
return {
|
|
167
|
+
op: "set",
|
|
168
|
+
path: [...path],
|
|
169
|
+
value: normalized
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Diff one role between its stored value and the edited draft. Only changed
|
|
174
|
+
* fields become ops; clearing a field becomes an unset that restores the
|
|
175
|
+
* composition base / removes the user override.
|
|
176
|
+
*/
|
|
177
|
+
function updateRoleOps(id, before, draft) {
|
|
178
|
+
const b = before ?? {};
|
|
179
|
+
const ops = [];
|
|
180
|
+
const push = (op) => {
|
|
181
|
+
if (op) ops.push(op);
|
|
182
|
+
};
|
|
183
|
+
const base = [ROLES_PATH[0], id];
|
|
184
|
+
push(fieldEdit([...base, "displayName"], b.displayName, draft.displayName));
|
|
185
|
+
push(fieldEdit([...base, "description"], b.description, draft.description));
|
|
186
|
+
push(fieldEdit([...base, "persona"], b.persona, draft.persona));
|
|
187
|
+
push(fieldEdit([...base, "provider"], b.provider, draft.provider));
|
|
188
|
+
push(fieldEdit([...base, "model"], b.model, draft.model));
|
|
189
|
+
push(fieldEdit([...base, "reasoningEffort"], b.reasoningEffort, draft.reasoningEffort));
|
|
190
|
+
return ops;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Ops to remove one role. When it was the defaultRole, the reference is cleared
|
|
194
|
+
* too so a stale default never points at a removed role.
|
|
195
|
+
*/
|
|
196
|
+
function removeRoleOps(id, current) {
|
|
197
|
+
const ops = [{
|
|
198
|
+
op: "unset",
|
|
199
|
+
path: [ROLES_PATH[0], id]
|
|
200
|
+
}];
|
|
201
|
+
if (current.defaultRole === id) ops.push({
|
|
202
|
+
op: "unset",
|
|
203
|
+
path: ["defaultRole"]
|
|
204
|
+
});
|
|
205
|
+
return ops;
|
|
206
|
+
}
|
|
207
|
+
/** Ops to promote one role to the default. */
|
|
208
|
+
function setDefaultRoleOps(id) {
|
|
209
|
+
return [{
|
|
210
|
+
op: "set",
|
|
211
|
+
path: ["defaultRole"],
|
|
212
|
+
value: id
|
|
213
|
+
}];
|
|
214
|
+
}
|
|
215
|
+
function defaultModelOps(before, edits) {
|
|
216
|
+
const ops = [];
|
|
217
|
+
const push = (op) => {
|
|
218
|
+
if (op) ops.push(op);
|
|
219
|
+
};
|
|
220
|
+
push(fieldEdit(["defaultProvider"], before.defaultProvider, edits.provider));
|
|
221
|
+
push(fieldEdit(["defaultModel"], before.defaultModel, edits.model));
|
|
222
|
+
push(fieldEdit(["defaultReasoningEffort"], before.defaultReasoningEffort, edits.reasoningEffort));
|
|
223
|
+
return ops;
|
|
224
|
+
}
|
|
225
|
+
/** Ops to clear every default-model field and the defaultRole back to composition defaults. */
|
|
226
|
+
function restoreDefaultsOps(current) {
|
|
227
|
+
const ops = [];
|
|
228
|
+
if (current.defaultProvider !== void 0) ops.push({
|
|
229
|
+
op: "unset",
|
|
230
|
+
path: ["defaultProvider"]
|
|
231
|
+
});
|
|
232
|
+
if (current.defaultModel !== void 0) ops.push({
|
|
233
|
+
op: "unset",
|
|
234
|
+
path: ["defaultModel"]
|
|
235
|
+
});
|
|
236
|
+
if (current.defaultReasoningEffort !== void 0) ops.push({
|
|
237
|
+
op: "unset",
|
|
238
|
+
path: ["defaultReasoningEffort"]
|
|
239
|
+
});
|
|
240
|
+
if (current.defaultRole !== void 0) ops.push({
|
|
241
|
+
op: "unset",
|
|
242
|
+
path: ["defaultRole"]
|
|
243
|
+
});
|
|
244
|
+
return ops;
|
|
245
|
+
}
|
|
246
|
+
/** Map an RPC error code to a UI outcome; unknown/undefined errors are fatal. */
|
|
247
|
+
function classifyMutateError(code, _message) {
|
|
248
|
+
if (code === "settings-conflict") return "conflict";
|
|
249
|
+
if (code === "settings-rejected" || code === "schema-validation") return "rejected";
|
|
250
|
+
return "fatal";
|
|
251
|
+
}
|
|
252
|
+
//#endregion
|
|
253
|
+
//#region src/client/store.ts
|
|
254
|
+
/** The settings namespace this page reads and writes. */
|
|
255
|
+
const SUBAGENT_DIRECTOR_NS = "subagent-director";
|
|
256
|
+
/** Initial empty snapshot. */
|
|
257
|
+
function initialSubagentOptionsState() {
|
|
258
|
+
return {
|
|
259
|
+
status: "idle",
|
|
260
|
+
error: null,
|
|
261
|
+
writable: true,
|
|
262
|
+
namespace: void 0,
|
|
263
|
+
section: void 0,
|
|
264
|
+
revision: 0,
|
|
265
|
+
providers: [],
|
|
266
|
+
models: [],
|
|
267
|
+
loading: false
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
/** Human text for a rejected wire call. */
|
|
271
|
+
function messageOf(error) {
|
|
272
|
+
if (error instanceof Error) return error.message;
|
|
273
|
+
return typeof error === "string" ? error : String(error);
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Raised when the /subagent-director bridge channel is not reachable on the
|
|
277
|
+
* current transport (a Host that predates the bridge answers nothing). The
|
|
278
|
+
* store surfaces this as the localized `bridgeUnavailable` copy instead of a
|
|
279
|
+
* raw transport string.
|
|
280
|
+
*/
|
|
281
|
+
var BridgeUnavailableError = class extends Error {
|
|
282
|
+
constructor() {
|
|
283
|
+
super("Subagent Director settings bridge (/subagent-director) is not available on this server");
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
/** Whether a thrown value means the bridge channel could not be called at all. */
|
|
287
|
+
function isBridgeUnavailable(error) {
|
|
288
|
+
return error instanceof BridgeUnavailableError;
|
|
289
|
+
}
|
|
290
|
+
/** The settings page controller (one per settings surface). */
|
|
291
|
+
var SubagentOptionsStore = class {
|
|
292
|
+
store;
|
|
293
|
+
wire;
|
|
294
|
+
generation = 0;
|
|
295
|
+
constructor(wire) {
|
|
296
|
+
this.wire = wire;
|
|
297
|
+
this.store = (0, _deepseek_ai_dsh_client_runtime_client.createSnapshotStore)(initialSubagentOptionsState());
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Call one bridge endpoint over the generic RPC channel. Returns the RpcResult
|
|
301
|
+
* (ok or error branch). A transport-level rejection — the Host has no
|
|
302
|
+
* /subagent-director channel — is folded into BridgeUnavailableError so the
|
|
303
|
+
* caller can show the localized message.
|
|
304
|
+
*/
|
|
305
|
+
async callBridge(endpoint, payload) {
|
|
306
|
+
let result;
|
|
307
|
+
try {
|
|
308
|
+
result = await this.wire.rpc.call(SUBAGENT_DIRECTOR_RPC_CHANNEL, endpoint, payload);
|
|
309
|
+
} catch (error) {
|
|
310
|
+
throw new BridgeUnavailableError();
|
|
311
|
+
}
|
|
312
|
+
if (!result.ok) return {
|
|
313
|
+
ok: false,
|
|
314
|
+
error: result.error
|
|
315
|
+
};
|
|
316
|
+
return {
|
|
317
|
+
ok: true,
|
|
318
|
+
value: result.value
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
/** Refresh the whole page snapshot: provider directory + model catalog + own namespace. */
|
|
322
|
+
async load() {
|
|
323
|
+
const generation = ++this.generation;
|
|
324
|
+
this.store.update((s) => {
|
|
325
|
+
s.status = "loading";
|
|
326
|
+
s.error = null;
|
|
327
|
+
s.loading = true;
|
|
328
|
+
});
|
|
329
|
+
try {
|
|
330
|
+
const [providersResponse, modelsResponse, viewResult] = await Promise.all([
|
|
331
|
+
this.wire.llm.providers({}),
|
|
332
|
+
this.wire.llm.models({}),
|
|
333
|
+
this.callBridge(SUBAGENT_DIRECTOR_RPC_VIEW)
|
|
334
|
+
]);
|
|
335
|
+
if (!providersResponse.result.ok) throw new Error(providersResponse.result.error.message);
|
|
336
|
+
if (!modelsResponse.result.ok) throw new Error(modelsResponse.result.error.message);
|
|
337
|
+
if (!viewResult.ok) throw new Error(this.errorMessage(viewResult.error));
|
|
338
|
+
if (generation !== this.generation) return;
|
|
339
|
+
const view = viewResult.value.view;
|
|
340
|
+
const section = view?.value ?? {};
|
|
341
|
+
const writable = viewResult.value.writable;
|
|
342
|
+
const providers = providersResponse.result.value.providers;
|
|
343
|
+
const models = modelsResponse.result.value.groups;
|
|
344
|
+
this.store.update((s) => {
|
|
345
|
+
s.status = "ready";
|
|
346
|
+
s.error = null;
|
|
347
|
+
s.writable = writable;
|
|
348
|
+
s.providers = providers;
|
|
349
|
+
s.models = models;
|
|
350
|
+
s.namespace = view;
|
|
351
|
+
s.section = section;
|
|
352
|
+
s.revision = view?.revision ?? 0;
|
|
353
|
+
s.loading = false;
|
|
354
|
+
});
|
|
355
|
+
} catch (error) {
|
|
356
|
+
if (generation !== this.generation) return;
|
|
357
|
+
this.store.update((s) => {
|
|
358
|
+
s.status = "error";
|
|
359
|
+
s.error = isBridgeUnavailable(error) ? this.wire.t("bridgeUnavailable") : messageOf(error);
|
|
360
|
+
s.loading = false;
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
/** Human text for a bridge RPC error branch. */
|
|
365
|
+
errorMessage(error) {
|
|
366
|
+
if (error !== null && typeof error === "object" && "message" in error) {
|
|
367
|
+
const message = error.message;
|
|
368
|
+
if (typeof message === "string") return message;
|
|
369
|
+
}
|
|
370
|
+
return messageOf(error);
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Run one mutate and update the snapshot's revision. Returns a failure
|
|
374
|
+
* message (localized by the caller) or undefined on success. A
|
|
375
|
+
* settings-conflict re-reads the namespace and returns the conflict kind so
|
|
376
|
+
* the UI can show the "please review and retry" message.
|
|
377
|
+
*/
|
|
378
|
+
async mutate(ops) {
|
|
379
|
+
const state = this.store.getSnapshot();
|
|
380
|
+
const ns = SUBAGENT_DIRECTOR_NS;
|
|
381
|
+
const revision = state.revision;
|
|
382
|
+
let result;
|
|
383
|
+
try {
|
|
384
|
+
result = await this.callBridge(SUBAGENT_DIRECTOR_RPC_MUTATE, {
|
|
385
|
+
ns,
|
|
386
|
+
ops,
|
|
387
|
+
expectedRevision: revision
|
|
388
|
+
});
|
|
389
|
+
} catch (error) {
|
|
390
|
+
return {
|
|
391
|
+
ok: false,
|
|
392
|
+
kind: "fatal",
|
|
393
|
+
message: isBridgeUnavailable(error) ? this.wire.t("bridgeUnavailable") : messageOf(error)
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
if (result.ok) {
|
|
397
|
+
const admitted = result.value;
|
|
398
|
+
this.store.update((s) => {
|
|
399
|
+
s.revision = admitted.revision;
|
|
400
|
+
s.namespace = admitted;
|
|
401
|
+
s.section = admitted.value ?? {};
|
|
402
|
+
});
|
|
403
|
+
return {
|
|
404
|
+
ok: true,
|
|
405
|
+
kind: "fatal",
|
|
406
|
+
message: void 0
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
const code = this.errorCode(result.error);
|
|
410
|
+
const message = this.errorMessage(result.error);
|
|
411
|
+
const kind = classifyMutateError(code, message);
|
|
412
|
+
if (kind === "conflict") {
|
|
413
|
+
await this.reloadNamespace();
|
|
414
|
+
return {
|
|
415
|
+
ok: false,
|
|
416
|
+
kind,
|
|
417
|
+
message
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
return {
|
|
421
|
+
ok: false,
|
|
422
|
+
kind,
|
|
423
|
+
message
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
async reloadNamespace() {
|
|
427
|
+
try {
|
|
428
|
+
const result = await this.callBridge(SUBAGENT_DIRECTOR_RPC_VIEW);
|
|
429
|
+
if (!result.ok) return;
|
|
430
|
+
const view = result.value.view;
|
|
431
|
+
if (!view) return;
|
|
432
|
+
this.store.update((s) => {
|
|
433
|
+
s.namespace = view;
|
|
434
|
+
s.section = view.value ?? {};
|
|
435
|
+
s.revision = view.revision;
|
|
436
|
+
s.writable = result.value.writable;
|
|
437
|
+
});
|
|
438
|
+
} catch {}
|
|
439
|
+
}
|
|
440
|
+
/** Error code of a bridge RPC error branch, when present. */
|
|
441
|
+
errorCode(error) {
|
|
442
|
+
if (error !== null && typeof error === "object" && "code" in error) {
|
|
443
|
+
const code = error.code;
|
|
444
|
+
return typeof code === "string" ? code : void 0;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
async addRole(id, draft) {
|
|
448
|
+
const result = await this.mutate(addRoleOps(id, draft));
|
|
449
|
+
return result.ok ? void 0 : result.message;
|
|
450
|
+
}
|
|
451
|
+
async updateRole(id, before, draft) {
|
|
452
|
+
const result = await this.mutate(updateRoleOps(id, before, draft));
|
|
453
|
+
return result.ok ? void 0 : result.message;
|
|
454
|
+
}
|
|
455
|
+
async removeRole(id) {
|
|
456
|
+
const state = this.store.getSnapshot();
|
|
457
|
+
const result = await this.mutate(removeRoleOps(id, { defaultRole: state.section?.defaultRole }));
|
|
458
|
+
return result.ok ? void 0 : result.message;
|
|
459
|
+
}
|
|
460
|
+
async setDefaultRole(id) {
|
|
461
|
+
const result = await this.mutate(setDefaultRoleOps(id));
|
|
462
|
+
return result.ok ? void 0 : result.message;
|
|
463
|
+
}
|
|
464
|
+
async setDefaultModel(edits) {
|
|
465
|
+
const state = this.store.getSnapshot();
|
|
466
|
+
const result = await this.mutate(defaultModelOps(state.section ?? {}, edits));
|
|
467
|
+
return result.ok ? void 0 : result.message;
|
|
468
|
+
}
|
|
469
|
+
async restoreDefaults() {
|
|
470
|
+
const state = this.store.getSnapshot();
|
|
471
|
+
const result = await this.mutate(restoreDefaultsOps(state.section ?? {}));
|
|
472
|
+
return result.ok ? void 0 : result.message;
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
//#endregion
|
|
476
|
+
//#region src/client/ui.ts
|
|
477
|
+
const token = {
|
|
478
|
+
labelPrimary: "var(--dsw-alias-label-primary)",
|
|
479
|
+
labelSecondary: "var(--dsw-alias-label-secondary)",
|
|
480
|
+
labelTertiary: "var(--dsw-alias-label-tertiary)",
|
|
481
|
+
border: "var(--dsw-alias-border-l2)",
|
|
482
|
+
bgLayer1: "var(--dsw-alias-bg-layer-1)",
|
|
483
|
+
bgLayer3: "var(--dsw-alias-bg-layer-3)",
|
|
484
|
+
accent: "var(--dsw-alias-state-business-primary)",
|
|
485
|
+
danger: "var(--dsw-alias-state-error-primary)",
|
|
486
|
+
shadow: "var(--dsw-shadow-lv1)"
|
|
487
|
+
};
|
|
488
|
+
const rowStyle = {
|
|
489
|
+
display: "flex",
|
|
490
|
+
flexDirection: "column",
|
|
491
|
+
gap: 6
|
|
492
|
+
};
|
|
493
|
+
const fieldLabelStyle = {
|
|
494
|
+
color: token.labelSecondary,
|
|
495
|
+
fontSize: 12,
|
|
496
|
+
lineHeight: "16px"
|
|
497
|
+
};
|
|
498
|
+
const selectStyle = {
|
|
499
|
+
height: 30,
|
|
500
|
+
borderRadius: 6,
|
|
501
|
+
border: "1px solid " + token.border,
|
|
502
|
+
background: token.bgLayer1,
|
|
503
|
+
color: token.labelPrimary,
|
|
504
|
+
font: "inherit",
|
|
505
|
+
fontSize: 13,
|
|
506
|
+
padding: "0 8px",
|
|
507
|
+
outline: "none"
|
|
508
|
+
};
|
|
509
|
+
const textInputStyle = {
|
|
510
|
+
height: 30,
|
|
511
|
+
borderRadius: 6,
|
|
512
|
+
border: "1px solid " + token.border,
|
|
513
|
+
background: token.bgLayer1,
|
|
514
|
+
color: token.labelPrimary,
|
|
515
|
+
font: "inherit",
|
|
516
|
+
fontSize: 13,
|
|
517
|
+
padding: "0 8px",
|
|
518
|
+
outline: "none"
|
|
519
|
+
};
|
|
520
|
+
const textAreaStyle = {
|
|
521
|
+
borderRadius: 6,
|
|
522
|
+
border: "1px solid " + token.border,
|
|
523
|
+
background: token.bgLayer1,
|
|
524
|
+
color: token.labelPrimary,
|
|
525
|
+
font: "inherit",
|
|
526
|
+
fontSize: 13,
|
|
527
|
+
lineHeight: "18px",
|
|
528
|
+
padding: "6px 8px",
|
|
529
|
+
resize: "vertical",
|
|
530
|
+
minHeight: 56,
|
|
531
|
+
outline: "none"
|
|
532
|
+
};
|
|
533
|
+
const primaryButtonStyle = {
|
|
534
|
+
height: 28,
|
|
535
|
+
borderRadius: 6,
|
|
536
|
+
border: "1px solid " + token.accent,
|
|
537
|
+
background: token.accent,
|
|
538
|
+
color: "#fff",
|
|
539
|
+
font: "inherit",
|
|
540
|
+
fontSize: 13,
|
|
541
|
+
cursor: "pointer",
|
|
542
|
+
padding: "0 12px"
|
|
543
|
+
};
|
|
544
|
+
const ghostButtonStyle = {
|
|
545
|
+
height: 28,
|
|
546
|
+
borderRadius: 6,
|
|
547
|
+
border: "1px solid " + token.border,
|
|
548
|
+
background: "transparent",
|
|
549
|
+
color: token.labelPrimary,
|
|
550
|
+
font: "inherit",
|
|
551
|
+
fontSize: 13,
|
|
552
|
+
cursor: "pointer",
|
|
553
|
+
padding: "0 12px"
|
|
554
|
+
};
|
|
555
|
+
const dangerButtonStyle = {
|
|
556
|
+
...ghostButtonStyle,
|
|
557
|
+
color: token.danger,
|
|
558
|
+
borderColor: token.danger
|
|
559
|
+
};
|
|
560
|
+
const cardStyle = {
|
|
561
|
+
border: "1px solid " + token.border,
|
|
562
|
+
background: token.bgLayer3,
|
|
563
|
+
borderRadius: 10,
|
|
564
|
+
padding: 12,
|
|
565
|
+
display: "flex",
|
|
566
|
+
flexDirection: "column",
|
|
567
|
+
gap: 10,
|
|
568
|
+
minWidth: 0
|
|
569
|
+
};
|
|
570
|
+
const sectionWidth = {
|
|
571
|
+
width: "100%",
|
|
572
|
+
maxWidth: 760,
|
|
573
|
+
display: "flex",
|
|
574
|
+
flexDirection: "column",
|
|
575
|
+
gap: 16,
|
|
576
|
+
color: token.labelPrimary
|
|
577
|
+
};
|
|
578
|
+
//#endregion
|
|
579
|
+
//#region src/client/RoleCard.tsx
|
|
580
|
+
/** One role template card: read-only summary plus an inline editor.
|
|
581
|
+
* Provider/model/effort selects cascade from the model catalog; writes go
|
|
582
|
+
* through the controller as path ops, and failures return a localized message. */
|
|
583
|
+
function effortsFor$1(groups, provider, model) {
|
|
584
|
+
if (!provider || !model) return [];
|
|
585
|
+
return (groups.find((g) => g.id === provider)?.models.find((m) => m.id === model))?.reasoning?.efforts ?? [];
|
|
586
|
+
}
|
|
587
|
+
function RoleCard({ id, role, isDefault, groups, t, onSave, onDelete, onSetDefault }) {
|
|
588
|
+
const [editing, setEditing] = (0, react.useState)(false);
|
|
589
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
590
|
+
const [failure, setFailure] = (0, react.useState)(void 0);
|
|
591
|
+
const [draft, setDraft] = (0, react.useState)({
|
|
592
|
+
displayName: role.displayName,
|
|
593
|
+
description: role.description,
|
|
594
|
+
persona: role.persona ?? "",
|
|
595
|
+
provider: role.provider ?? "",
|
|
596
|
+
model: role.model ?? "",
|
|
597
|
+
reasoningEffort: role.reasoningEffort ?? ""
|
|
598
|
+
});
|
|
599
|
+
const provider = draft.provider || role.provider;
|
|
600
|
+
const model = draft.model || role.model;
|
|
601
|
+
const modelOptions = provider ? groups.find((g) => g.id === provider)?.models ?? [] : [];
|
|
602
|
+
const effortOptions = effortsFor$1(groups, provider, model);
|
|
603
|
+
const save = async () => {
|
|
604
|
+
setBusy(true);
|
|
605
|
+
setFailure(void 0);
|
|
606
|
+
try {
|
|
607
|
+
const message = await onSave(draft);
|
|
608
|
+
if (message !== void 0) {
|
|
609
|
+
setFailure(message);
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
setEditing(false);
|
|
613
|
+
} finally {
|
|
614
|
+
setBusy(false);
|
|
615
|
+
}
|
|
616
|
+
};
|
|
617
|
+
const remove = async () => {
|
|
618
|
+
if (!window.confirm(t("confirmDeleteRole").replace("{id}", id))) return;
|
|
619
|
+
setBusy(true);
|
|
620
|
+
setFailure(void 0);
|
|
621
|
+
try {
|
|
622
|
+
const message = await onDelete();
|
|
623
|
+
if (message !== void 0) setFailure(message);
|
|
624
|
+
} finally {
|
|
625
|
+
setBusy(false);
|
|
626
|
+
}
|
|
627
|
+
};
|
|
628
|
+
const setField = (field, value) => {
|
|
629
|
+
setDraft((d) => ({
|
|
630
|
+
...d,
|
|
631
|
+
[field]: value
|
|
632
|
+
}));
|
|
633
|
+
};
|
|
634
|
+
if (editing) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
635
|
+
style: cardStyle,
|
|
636
|
+
children: [
|
|
637
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
638
|
+
style: {
|
|
639
|
+
display: "flex",
|
|
640
|
+
alignItems: "baseline",
|
|
641
|
+
gap: 8
|
|
642
|
+
},
|
|
643
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", {
|
|
644
|
+
style: {
|
|
645
|
+
color: token.labelPrimary,
|
|
646
|
+
fontSize: 14
|
|
647
|
+
},
|
|
648
|
+
children: t("roleDisplayName")
|
|
649
|
+
}), isDefault ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
650
|
+
style: {
|
|
651
|
+
color: token.accent,
|
|
652
|
+
fontSize: 12
|
|
653
|
+
},
|
|
654
|
+
children: t("defaultRoleBadge")
|
|
655
|
+
}) : null]
|
|
656
|
+
}),
|
|
657
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
658
|
+
style: rowStyle,
|
|
659
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
660
|
+
style: fieldLabelStyle,
|
|
661
|
+
children: t("roleDisplayName")
|
|
662
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
663
|
+
style: textInputStyle,
|
|
664
|
+
value: draft.displayName,
|
|
665
|
+
placeholder: t("displayNamePlaceholder"),
|
|
666
|
+
onChange: (e) => setField("displayName", e.target.value)
|
|
667
|
+
})]
|
|
668
|
+
}),
|
|
669
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
670
|
+
style: rowStyle,
|
|
671
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
672
|
+
style: fieldLabelStyle,
|
|
673
|
+
children: t("roleDescription")
|
|
674
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
|
|
675
|
+
style: textAreaStyle,
|
|
676
|
+
value: draft.description,
|
|
677
|
+
placeholder: t("descriptionPlaceholder"),
|
|
678
|
+
onChange: (e) => setField("description", e.target.value)
|
|
679
|
+
})]
|
|
680
|
+
}),
|
|
681
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
682
|
+
style: rowStyle,
|
|
683
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
684
|
+
style: fieldLabelStyle,
|
|
685
|
+
children: t("rolePersona")
|
|
686
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
|
|
687
|
+
style: textAreaStyle,
|
|
688
|
+
value: draft.persona,
|
|
689
|
+
placeholder: t("personaPlaceholder"),
|
|
690
|
+
onChange: (e) => setField("persona", e.target.value)
|
|
691
|
+
})]
|
|
692
|
+
}),
|
|
693
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
694
|
+
style: {
|
|
695
|
+
display: "grid",
|
|
696
|
+
gridTemplateColumns: "repeat(3, minmax(0, 1fr))",
|
|
697
|
+
gap: 8
|
|
698
|
+
},
|
|
699
|
+
children: [
|
|
700
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
701
|
+
style: rowStyle,
|
|
702
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
703
|
+
style: fieldLabelStyle,
|
|
704
|
+
children: t("provider")
|
|
705
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
706
|
+
style: selectStyle,
|
|
707
|
+
value: draft.provider,
|
|
708
|
+
disabled: groups.length === 0,
|
|
709
|
+
onChange: (e) => setField("provider", e.target.value),
|
|
710
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
711
|
+
value: "",
|
|
712
|
+
children: "—"
|
|
713
|
+
}), groups.map((g) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
714
|
+
value: g.id,
|
|
715
|
+
children: g.name
|
|
716
|
+
}, g.id))]
|
|
717
|
+
})]
|
|
718
|
+
}),
|
|
719
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
720
|
+
style: rowStyle,
|
|
721
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
722
|
+
style: fieldLabelStyle,
|
|
723
|
+
children: t("model")
|
|
724
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
725
|
+
style: selectStyle,
|
|
726
|
+
value: draft.model,
|
|
727
|
+
disabled: modelOptions.length === 0,
|
|
728
|
+
onChange: (e) => setField("model", e.target.value),
|
|
729
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
730
|
+
value: "",
|
|
731
|
+
children: "—"
|
|
732
|
+
}), modelOptions.map((m) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
733
|
+
value: m.id,
|
|
734
|
+
children: m.name
|
|
735
|
+
}, m.id))]
|
|
736
|
+
})]
|
|
737
|
+
}),
|
|
738
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
739
|
+
style: rowStyle,
|
|
740
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
741
|
+
style: fieldLabelStyle,
|
|
742
|
+
children: t("reasoningEffort")
|
|
743
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
744
|
+
style: selectStyle,
|
|
745
|
+
value: draft.reasoningEffort,
|
|
746
|
+
disabled: effortOptions.length === 0,
|
|
747
|
+
onChange: (e) => setField("reasoningEffort", e.target.value),
|
|
748
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
749
|
+
value: "",
|
|
750
|
+
children: "—"
|
|
751
|
+
}), effortOptions.map((e) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
752
|
+
value: e.id,
|
|
753
|
+
children: e.name
|
|
754
|
+
}, e.id))]
|
|
755
|
+
})]
|
|
756
|
+
})
|
|
757
|
+
]
|
|
758
|
+
}),
|
|
759
|
+
failure !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
760
|
+
style: {
|
|
761
|
+
color: token.danger,
|
|
762
|
+
fontSize: 12
|
|
763
|
+
},
|
|
764
|
+
children: failure
|
|
765
|
+
}) : null,
|
|
766
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
767
|
+
style: {
|
|
768
|
+
display: "flex",
|
|
769
|
+
gap: 8
|
|
770
|
+
},
|
|
771
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
772
|
+
style: primaryButtonStyle,
|
|
773
|
+
disabled: busy,
|
|
774
|
+
onClick: save,
|
|
775
|
+
children: "Edit"
|
|
776
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
777
|
+
style: ghostButtonStyle,
|
|
778
|
+
disabled: busy,
|
|
779
|
+
onClick: () => setEditing(false),
|
|
780
|
+
children: t("cancel")
|
|
781
|
+
})]
|
|
782
|
+
})
|
|
783
|
+
]
|
|
784
|
+
});
|
|
785
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
786
|
+
style: cardStyle,
|
|
787
|
+
children: [
|
|
788
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
789
|
+
style: {
|
|
790
|
+
display: "flex",
|
|
791
|
+
alignItems: "baseline",
|
|
792
|
+
justifyContent: "space-between",
|
|
793
|
+
gap: 8
|
|
794
|
+
},
|
|
795
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", {
|
|
796
|
+
style: {
|
|
797
|
+
color: token.labelPrimary,
|
|
798
|
+
fontSize: 14
|
|
799
|
+
},
|
|
800
|
+
children: role.displayName || id
|
|
801
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
802
|
+
style: {
|
|
803
|
+
display: "flex",
|
|
804
|
+
alignItems: "center",
|
|
805
|
+
gap: 6
|
|
806
|
+
},
|
|
807
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
808
|
+
style: {
|
|
809
|
+
color: token.labelTertiary,
|
|
810
|
+
fontSize: 12,
|
|
811
|
+
fontVariantNumeric: "tabular-nums"
|
|
812
|
+
},
|
|
813
|
+
children: id
|
|
814
|
+
}), isDefault ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
815
|
+
style: {
|
|
816
|
+
color: token.accent,
|
|
817
|
+
fontSize: 12
|
|
818
|
+
},
|
|
819
|
+
children: t("defaultRoleBadge")
|
|
820
|
+
}) : null]
|
|
821
|
+
})]
|
|
822
|
+
}),
|
|
823
|
+
role.description ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
824
|
+
style: {
|
|
825
|
+
margin: 0,
|
|
826
|
+
color: token.labelSecondary,
|
|
827
|
+
fontSize: 13,
|
|
828
|
+
lineHeight: "18px"
|
|
829
|
+
},
|
|
830
|
+
children: role.description
|
|
831
|
+
}) : null,
|
|
832
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
833
|
+
style: {
|
|
834
|
+
display: "flex",
|
|
835
|
+
flexWrap: "wrap",
|
|
836
|
+
gap: 8
|
|
837
|
+
},
|
|
838
|
+
children: [
|
|
839
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Metadata, {
|
|
840
|
+
label: t("provider"),
|
|
841
|
+
value: role.provider
|
|
842
|
+
}),
|
|
843
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Metadata, {
|
|
844
|
+
label: t("model"),
|
|
845
|
+
value: role.model
|
|
846
|
+
}),
|
|
847
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Metadata, {
|
|
848
|
+
label: t("reasoningEffort"),
|
|
849
|
+
value: role.reasoningEffort
|
|
850
|
+
}),
|
|
851
|
+
role.persona ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Metadata, {
|
|
852
|
+
label: t("persona"),
|
|
853
|
+
value: role.persona
|
|
854
|
+
}) : null
|
|
855
|
+
]
|
|
856
|
+
}),
|
|
857
|
+
failure !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
858
|
+
style: {
|
|
859
|
+
color: token.danger,
|
|
860
|
+
fontSize: 12
|
|
861
|
+
},
|
|
862
|
+
children: failure
|
|
863
|
+
}) : null,
|
|
864
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
865
|
+
style: {
|
|
866
|
+
display: "flex",
|
|
867
|
+
gap: 8
|
|
868
|
+
},
|
|
869
|
+
children: [
|
|
870
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
871
|
+
style: ghostButtonStyle,
|
|
872
|
+
disabled: busy,
|
|
873
|
+
onClick: () => setEditing(true),
|
|
874
|
+
children: t("save")
|
|
875
|
+
}),
|
|
876
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
877
|
+
style: ghostButtonStyle,
|
|
878
|
+
disabled: busy || isDefault,
|
|
879
|
+
onClick: () => (onSetDefault(), void 0),
|
|
880
|
+
children: t("setDefaultRole")
|
|
881
|
+
}),
|
|
882
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
883
|
+
style: dangerButtonStyle,
|
|
884
|
+
disabled: busy,
|
|
885
|
+
onClick: remove,
|
|
886
|
+
children: t("deleteRole")
|
|
887
|
+
})
|
|
888
|
+
]
|
|
889
|
+
})
|
|
890
|
+
]
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
function Metadata({ label, value }) {
|
|
894
|
+
if (!value) return null;
|
|
895
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
896
|
+
style: {
|
|
897
|
+
color: token.labelSecondary,
|
|
898
|
+
fontSize: 12
|
|
899
|
+
},
|
|
900
|
+
children: [
|
|
901
|
+
label,
|
|
902
|
+
": ",
|
|
903
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
904
|
+
style: { color: token.labelPrimary },
|
|
905
|
+
children: value
|
|
906
|
+
})
|
|
907
|
+
]
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
//#endregion
|
|
911
|
+
//#region src/client/SubagentOptionsSection.tsx
|
|
912
|
+
/**
|
|
913
|
+
* Subagent Director settings section (design section 9): the default-model row
|
|
914
|
+
* plus the role-template cards. The slot outlet erases the share boundary and
|
|
915
|
+
* delivers the inject face flat (PropsRuntime<'settings.section'> renderer is
|
|
916
|
+
* the DSH shell); this component guards for a not-yet-injected render and then
|
|
917
|
+
* renders its content column from the live snapshot.
|
|
918
|
+
*
|
|
919
|
+
* State lives in the page store (SubagentOptionsStore); every write travels as
|
|
920
|
+
* path ops through settings.mutate with an optimistic-revision lock, so the
|
|
921
|
+
* section only ever echoes the acknowledge-and-reload outcome. Failures return
|
|
922
|
+
* a localized message that the save/delete/restore controls surface inline.
|
|
923
|
+
*/
|
|
924
|
+
/** Derive reasoning-effort options from the catalog for an exact provider+model. */
|
|
925
|
+
function effortsFor(groups, provider, model) {
|
|
926
|
+
if (!provider || !model) return [];
|
|
927
|
+
return (groups.find((g) => g.id === provider)?.models.find((m) => m.id === model))?.reasoning?.efforts ?? [];
|
|
928
|
+
}
|
|
929
|
+
/**
|
|
930
|
+
* Render the Subagent Director settings section content column.
|
|
931
|
+
* @param props - slot-delivered injected dependencies.
|
|
932
|
+
* @returns the section, or null while the shell has not injected yet.
|
|
933
|
+
*/
|
|
934
|
+
function SubagentOptionsSection(props) {
|
|
935
|
+
const { controller, useSnapshot, t } = props;
|
|
936
|
+
if (controller === void 0 || useSnapshot === void 0 || t === void 0) return null;
|
|
937
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Loaded, { injected: {
|
|
938
|
+
controller,
|
|
939
|
+
useSnapshot,
|
|
940
|
+
t
|
|
941
|
+
} });
|
|
942
|
+
}
|
|
943
|
+
function Loaded({ injected }) {
|
|
944
|
+
const { controller, t } = injected;
|
|
945
|
+
const state = injected.useSnapshot((s) => s);
|
|
946
|
+
(0, react.useEffect)(() => {
|
|
947
|
+
if (state.status === "idle" && !state.loading) controller.load();
|
|
948
|
+
}, [state.status, state.loading]);
|
|
949
|
+
if (state.status === "error") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
950
|
+
style: sectionWidth,
|
|
951
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
952
|
+
style: {
|
|
953
|
+
color: token.danger,
|
|
954
|
+
fontSize: 13
|
|
955
|
+
},
|
|
956
|
+
children: [
|
|
957
|
+
t("loadError"),
|
|
958
|
+
": ",
|
|
959
|
+
state.error ?? ""
|
|
960
|
+
]
|
|
961
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
962
|
+
style: ghostButtonStyle,
|
|
963
|
+
onClick: () => void controller.load(),
|
|
964
|
+
children: t("save")
|
|
965
|
+
})]
|
|
966
|
+
});
|
|
967
|
+
if (state.status !== "ready") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
968
|
+
style: sectionWidth,
|
|
969
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
970
|
+
style: {
|
|
971
|
+
color: token.labelSecondary,
|
|
972
|
+
fontSize: 13
|
|
973
|
+
},
|
|
974
|
+
children: t("sectionIntro")
|
|
975
|
+
})
|
|
976
|
+
});
|
|
977
|
+
const section = state.section;
|
|
978
|
+
const writable = state.writable;
|
|
979
|
+
const roles = section?.roles ?? {};
|
|
980
|
+
const entries = Object.entries(roles);
|
|
981
|
+
const groups = state.models;
|
|
982
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
983
|
+
style: sectionWidth,
|
|
984
|
+
children: [
|
|
985
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
986
|
+
style: {
|
|
987
|
+
margin: 0,
|
|
988
|
+
color: token.labelSecondary,
|
|
989
|
+
fontSize: 13,
|
|
990
|
+
lineHeight: "18px"
|
|
991
|
+
},
|
|
992
|
+
children: t("sectionIntro")
|
|
993
|
+
}),
|
|
994
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DefaultModelRow, {
|
|
995
|
+
controller,
|
|
996
|
+
groups,
|
|
997
|
+
writable,
|
|
998
|
+
current: {
|
|
999
|
+
provider: section?.defaultProvider,
|
|
1000
|
+
model: section?.defaultModel,
|
|
1001
|
+
reasoningEffort: section?.defaultReasoningEffort
|
|
1002
|
+
},
|
|
1003
|
+
t
|
|
1004
|
+
}),
|
|
1005
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(RolesBlock, {
|
|
1006
|
+
controller,
|
|
1007
|
+
groups,
|
|
1008
|
+
writable,
|
|
1009
|
+
roles: entries,
|
|
1010
|
+
defaultRole: section?.defaultRole,
|
|
1011
|
+
t
|
|
1012
|
+
})
|
|
1013
|
+
]
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
/** The default-model row: provider → model → reasoning-effort cascade + restore. */
|
|
1017
|
+
function DefaultModelRow({ controller, groups, writable, current, t }) {
|
|
1018
|
+
const [draft, setDraft] = (0, react.useState)({
|
|
1019
|
+
provider: current.provider ?? "",
|
|
1020
|
+
model: current.model ?? "",
|
|
1021
|
+
reasoningEffort: current.reasoningEffort ?? ""
|
|
1022
|
+
});
|
|
1023
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
1024
|
+
const [failure, setFailure] = (0, react.useState)(void 0);
|
|
1025
|
+
const [done, setDone] = (0, react.useState)(false);
|
|
1026
|
+
(0, react.useEffect)(() => {
|
|
1027
|
+
if (busy) return;
|
|
1028
|
+
setDraft({
|
|
1029
|
+
provider: current.provider ?? "",
|
|
1030
|
+
model: current.model ?? "",
|
|
1031
|
+
reasoningEffort: current.reasoningEffort ?? ""
|
|
1032
|
+
});
|
|
1033
|
+
}, [
|
|
1034
|
+
current.provider,
|
|
1035
|
+
current.model,
|
|
1036
|
+
current.reasoningEffort
|
|
1037
|
+
]);
|
|
1038
|
+
const provider = draft.provider;
|
|
1039
|
+
const model = draft.model;
|
|
1040
|
+
const modelOptions = provider ? groups.find((g) => g.id === provider)?.models ?? [] : [];
|
|
1041
|
+
const effortOptions = effortsFor(groups, provider, model);
|
|
1042
|
+
const save = async () => {
|
|
1043
|
+
setBusy(true);
|
|
1044
|
+
setFailure(void 0);
|
|
1045
|
+
try {
|
|
1046
|
+
const message = await controller.setDefaultModel({
|
|
1047
|
+
provider: draft.provider || void 0,
|
|
1048
|
+
model: draft.model || void 0,
|
|
1049
|
+
reasoningEffort: draft.reasoningEffort || void 0
|
|
1050
|
+
});
|
|
1051
|
+
if (message !== void 0) {
|
|
1052
|
+
setFailure(message);
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
setDone(true);
|
|
1056
|
+
} finally {
|
|
1057
|
+
setBusy(false);
|
|
1058
|
+
}
|
|
1059
|
+
};
|
|
1060
|
+
const restore = async () => {
|
|
1061
|
+
setBusy(true);
|
|
1062
|
+
setFailure(void 0);
|
|
1063
|
+
try {
|
|
1064
|
+
const message = await controller.restoreDefaults();
|
|
1065
|
+
if (message !== void 0) {
|
|
1066
|
+
setFailure(message);
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
setDone(true);
|
|
1070
|
+
} finally {
|
|
1071
|
+
setBusy(false);
|
|
1072
|
+
}
|
|
1073
|
+
};
|
|
1074
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1075
|
+
style: cardStyle,
|
|
1076
|
+
children: [
|
|
1077
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1078
|
+
style: {
|
|
1079
|
+
display: "flex",
|
|
1080
|
+
alignItems: "baseline",
|
|
1081
|
+
justifyContent: "space-between",
|
|
1082
|
+
gap: 8
|
|
1083
|
+
},
|
|
1084
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", {
|
|
1085
|
+
style: {
|
|
1086
|
+
color: token.labelPrimary,
|
|
1087
|
+
fontSize: 14
|
|
1088
|
+
},
|
|
1089
|
+
children: t("defaultsHeading")
|
|
1090
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1091
|
+
style: ghostButtonStyle,
|
|
1092
|
+
disabled: !writable || busy,
|
|
1093
|
+
onClick: () => void restore(),
|
|
1094
|
+
children: t("restoreDefaults")
|
|
1095
|
+
})]
|
|
1096
|
+
}),
|
|
1097
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1098
|
+
style: {
|
|
1099
|
+
margin: 0,
|
|
1100
|
+
color: token.labelSecondary,
|
|
1101
|
+
fontSize: 13,
|
|
1102
|
+
lineHeight: "18px"
|
|
1103
|
+
},
|
|
1104
|
+
children: t("defaultsHint")
|
|
1105
|
+
}),
|
|
1106
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1107
|
+
style: {
|
|
1108
|
+
display: "grid",
|
|
1109
|
+
gridTemplateColumns: "repeat(3, minmax(0, 1fr))",
|
|
1110
|
+
gap: 8
|
|
1111
|
+
},
|
|
1112
|
+
children: [
|
|
1113
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1114
|
+
style: rowStyle,
|
|
1115
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
1116
|
+
style: fieldLabelStyle,
|
|
1117
|
+
children: t("defaultProvider")
|
|
1118
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
1119
|
+
style: selectStyle,
|
|
1120
|
+
value: draft.provider,
|
|
1121
|
+
disabled: !writable || groups.length === 0,
|
|
1122
|
+
onChange: (e) => setDraft((d) => ({
|
|
1123
|
+
provider: e.target.value,
|
|
1124
|
+
model: "",
|
|
1125
|
+
reasoningEffort: ""
|
|
1126
|
+
})),
|
|
1127
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
1128
|
+
value: "",
|
|
1129
|
+
children: "—"
|
|
1130
|
+
}), groups.map((g) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
1131
|
+
value: g.id,
|
|
1132
|
+
children: g.name
|
|
1133
|
+
}, g.id))]
|
|
1134
|
+
})]
|
|
1135
|
+
}),
|
|
1136
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1137
|
+
style: rowStyle,
|
|
1138
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
1139
|
+
style: fieldLabelStyle,
|
|
1140
|
+
children: t("defaultModel")
|
|
1141
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
1142
|
+
style: selectStyle,
|
|
1143
|
+
value: draft.model,
|
|
1144
|
+
disabled: !writable || modelOptions.length === 0,
|
|
1145
|
+
onChange: (e) => setDraft((d) => ({
|
|
1146
|
+
...d,
|
|
1147
|
+
model: e.target.value,
|
|
1148
|
+
reasoningEffort: ""
|
|
1149
|
+
})),
|
|
1150
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
1151
|
+
value: "",
|
|
1152
|
+
children: "—"
|
|
1153
|
+
}), modelOptions.map((m) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
1154
|
+
value: m.id,
|
|
1155
|
+
children: m.name
|
|
1156
|
+
}, m.id))]
|
|
1157
|
+
})]
|
|
1158
|
+
}),
|
|
1159
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1160
|
+
style: rowStyle,
|
|
1161
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
1162
|
+
style: fieldLabelStyle,
|
|
1163
|
+
children: t("defaultReasoningEffort")
|
|
1164
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
1165
|
+
style: selectStyle,
|
|
1166
|
+
value: draft.reasoningEffort,
|
|
1167
|
+
disabled: !writable || effortOptions.length === 0,
|
|
1168
|
+
onChange: (e) => setDraft((d) => ({
|
|
1169
|
+
...d,
|
|
1170
|
+
reasoningEffort: e.target.value
|
|
1171
|
+
})),
|
|
1172
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
1173
|
+
value: "",
|
|
1174
|
+
children: "—"
|
|
1175
|
+
}), effortOptions.map((e) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
1176
|
+
value: e.id,
|
|
1177
|
+
children: e.name
|
|
1178
|
+
}, e.id))]
|
|
1179
|
+
})]
|
|
1180
|
+
})
|
|
1181
|
+
]
|
|
1182
|
+
}),
|
|
1183
|
+
failure !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1184
|
+
style: {
|
|
1185
|
+
color: token.danger,
|
|
1186
|
+
fontSize: 12
|
|
1187
|
+
},
|
|
1188
|
+
children: failure
|
|
1189
|
+
}) : null,
|
|
1190
|
+
done ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1191
|
+
style: {
|
|
1192
|
+
color: token.accent,
|
|
1193
|
+
fontSize: 12
|
|
1194
|
+
},
|
|
1195
|
+
children: t("restoreDone")
|
|
1196
|
+
}) : null,
|
|
1197
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1198
|
+
style: {
|
|
1199
|
+
display: "flex",
|
|
1200
|
+
gap: 8
|
|
1201
|
+
},
|
|
1202
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1203
|
+
style: primaryButtonStyle,
|
|
1204
|
+
disabled: !writable || busy,
|
|
1205
|
+
onClick: () => void save(),
|
|
1206
|
+
children: t("save")
|
|
1207
|
+
})
|
|
1208
|
+
})
|
|
1209
|
+
]
|
|
1210
|
+
});
|
|
1211
|
+
}
|
|
1212
|
+
/** The role-template roster: cards plus an inline add form. */
|
|
1213
|
+
function RolesBlock({ controller, groups, writable, roles, defaultRole, t }) {
|
|
1214
|
+
const [adding, setAdding] = (0, react.useState)(false);
|
|
1215
|
+
const [draft, setDraft] = (0, react.useState)({
|
|
1216
|
+
displayName: "",
|
|
1217
|
+
description: "",
|
|
1218
|
+
persona: "",
|
|
1219
|
+
provider: "",
|
|
1220
|
+
model: "",
|
|
1221
|
+
reasoningEffort: ""
|
|
1222
|
+
});
|
|
1223
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
1224
|
+
const [failure, setFailure] = (0, react.useState)(void 0);
|
|
1225
|
+
const beginAdd = () => {
|
|
1226
|
+
setDraft({
|
|
1227
|
+
displayName: "",
|
|
1228
|
+
description: "",
|
|
1229
|
+
persona: "",
|
|
1230
|
+
provider: "",
|
|
1231
|
+
model: "",
|
|
1232
|
+
reasoningEffort: ""
|
|
1233
|
+
});
|
|
1234
|
+
setFailure(void 0);
|
|
1235
|
+
setAdding(true);
|
|
1236
|
+
};
|
|
1237
|
+
const saveAdd = async () => {
|
|
1238
|
+
setBusy(true);
|
|
1239
|
+
setFailure(void 0);
|
|
1240
|
+
try {
|
|
1241
|
+
const existing = new Set(roles.map(([id]) => id));
|
|
1242
|
+
const id = roleIdFromName(draft.displayName, existing);
|
|
1243
|
+
const message = await controller.addRole(id, draft);
|
|
1244
|
+
if (message !== void 0) {
|
|
1245
|
+
setFailure(message);
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
1248
|
+
setAdding(false);
|
|
1249
|
+
} finally {
|
|
1250
|
+
setBusy(false);
|
|
1251
|
+
}
|
|
1252
|
+
};
|
|
1253
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1254
|
+
style: cardStyle,
|
|
1255
|
+
children: [
|
|
1256
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1257
|
+
style: {
|
|
1258
|
+
display: "flex",
|
|
1259
|
+
alignItems: "baseline",
|
|
1260
|
+
justifyContent: "space-between",
|
|
1261
|
+
gap: 8
|
|
1262
|
+
},
|
|
1263
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", {
|
|
1264
|
+
style: {
|
|
1265
|
+
color: token.labelPrimary,
|
|
1266
|
+
fontSize: 14
|
|
1267
|
+
},
|
|
1268
|
+
children: t("rolesHeading")
|
|
1269
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1270
|
+
style: ghostButtonStyle,
|
|
1271
|
+
disabled: !writable || adding,
|
|
1272
|
+
onClick: beginAdd,
|
|
1273
|
+
children: t("addRole")
|
|
1274
|
+
})]
|
|
1275
|
+
}),
|
|
1276
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1277
|
+
style: {
|
|
1278
|
+
margin: 0,
|
|
1279
|
+
color: token.labelSecondary,
|
|
1280
|
+
fontSize: 13,
|
|
1281
|
+
lineHeight: "18px"
|
|
1282
|
+
},
|
|
1283
|
+
children: t("rolesHint")
|
|
1284
|
+
}),
|
|
1285
|
+
adding ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1286
|
+
style: {
|
|
1287
|
+
border: "1px dashed " + token.border,
|
|
1288
|
+
borderRadius: 10,
|
|
1289
|
+
padding: 12,
|
|
1290
|
+
display: "flex",
|
|
1291
|
+
flexDirection: "column",
|
|
1292
|
+
gap: 10
|
|
1293
|
+
},
|
|
1294
|
+
children: [
|
|
1295
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1296
|
+
style: rowStyle,
|
|
1297
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
1298
|
+
style: fieldLabelStyle,
|
|
1299
|
+
children: t("roleDisplayName")
|
|
1300
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1301
|
+
style: textInputStyle,
|
|
1302
|
+
value: draft.displayName,
|
|
1303
|
+
placeholder: t("displayNamePlaceholder"),
|
|
1304
|
+
onChange: (e) => setDraft((d) => ({
|
|
1305
|
+
...d,
|
|
1306
|
+
displayName: e.target.value
|
|
1307
|
+
}))
|
|
1308
|
+
})]
|
|
1309
|
+
}),
|
|
1310
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1311
|
+
style: rowStyle,
|
|
1312
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
1313
|
+
style: fieldLabelStyle,
|
|
1314
|
+
children: t("roleDescription")
|
|
1315
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
|
|
1316
|
+
style: textAreaStyle,
|
|
1317
|
+
value: draft.description,
|
|
1318
|
+
placeholder: t("descriptionPlaceholder"),
|
|
1319
|
+
onChange: (e) => setDraft((d) => ({
|
|
1320
|
+
...d,
|
|
1321
|
+
description: e.target.value
|
|
1322
|
+
}))
|
|
1323
|
+
})]
|
|
1324
|
+
}),
|
|
1325
|
+
failure !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1326
|
+
style: {
|
|
1327
|
+
color: token.danger,
|
|
1328
|
+
fontSize: 12
|
|
1329
|
+
},
|
|
1330
|
+
children: failure
|
|
1331
|
+
}) : null,
|
|
1332
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1333
|
+
style: {
|
|
1334
|
+
display: "flex",
|
|
1335
|
+
gap: 8
|
|
1336
|
+
},
|
|
1337
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1338
|
+
style: primaryButtonStyle,
|
|
1339
|
+
disabled: !writable || busy,
|
|
1340
|
+
onClick: () => void saveAdd(),
|
|
1341
|
+
children: t("addRole")
|
|
1342
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1343
|
+
style: ghostButtonStyle,
|
|
1344
|
+
disabled: busy,
|
|
1345
|
+
onClick: () => setAdding(false),
|
|
1346
|
+
children: t("cancel")
|
|
1347
|
+
})]
|
|
1348
|
+
})
|
|
1349
|
+
]
|
|
1350
|
+
}) : null,
|
|
1351
|
+
roles.length === 0 && !adding ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1352
|
+
style: {
|
|
1353
|
+
margin: 0,
|
|
1354
|
+
color: token.labelSecondary,
|
|
1355
|
+
fontSize: 13
|
|
1356
|
+
},
|
|
1357
|
+
children: t("emptyRoles")
|
|
1358
|
+
}) : roles.map(([id, role]) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RoleCard, {
|
|
1359
|
+
id,
|
|
1360
|
+
role,
|
|
1361
|
+
isDefault: defaultRole === id,
|
|
1362
|
+
groups,
|
|
1363
|
+
t,
|
|
1364
|
+
onSave: (d) => controller.updateRole(id, role, d),
|
|
1365
|
+
onDelete: () => controller.removeRole(id),
|
|
1366
|
+
onSetDefault: () => controller.setDefaultRole(id)
|
|
1367
|
+
}, id))
|
|
1368
|
+
]
|
|
1369
|
+
});
|
|
1370
|
+
}
|
|
1371
|
+
//#endregion
|
|
1372
|
+
//#region src/client/subagent-model.ts
|
|
1373
|
+
/**
|
|
1374
|
+
* Prefer the latest assistant message's reported provenance; fall back to its
|
|
1375
|
+
* requested config. Order walks the snapshot's node list from the tail so we
|
|
1376
|
+
* surface the most recent completed request, which is the meaningful one when
|
|
1377
|
+
* a subagent retried or only partially ran.
|
|
1378
|
+
*/
|
|
1379
|
+
function latestSubagentModel(snapshot) {
|
|
1380
|
+
if (snapshot === null || typeof snapshot !== "object") return { found: false };
|
|
1381
|
+
const nodes = snapshot.nodes;
|
|
1382
|
+
if (!Array.isArray(nodes)) return { found: false };
|
|
1383
|
+
for (let i = nodes.length - 1; i >= 0; i -= 1) {
|
|
1384
|
+
const node = nodes[i];
|
|
1385
|
+
if (node === null || typeof node !== "object") continue;
|
|
1386
|
+
if (node.kind !== "assistant") continue;
|
|
1387
|
+
const ref = provenanceOf(node);
|
|
1388
|
+
if (ref !== null) return ref;
|
|
1389
|
+
}
|
|
1390
|
+
return { found: false };
|
|
1391
|
+
}
|
|
1392
|
+
/** Resolve the model identity off one assistant message (provenance first). */
|
|
1393
|
+
function provenanceOf(assistant) {
|
|
1394
|
+
const reported = assistant.provenance;
|
|
1395
|
+
if (reported !== null && typeof reported === "object" && typeof reported.provider === "string" && reported.provider !== "" && typeof reported.model === "string" && reported.model !== "") return {
|
|
1396
|
+
found: true,
|
|
1397
|
+
provider: reported.provider,
|
|
1398
|
+
model: reported.model
|
|
1399
|
+
};
|
|
1400
|
+
const requested = assistant.requestConfig;
|
|
1401
|
+
if (requested !== null && typeof requested === "object" && typeof requested.provider === "string" && requested.provider !== "" && typeof requested.model === "string" && requested.model !== "") return {
|
|
1402
|
+
found: true,
|
|
1403
|
+
provider: requested.provider,
|
|
1404
|
+
model: requested.model
|
|
1405
|
+
};
|
|
1406
|
+
return null;
|
|
1407
|
+
}
|
|
1408
|
+
/**
|
|
1409
|
+
* Whether this conversation is an addressed subagent (a catalog-discovered
|
|
1410
|
+
* child) — the surface where the official read-only composer shows and where
|
|
1411
|
+
* a model readout is most useful. A null `subagent` on an ordinary session
|
|
1412
|
+
* returns false even when nodes carry provenance.
|
|
1413
|
+
*/
|
|
1414
|
+
function isAddressedSubagent(snapshot) {
|
|
1415
|
+
if (snapshot === null || snapshot === void 0) return false;
|
|
1416
|
+
const subagent = snapshot.subagent;
|
|
1417
|
+
return subagent !== null && subagent !== void 0;
|
|
1418
|
+
}
|
|
1419
|
+
/**
|
|
1420
|
+
* Compact provider/model label, e.g. "deepseek/deepseek-v4-flash". The model
|
|
1421
|
+
* id can already include a provider prefix; we do not hyphenate or re-quote
|
|
1422
|
+
* so the exact route stays readable in one line.
|
|
1423
|
+
*/
|
|
1424
|
+
function formatModelRef(ref) {
|
|
1425
|
+
return `${ref.provider}/${ref.model}`;
|
|
1426
|
+
}
|
|
1427
|
+
//#endregion
|
|
1428
|
+
//#region src/client/SubagentModelDock.tsx
|
|
1429
|
+
/** Inline styling using the shared token surface (no CSS pipeline; M2 deviation). */
|
|
1430
|
+
const style = {
|
|
1431
|
+
root: {
|
|
1432
|
+
display: "flex",
|
|
1433
|
+
alignItems: "center",
|
|
1434
|
+
gap: 6,
|
|
1435
|
+
padding: "3px 16px",
|
|
1436
|
+
fontSize: 12,
|
|
1437
|
+
lineHeight: "16px",
|
|
1438
|
+
color: "var(--dsw-alias-label-tertiary)"
|
|
1439
|
+
},
|
|
1440
|
+
ref: {
|
|
1441
|
+
color: "var(--dsw-alias-label-secondary)",
|
|
1442
|
+
fontFamily: "var(--dsw-font-family-mono, monospace)"
|
|
1443
|
+
}
|
|
1444
|
+
};
|
|
1445
|
+
/** Render the provider/model readout for an addressed subagent, or nothing. */
|
|
1446
|
+
function SubagentModelDock({ session, t }) {
|
|
1447
|
+
if (!isAddressedSubagent(session)) return null;
|
|
1448
|
+
const lookup = latestSubagentModel(session);
|
|
1449
|
+
if (!lookup.found) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1450
|
+
style: style.root,
|
|
1451
|
+
role: "status",
|
|
1452
|
+
children: t("modelNotRecorded")
|
|
1453
|
+
});
|
|
1454
|
+
const ref = lookup;
|
|
1455
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1456
|
+
style: style.root,
|
|
1457
|
+
role: "status",
|
|
1458
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("modelRanOn") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1459
|
+
style: style.ref,
|
|
1460
|
+
title: t("modelRanOnTitle", { model: formatModelRef(ref) }),
|
|
1461
|
+
children: formatModelRef(ref)
|
|
1462
|
+
})]
|
|
1463
|
+
});
|
|
1464
|
+
}
|
|
1465
|
+
//#endregion
|
|
1466
|
+
//#region src/client/index.ts
|
|
1467
|
+
/** Dictionary namespace owned by Subagent Director (bilingual, typed). */
|
|
1468
|
+
const NS = "settings.subagentDirector";
|
|
1469
|
+
/** Refetch the page snapshot only after its first load. */
|
|
1470
|
+
function refreshIfLoaded(controller) {
|
|
1471
|
+
if (controller.store.getSnapshot().status === "idle") return;
|
|
1472
|
+
controller.load();
|
|
1473
|
+
}
|
|
1474
|
+
/** Services required by the settings registration (cordis fiber inject). */
|
|
1475
|
+
const inject = [
|
|
1476
|
+
"slots",
|
|
1477
|
+
"locale",
|
|
1478
|
+
"connection",
|
|
1479
|
+
"remote"
|
|
1480
|
+
];
|
|
1481
|
+
/**
|
|
1482
|
+
* Register the Subagent Director section once the `settings.section`
|
|
1483
|
+
* declaration is on the ledger, wire its store to the connection, and keep it
|
|
1484
|
+
* fresh on every pushed invalidation (settings or provider topology).
|
|
1485
|
+
*/
|
|
1486
|
+
function apply(ctx) {
|
|
1487
|
+
ctx.effect(() => ctx.locale.register(NS, {
|
|
1488
|
+
zh,
|
|
1489
|
+
en
|
|
1490
|
+
}), "subagent-director: copy dictionaries");
|
|
1491
|
+
const connection = ctx.get("connection");
|
|
1492
|
+
const t = ctx.locale.bind(NS);
|
|
1493
|
+
const controller = new SubagentOptionsStore({
|
|
1494
|
+
rpc: connection.rpc,
|
|
1495
|
+
llm: connection.api.llm,
|
|
1496
|
+
t
|
|
1497
|
+
});
|
|
1498
|
+
const useSnapshot = (0, _deepseek_ai_dsh_client_web_react.bindSnapshotSelector)(controller.store);
|
|
1499
|
+
const injected = () => ({
|
|
1500
|
+
controller,
|
|
1501
|
+
useSnapshot,
|
|
1502
|
+
api: connection.api,
|
|
1503
|
+
t
|
|
1504
|
+
});
|
|
1505
|
+
ctx.effect(() => {
|
|
1506
|
+
const refresh = () => refreshIfLoaded(controller);
|
|
1507
|
+
const disposers = [
|
|
1508
|
+
ctx.remote.$on("settings/document-updated", refresh),
|
|
1509
|
+
ctx.remote.$on("llm/adapters-updated", refresh),
|
|
1510
|
+
ctx.on("connection/reset", refresh)
|
|
1511
|
+
];
|
|
1512
|
+
return () => {
|
|
1513
|
+
for (const dispose of disposers) dispose();
|
|
1514
|
+
};
|
|
1515
|
+
}, "subagent-director: pushed invalidations");
|
|
1516
|
+
ctx.slots.inject("settings.section", () => ctx.slots.register({
|
|
1517
|
+
name: "settings.section",
|
|
1518
|
+
id: "subagent-director",
|
|
1519
|
+
order: 20,
|
|
1520
|
+
label: () => t("nav"),
|
|
1521
|
+
locale: NS,
|
|
1522
|
+
inject: injected
|
|
1523
|
+
}, SubagentOptionsSection));
|
|
1524
|
+
ctx.slots.inject("conversation.composer.dock", () => ctx.slots.register({
|
|
1525
|
+
name: "conversation.composer.dock",
|
|
1526
|
+
id: "subagent-director-model",
|
|
1527
|
+
order: 90,
|
|
1528
|
+
locale: NS
|
|
1529
|
+
}, SubagentModelDock));
|
|
1530
|
+
}
|
|
1531
|
+
//#endregion
|
|
1532
|
+
exports.NS = NS;
|
|
1533
|
+
exports.apply = apply;
|
|
1534
|
+
exports.en = en;
|
|
1535
|
+
exports.inject = inject;
|
|
1536
|
+
exports.refreshIfLoaded = refreshIfLoaded;
|
|
1537
|
+
exports.zh = zh;
|
|
1538
|
+
|
|
1539
|
+
return module.exports;
|
|
1540
|
+
}
|
|
1541
|
+
});
|