@morlay/dsh-session-mode 0.0.8 → 0.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -11
- package/dist/catalog-DWxIg3qw.mjs +131 -0
- package/dist/catalog-K5cESbqA.cjs +136 -0
- package/dist/catalog-remote-B7ZgYqun.cjs +28 -0
- package/dist/{catalog-remote-B-NMi_vS.mjs → catalog-remote-KXJjDaN6.mjs} +1 -5
- package/dist/client.cjs +1253 -362
- package/dist/client.d.cts +3 -326
- package/dist/client.d.mts +3 -326
- package/dist/client.mjs +1612 -0
- package/dist/index.cjs +613 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +45 -169
- package/dist/rows.cjs +85 -0
- package/dist/rows.mjs +2 -5
- package/dist/shared-BusmswG5.cjs +66 -0
- package/dist/shared-DX2Giyh3.mjs +5 -0
- package/dist/testing.cjs +6 -0
- package/dist/testing.d.mts +35 -0
- package/dist/testing.mjs +3 -0
- package/package.json +71 -59
- package/src/client/BundleConfigPage.tsx +12 -11
- package/src/client/SessionModeSeat.tsx +1 -1
- package/src/client/bundle-config.ts +1 -1
- package/src/client/bundle-locales.ts +4 -2
- package/src/client/index.ts +8 -12
- package/src/client/llm-directory.ts +7 -4
- package/src/index.ts +4 -1
- package/src/mode-sources.ts +11 -8
- package/src/modes.ts +6 -5
- package/src/scope.ts +74 -65
- package/src/testing.ts +3 -0
- package/dist/index.d.cts +0 -146
- package/dist/rows.d.cts +0 -11
package/dist/client.mjs
ADDED
|
@@ -0,0 +1,1612 @@
|
|
|
1
|
+
import { createSnapshotStore } from "@deepseek-ai/dsh-client-store";
|
|
2
|
+
import { Button, DisclosureRow, IconAgentPresetOutlineRegular, IconButton, IconChevronDownOutlineRegular, IconPlusOutlineRegular, IconTrashOutlineRegular, Input, Menu, Modal, ModelRouteList, MultilineField, Row, SchemaFormController, SearchSelect, SeatButton, SegmentedControl, SettingsFieldRow, SettingsForm, Stack, Switch, TagInput, Text, failureOf, projectRoot } from "@morlay/dsh-client-ui-primitives";
|
|
3
|
+
import { useEffect, useId, useMemo, useState } from "react";
|
|
4
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
//#region src/catalog-remote.ts
|
|
6
|
+
const CATALOG_NS = "sessionModeCatalog";
|
|
7
|
+
const SESSION_MODE_CATALOG_REMOTE = {
|
|
8
|
+
package: "@morlay/dsh-session-mode",
|
|
9
|
+
descriptors: [{
|
|
10
|
+
id: "@morlay/dsh-session-mode#sessionModeCatalog/list",
|
|
11
|
+
service: CATALOG_NS,
|
|
12
|
+
namespace: CATALOG_NS,
|
|
13
|
+
method: "list",
|
|
14
|
+
implementation: "remoteExportList",
|
|
15
|
+
invocation: { kind: "direct" },
|
|
16
|
+
parameters: [],
|
|
17
|
+
result: { mode: "src-json" }
|
|
18
|
+
}]
|
|
19
|
+
};
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region src/shared.ts
|
|
22
|
+
const SESSION_MODE_PATH = "/session-mode";
|
|
23
|
+
const POLICY_NAMES = ["fs/write-intent", "fs/edit-intent"];
|
|
24
|
+
//#endregion
|
|
25
|
+
//#region src/client/llm-directory.ts
|
|
26
|
+
function routeKey(provider, model) {
|
|
27
|
+
return `${provider}\u0000${model}`;
|
|
28
|
+
}
|
|
29
|
+
function remoteNamespace(scope, namespace) {
|
|
30
|
+
try {
|
|
31
|
+
return scope.get(`remote.${namespace}`);
|
|
32
|
+
} catch {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function readAt(root, path) {
|
|
37
|
+
return path.reduce((node, segment) => {
|
|
38
|
+
if (Array.isArray(node)) return node[Number(segment)];
|
|
39
|
+
if (typeof node !== "object" || node === null) return void 0;
|
|
40
|
+
return Reflect.get(node, segment);
|
|
41
|
+
}, root);
|
|
42
|
+
}
|
|
43
|
+
function modelsOf(entry, forms) {
|
|
44
|
+
if (entry.settingsNs === "") return [];
|
|
45
|
+
const profile = readAt(forms.get(entry.settingsNs).getSnapshot().value, entry.settingsPath);
|
|
46
|
+
const models = typeof profile === "object" && profile !== null ? Reflect.get(profile, "models") : void 0;
|
|
47
|
+
if (!Array.isArray(models)) return [];
|
|
48
|
+
return models.flatMap((model) => {
|
|
49
|
+
if (typeof model !== "object" || model === null) return [];
|
|
50
|
+
const id = Reflect.get(model, "id");
|
|
51
|
+
if (typeof id !== "string") return [];
|
|
52
|
+
const name = Reflect.get(model, "name");
|
|
53
|
+
return [{
|
|
54
|
+
value: id,
|
|
55
|
+
label: typeof name === "string" && name !== "" ? name : id
|
|
56
|
+
}];
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
async function loadLlmProviders(scope) {
|
|
60
|
+
const llm = remoteNamespace(scope, "llm");
|
|
61
|
+
const forms = scope.get("configForms");
|
|
62
|
+
if (llm === void 0 || forms === void 0) return [];
|
|
63
|
+
const [routes, directory] = await Promise.all([llm.listProviders(), llm.listConfigurableProviders()]);
|
|
64
|
+
if (!routes.ok || !directory.ok) return [];
|
|
65
|
+
const merged = /* @__PURE__ */ new Map();
|
|
66
|
+
for (const entry of directory.value) merged.set(entry.provider, {
|
|
67
|
+
value: entry.provider,
|
|
68
|
+
label: entry.displayName,
|
|
69
|
+
settingsNs: entry.settingsNs,
|
|
70
|
+
settingsPath: [...entry.settingsPath]
|
|
71
|
+
});
|
|
72
|
+
for (const route of routes.value) if (!merged.has(route.id)) merged.set(route.id, {
|
|
73
|
+
value: route.id,
|
|
74
|
+
label: route.name,
|
|
75
|
+
settingsNs: "",
|
|
76
|
+
settingsPath: []
|
|
77
|
+
});
|
|
78
|
+
return [...merged.values()].map((entry) => ({
|
|
79
|
+
value: entry.value,
|
|
80
|
+
label: entry.label,
|
|
81
|
+
models: modelsOf(entry, forms)
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
function routesOf(providers) {
|
|
85
|
+
return providers.flatMap((provider) => provider.models.map((model) => ({
|
|
86
|
+
key: routeKey(provider.value, model.value),
|
|
87
|
+
provider: provider.value,
|
|
88
|
+
providerName: provider.label,
|
|
89
|
+
model: model.value,
|
|
90
|
+
modelName: model.label
|
|
91
|
+
})));
|
|
92
|
+
}
|
|
93
|
+
//#endregion
|
|
94
|
+
//#region src/client/bundle-config.ts
|
|
95
|
+
const SESSION_MODE_NS = "session-mode";
|
|
96
|
+
const BUNDLE_CONFIG_KEY = "@morlay/session-mode-profile";
|
|
97
|
+
const PROTECTED_MODE_IDS = ["noop"];
|
|
98
|
+
const ROLE_VALUES = ["main", "subagent"];
|
|
99
|
+
const GROUPS = [
|
|
100
|
+
{
|
|
101
|
+
key: "identity",
|
|
102
|
+
labelKey: "group.identity"
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
key: "persona",
|
|
106
|
+
labelKey: "group.persona"
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
key: "tools",
|
|
110
|
+
labelKey: "group.tools"
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
key: "rules",
|
|
114
|
+
labelKey: "group.rules"
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
key: "injections",
|
|
118
|
+
labelKey: "group.injections"
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
key: "model",
|
|
122
|
+
labelKey: "group.model"
|
|
123
|
+
}
|
|
124
|
+
];
|
|
125
|
+
const MODE_FIELDS = [
|
|
126
|
+
{
|
|
127
|
+
path: ["name"],
|
|
128
|
+
control: "text",
|
|
129
|
+
labelKey: "field.name",
|
|
130
|
+
hintKey: "hint.name",
|
|
131
|
+
group: "identity"
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
path: ["description"],
|
|
135
|
+
control: "text",
|
|
136
|
+
labelKey: "field.description",
|
|
137
|
+
hintKey: "hint.description",
|
|
138
|
+
group: "identity"
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
path: ["role"],
|
|
142
|
+
control: "roles",
|
|
143
|
+
labelKey: "field.role",
|
|
144
|
+
hintKey: "hint.role",
|
|
145
|
+
group: "identity"
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
path: ["presetsOnly"],
|
|
149
|
+
control: "tags",
|
|
150
|
+
labelKey: "field.presetsOnly",
|
|
151
|
+
hintKey: "hint.presetsOnly",
|
|
152
|
+
group: "identity"
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
path: ["persona", "prefix"],
|
|
156
|
+
control: "multiline",
|
|
157
|
+
labelKey: "field.prefix",
|
|
158
|
+
hintKey: "hint.prefix",
|
|
159
|
+
group: "persona"
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
path: ["persona", "suffix"],
|
|
163
|
+
control: "multiline",
|
|
164
|
+
labelKey: "field.suffix",
|
|
165
|
+
hintKey: "hint.suffix",
|
|
166
|
+
group: "persona"
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
path: ["allowTools"],
|
|
170
|
+
control: "tags",
|
|
171
|
+
labelKey: "field.allowTools",
|
|
172
|
+
hintKey: "hint.allowTools",
|
|
173
|
+
group: "tools"
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
path: ["denyTools"],
|
|
177
|
+
control: "tags",
|
|
178
|
+
labelKey: "field.denyTools",
|
|
179
|
+
hintKey: "hint.denyTools",
|
|
180
|
+
group: "tools"
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
path: ["allowSkills"],
|
|
184
|
+
control: "tags",
|
|
185
|
+
labelKey: "field.allowSkills",
|
|
186
|
+
hintKey: "hint.allowSkills",
|
|
187
|
+
group: "tools"
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
path: ["denySkills"],
|
|
191
|
+
control: "tags",
|
|
192
|
+
labelKey: "field.denySkills",
|
|
193
|
+
hintKey: "hint.denySkills",
|
|
194
|
+
group: "tools"
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
path: ["allowPolicies"],
|
|
198
|
+
control: "tags",
|
|
199
|
+
labelKey: "field.allowPolicies",
|
|
200
|
+
hintKey: "hint.allowPolicies",
|
|
201
|
+
group: "rules"
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
path: ["denyPolicies"],
|
|
205
|
+
control: "tags",
|
|
206
|
+
labelKey: "field.denyPolicies",
|
|
207
|
+
hintKey: "hint.denyPolicies",
|
|
208
|
+
group: "rules"
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
path: ["instructions"],
|
|
212
|
+
control: "switch",
|
|
213
|
+
labelKey: "field.instructions",
|
|
214
|
+
hintKey: "hint.instructions",
|
|
215
|
+
group: "injections"
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
path: ["skills"],
|
|
219
|
+
control: "tri",
|
|
220
|
+
labelKey: "field.skills",
|
|
221
|
+
hintKey: "hint.skills",
|
|
222
|
+
group: "injections"
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
path: ["runtimeContext"],
|
|
226
|
+
control: "switch",
|
|
227
|
+
labelKey: "field.runtimeContext",
|
|
228
|
+
hintKey: "hint.runtimeContext",
|
|
229
|
+
group: "injections"
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
path: ["defaultModel", "reasoningEffort"],
|
|
233
|
+
control: "text",
|
|
234
|
+
labelKey: "field.reasoningEffort",
|
|
235
|
+
hintKey: "hint.reasoningEffort",
|
|
236
|
+
group: "model"
|
|
237
|
+
}
|
|
238
|
+
];
|
|
239
|
+
function unwrapService(value) {
|
|
240
|
+
if (value === void 0) return void 0;
|
|
241
|
+
return value[Symbol.for("cordis.original")] ?? value;
|
|
242
|
+
}
|
|
243
|
+
function fieldKey(path) {
|
|
244
|
+
return JSON.stringify(path);
|
|
245
|
+
}
|
|
246
|
+
function createBundleConfigFace(ctx, t, hintsOf = () => void 0) {
|
|
247
|
+
const forms = ctx.configForms;
|
|
248
|
+
const hints = () => unwrapService(hintsOf());
|
|
249
|
+
const deps = () => ({
|
|
250
|
+
form: forms.get(SESSION_MODE_NS),
|
|
251
|
+
describe: forms.describe(),
|
|
252
|
+
rehydrate: (serialized) => ctx.settingsSchema.rehydrate(serialized),
|
|
253
|
+
validate: (schema, value) => validateSection(schema, value, t),
|
|
254
|
+
t: ctx.locale.bind("settings.schema-form"),
|
|
255
|
+
hints: {
|
|
256
|
+
keysFor: () => [],
|
|
257
|
+
textFor: (path) => hints()?.textFor("session-mode", path) ?? {},
|
|
258
|
+
selectFor: (path) => hints()?.selectFor(SESSION_MODE_NS, path),
|
|
259
|
+
sourceFor: (name) => hints()?.sourceFor(name)
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
const controller = new SchemaFormController(SESSION_MODE_NS, deps());
|
|
263
|
+
const face = controller.face();
|
|
264
|
+
const form = forms.get(SESSION_MODE_NS);
|
|
265
|
+
const statusStore = createSnapshotStore(form.getSnapshot().status);
|
|
266
|
+
const unsubscribeStatus = form.subscribe(() => {
|
|
267
|
+
statusStore.set(form.getSnapshot().status);
|
|
268
|
+
});
|
|
269
|
+
const modelsStore = createSnapshotStore({
|
|
270
|
+
status: "loading",
|
|
271
|
+
routes: []
|
|
272
|
+
});
|
|
273
|
+
const loadModels = async () => {
|
|
274
|
+
try {
|
|
275
|
+
const providers = await loadLlmProviders(ctx);
|
|
276
|
+
modelsStore.set({
|
|
277
|
+
status: "ready",
|
|
278
|
+
routes: routesOf(providers)
|
|
279
|
+
});
|
|
280
|
+
} catch {
|
|
281
|
+
modelsStore.set({
|
|
282
|
+
status: "error",
|
|
283
|
+
routes: []
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
const remote = ctx.get("remote");
|
|
288
|
+
const unsubscribeModels = typeof remote?.$on !== "function" ? [] : [remote.$on("llm/adapters-updated", () => {
|
|
289
|
+
loadModels();
|
|
290
|
+
}), remote.$on("settings/document-updated", () => {
|
|
291
|
+
loadModels();
|
|
292
|
+
})];
|
|
293
|
+
loadModels();
|
|
294
|
+
const diagnose = () => {
|
|
295
|
+
const view = forms.describe().getSnapshot().view;
|
|
296
|
+
const namespaces = view?.namespaces.map((row) => row.ns) ?? [];
|
|
297
|
+
const live = controller.face().hooks.schemaForm.getSnapshot();
|
|
298
|
+
const controllerSummary = `store configured=${String(live.configured)} walked=${String(live.walked.length)}`;
|
|
299
|
+
const own = view?.namespaces.find((row) => row.ns === SESSION_MODE_NS);
|
|
300
|
+
if (own === void 0) return {
|
|
301
|
+
namespaces,
|
|
302
|
+
controller: controllerSummary,
|
|
303
|
+
problem: `describe has no namespace "${SESSION_MODE_NS}"`
|
|
304
|
+
};
|
|
305
|
+
try {
|
|
306
|
+
const schema = ctx.settingsSchema.rehydrate(own.schema);
|
|
307
|
+
const renderable = projectRoot(own.schema, (serialized) => ctx.settingsSchema.rehydrate(serialized)) !== void 0;
|
|
308
|
+
let probe;
|
|
309
|
+
try {
|
|
310
|
+
const rebuilt = new SchemaFormController(SESSION_MODE_NS, deps());
|
|
311
|
+
probe = `probe configured=${String(rebuilt.face().hooks.schemaForm.getSnapshot().configured)}`;
|
|
312
|
+
rebuilt.dispose();
|
|
313
|
+
} catch (error) {
|
|
314
|
+
probe = `probe threw: ${error instanceof Error ? error.message : String(error)}`;
|
|
315
|
+
}
|
|
316
|
+
return {
|
|
317
|
+
namespaces,
|
|
318
|
+
controller: `${controllerSummary}; ${probe}`,
|
|
319
|
+
problem: renderable ? "renderable" : `schema root is "${String(schema.type)}"`
|
|
320
|
+
};
|
|
321
|
+
} catch (error) {
|
|
322
|
+
return {
|
|
323
|
+
namespaces,
|
|
324
|
+
controller: controllerSummary,
|
|
325
|
+
problem: `schema rehydrate failed: ${error instanceof Error ? error.message : String(error)}`
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
const resync = () => {
|
|
330
|
+
statusStore.set(form.getSnapshot().status);
|
|
331
|
+
controller.refresh();
|
|
332
|
+
};
|
|
333
|
+
return {
|
|
334
|
+
face: {
|
|
335
|
+
hooks: {
|
|
336
|
+
bundleConfig: face.hooks.schemaForm,
|
|
337
|
+
bundleStatus: statusStore,
|
|
338
|
+
bundleModels: modelsStore
|
|
339
|
+
},
|
|
340
|
+
diagnose,
|
|
341
|
+
resync,
|
|
342
|
+
set: (path, value) => {
|
|
343
|
+
face.set(path, value);
|
|
344
|
+
},
|
|
345
|
+
editText: (path, text) => {
|
|
346
|
+
if (text === "") face.clear(path);
|
|
347
|
+
else face.setText(path, text, (raw) => ({
|
|
348
|
+
kind: "value",
|
|
349
|
+
value: raw
|
|
350
|
+
}));
|
|
351
|
+
},
|
|
352
|
+
clear: (path) => {
|
|
353
|
+
face.clear(path);
|
|
354
|
+
},
|
|
355
|
+
appendItem: (path) => {
|
|
356
|
+
face.appendItem(path);
|
|
357
|
+
},
|
|
358
|
+
removeItem: (path, index) => {
|
|
359
|
+
face.removeItem(path, index);
|
|
360
|
+
},
|
|
361
|
+
addMode: (id) => {
|
|
362
|
+
const trimmed = id.trim();
|
|
363
|
+
if (trimmed === "") return;
|
|
364
|
+
face.addKey(["modes"], trimmed);
|
|
365
|
+
face.setText([
|
|
366
|
+
"modes",
|
|
367
|
+
trimmed,
|
|
368
|
+
"name"
|
|
369
|
+
], trimmed, (raw) => ({
|
|
370
|
+
kind: "value",
|
|
371
|
+
value: raw
|
|
372
|
+
}));
|
|
373
|
+
},
|
|
374
|
+
removeMode: (id) => {
|
|
375
|
+
if (PROTECTED_MODE_IDS.includes(id)) return;
|
|
376
|
+
face.removeKey(["modes"], id);
|
|
377
|
+
},
|
|
378
|
+
save: () => {
|
|
379
|
+
face.save();
|
|
380
|
+
},
|
|
381
|
+
discard: () => {
|
|
382
|
+
face.discard();
|
|
383
|
+
}
|
|
384
|
+
},
|
|
385
|
+
refresh: resync,
|
|
386
|
+
dispose: () => {
|
|
387
|
+
unsubscribeStatus();
|
|
388
|
+
for (const off of unsubscribeModels) off?.();
|
|
389
|
+
controller.dispose();
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
function validateSection(schema, value, t) {
|
|
394
|
+
try {
|
|
395
|
+
schema(value);
|
|
396
|
+
} catch (error) {
|
|
397
|
+
return failureOf(error);
|
|
398
|
+
}
|
|
399
|
+
return rosterProblem(value, t);
|
|
400
|
+
}
|
|
401
|
+
function rosterProblem(value, t) {
|
|
402
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
403
|
+
const modes = Reflect.get(value, "modes");
|
|
404
|
+
if (typeof modes !== "object" || modes === null || Array.isArray(modes)) return {
|
|
405
|
+
message: t("violation.modesEmpty"),
|
|
406
|
+
path: ["modes"]
|
|
407
|
+
};
|
|
408
|
+
const ids = Object.keys(modes);
|
|
409
|
+
if (ids.length === 0) return {
|
|
410
|
+
message: t("violation.modesEmpty"),
|
|
411
|
+
path: ["modes"]
|
|
412
|
+
};
|
|
413
|
+
const selected = Reflect.get(value, "default");
|
|
414
|
+
if (typeof selected === "string" && selected !== "" && !ids.includes(selected)) return {
|
|
415
|
+
message: t("violation.defaultMissing"),
|
|
416
|
+
path: ["default"]
|
|
417
|
+
};
|
|
418
|
+
for (const id of ids) {
|
|
419
|
+
const mode = Reflect.get(modes, id);
|
|
420
|
+
const model = typeof mode === "object" && mode !== null ? Reflect.get(mode, "defaultModel") : void 0;
|
|
421
|
+
if (typeof model !== "object" || model === null) continue;
|
|
422
|
+
const provider = Reflect.get(model, "provider");
|
|
423
|
+
const name = Reflect.get(model, "model");
|
|
424
|
+
if (provider === void 0 && name === void 0) continue;
|
|
425
|
+
if (typeof provider === "string" && provider !== "" && typeof name === "string" && name !== "") continue;
|
|
426
|
+
return {
|
|
427
|
+
message: t("violation.modelPair"),
|
|
428
|
+
path: [
|
|
429
|
+
"modes",
|
|
430
|
+
id,
|
|
431
|
+
"defaultModel"
|
|
432
|
+
]
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
function projectBundleConfig(state, t, status = "ready") {
|
|
437
|
+
const read = (path) => state.fields.get(fieldKey(path));
|
|
438
|
+
const optionsAt = (path) => state.options.get(fieldKey(path)) ?? [];
|
|
439
|
+
const invalidAt = (path) => state.invalidAt.get(fieldKey(path)) ?? read(path)?.invalid;
|
|
440
|
+
const modes = collectModeIds(state);
|
|
441
|
+
const selected = textOf(read(["default"]));
|
|
442
|
+
return {
|
|
443
|
+
available: state.available,
|
|
444
|
+
writable: state.writable,
|
|
445
|
+
dirty: state.dirty,
|
|
446
|
+
invalid: state.invalid,
|
|
447
|
+
saving: state.saving,
|
|
448
|
+
failed: state.failed,
|
|
449
|
+
configured: state.configured,
|
|
450
|
+
readiness: state.configured ? "ready" : status === "loading" ? "loading" : status === "ready" ? "unreadable" : "missing",
|
|
451
|
+
defaultMode: {
|
|
452
|
+
value: selected,
|
|
453
|
+
options: modes.map((id) => ({
|
|
454
|
+
value: id,
|
|
455
|
+
label: labelOf(id, read)
|
|
456
|
+
})),
|
|
457
|
+
invalid: invalidAt(["default"]) ?? (selected === "" || modes.includes(selected) ? void 0 : t("violation.defaultMissing"))
|
|
458
|
+
},
|
|
459
|
+
modes: modes.map((id) => ({
|
|
460
|
+
id,
|
|
461
|
+
title: labelOf(id, read),
|
|
462
|
+
role: arrayOf(read([
|
|
463
|
+
"modes",
|
|
464
|
+
id,
|
|
465
|
+
"role"
|
|
466
|
+
])),
|
|
467
|
+
summary: summaryOf(id, read, t),
|
|
468
|
+
model: modelOf(id, read),
|
|
469
|
+
deletable: !PROTECTED_MODE_IDS.includes(id),
|
|
470
|
+
staged: MODE_FIELDS.some((spec) => read([
|
|
471
|
+
"modes",
|
|
472
|
+
id,
|
|
473
|
+
...spec.path
|
|
474
|
+
])?.staged === true),
|
|
475
|
+
groups: GROUPS.map((group) => ({
|
|
476
|
+
key: group.key,
|
|
477
|
+
label: t(group.labelKey),
|
|
478
|
+
fields: MODE_FIELDS.filter((spec) => spec.group === group.key).map((spec) => fieldView(id, spec, read, optionsAt, invalidAt, t))
|
|
479
|
+
}))
|
|
480
|
+
})),
|
|
481
|
+
violation: state.violation?.message
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
function labelOf(id, read) {
|
|
485
|
+
const name = textOf(read([
|
|
486
|
+
"modes",
|
|
487
|
+
id,
|
|
488
|
+
"name"
|
|
489
|
+
]));
|
|
490
|
+
return name === "" ? id : name;
|
|
491
|
+
}
|
|
492
|
+
function collectModeIds(state) {
|
|
493
|
+
const ids = [];
|
|
494
|
+
for (const item of state.walked) {
|
|
495
|
+
if (item.path.length < 2 || item.path[0] !== "modes") continue;
|
|
496
|
+
const id = item.path[1];
|
|
497
|
+
if (id === void 0 || ids.includes(id)) continue;
|
|
498
|
+
ids.push(id);
|
|
499
|
+
}
|
|
500
|
+
return ids;
|
|
501
|
+
}
|
|
502
|
+
function modelOf(id, read) {
|
|
503
|
+
if (read([
|
|
504
|
+
"modes",
|
|
505
|
+
id,
|
|
506
|
+
"defaultModel"
|
|
507
|
+
]) === void 0) return void 0;
|
|
508
|
+
return {
|
|
509
|
+
provider: textOf(read([
|
|
510
|
+
"modes",
|
|
511
|
+
id,
|
|
512
|
+
"defaultModel",
|
|
513
|
+
"provider"
|
|
514
|
+
])),
|
|
515
|
+
model: textOf(read([
|
|
516
|
+
"modes",
|
|
517
|
+
id,
|
|
518
|
+
"defaultModel",
|
|
519
|
+
"model"
|
|
520
|
+
]))
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
function fieldView(id, spec, read, optionsAt, invalidAt, t) {
|
|
524
|
+
const path = [
|
|
525
|
+
"modes",
|
|
526
|
+
id,
|
|
527
|
+
...spec.path
|
|
528
|
+
];
|
|
529
|
+
const field = read(path);
|
|
530
|
+
const common = {
|
|
531
|
+
key: fieldKey(path),
|
|
532
|
+
path,
|
|
533
|
+
control: spec.control,
|
|
534
|
+
label: t(spec.labelKey),
|
|
535
|
+
hint: t(spec.hintKey),
|
|
536
|
+
present: field !== void 0,
|
|
537
|
+
overridden: field?.overridden === true,
|
|
538
|
+
options: optionsAt(path),
|
|
539
|
+
invalid: invalidAt(path)
|
|
540
|
+
};
|
|
541
|
+
if (spec.control === "tags" || spec.control === "roles") return {
|
|
542
|
+
...common,
|
|
543
|
+
text: "",
|
|
544
|
+
value: arrayOf(field)
|
|
545
|
+
};
|
|
546
|
+
if (spec.control === "switch") return {
|
|
547
|
+
...common,
|
|
548
|
+
text: "",
|
|
549
|
+
value: field?.value === true
|
|
550
|
+
};
|
|
551
|
+
if (spec.control === "tri") return {
|
|
552
|
+
...common,
|
|
553
|
+
text: "",
|
|
554
|
+
value: triOf(field)
|
|
555
|
+
};
|
|
556
|
+
return {
|
|
557
|
+
...common,
|
|
558
|
+
text: textOf(field),
|
|
559
|
+
value: field?.value
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
function triOf(field) {
|
|
563
|
+
if (field === void 0 || field.value === void 0) return "unset";
|
|
564
|
+
return field.value === true ? "on" : "off";
|
|
565
|
+
}
|
|
566
|
+
function summaryOf(id, read, t) {
|
|
567
|
+
const allowed = arrayOf(read([
|
|
568
|
+
"modes",
|
|
569
|
+
id,
|
|
570
|
+
"allowTools"
|
|
571
|
+
]));
|
|
572
|
+
const denied = arrayOf(read([
|
|
573
|
+
"modes",
|
|
574
|
+
id,
|
|
575
|
+
"denyTools"
|
|
576
|
+
]));
|
|
577
|
+
const provider = read([
|
|
578
|
+
"modes",
|
|
579
|
+
id,
|
|
580
|
+
"defaultModel",
|
|
581
|
+
"provider"
|
|
582
|
+
])?.value;
|
|
583
|
+
const model = read([
|
|
584
|
+
"modes",
|
|
585
|
+
id,
|
|
586
|
+
"defaultModel",
|
|
587
|
+
"model"
|
|
588
|
+
])?.value;
|
|
589
|
+
return `${allowed.length > 0 ? t("summary.tools", { n: String(allowed.length) }) : denied.length > 0 ? t("summary.denyTools", { n: String(denied.length) }) : t("summary.allTools")} · ${typeof model === "string" && model !== "" ? `${typeof provider === "string" && provider !== "" ? `${provider} / ` : ""}${model}` : t("summary.globalModel")}`;
|
|
590
|
+
}
|
|
591
|
+
function textOf(field) {
|
|
592
|
+
if (field === void 0) return "";
|
|
593
|
+
if (typeof field.text === "string") return field.text;
|
|
594
|
+
return typeof field.value === "string" ? field.value : "";
|
|
595
|
+
}
|
|
596
|
+
function arrayOf(field) {
|
|
597
|
+
const value = field?.value;
|
|
598
|
+
return Array.isArray(value) ? value.map((item) => String(item)) : [];
|
|
599
|
+
}
|
|
600
|
+
const ROLES = ROLE_VALUES;
|
|
601
|
+
//#endregion
|
|
602
|
+
//#region src/client/BundleConfigPage.tsx
|
|
603
|
+
function BundleConfigPage(props) {
|
|
604
|
+
const { t, view } = props;
|
|
605
|
+
const face = {
|
|
606
|
+
set: props.set,
|
|
607
|
+
editText: props.editText,
|
|
608
|
+
clear: props.clear,
|
|
609
|
+
appendItem: props.appendItem,
|
|
610
|
+
removeItem: props.removeItem,
|
|
611
|
+
addMode: props.addMode,
|
|
612
|
+
removeMode: props.removeMode,
|
|
613
|
+
save: props.save,
|
|
614
|
+
discard: props.discard
|
|
615
|
+
};
|
|
616
|
+
const snapshot = props.useBundleConfig((current) => current);
|
|
617
|
+
const status = props.useBundleStatus((current) => current);
|
|
618
|
+
const models = props.useBundleModels((current) => current);
|
|
619
|
+
const state = useMemo(() => projectBundleConfig(snapshot, t, status), [
|
|
620
|
+
snapshot,
|
|
621
|
+
t,
|
|
622
|
+
status
|
|
623
|
+
]);
|
|
624
|
+
const [expanded, setExpanded] = useState(() => /* @__PURE__ */ new Set());
|
|
625
|
+
const [addingId, setAddingId] = useState("");
|
|
626
|
+
const [addingOpen, setAddingOpen] = useState(false);
|
|
627
|
+
const [removing, setRemoving] = useState(null);
|
|
628
|
+
const resync = props.resync;
|
|
629
|
+
useEffect(() => {
|
|
630
|
+
resync();
|
|
631
|
+
}, [resync]);
|
|
632
|
+
if (view !== "page") return null;
|
|
633
|
+
if (state.readiness !== "ready") {
|
|
634
|
+
const diagnosis = props.diagnose();
|
|
635
|
+
const message = state.readiness === "loading" ? t("configured.loading") : state.readiness === "missing" ? t("configured.missing") : t("configured.unreadable", { problem: diagnosis.problem });
|
|
636
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
637
|
+
"data-bundle-config": state.readiness,
|
|
638
|
+
"data-namespace": SESSION_MODE_NS,
|
|
639
|
+
children: [/* @__PURE__ */ jsx(Text, {
|
|
640
|
+
as: "p",
|
|
641
|
+
size: "sm",
|
|
642
|
+
tone: "tertiary",
|
|
643
|
+
"data-namespaces": diagnosis.namespaces.join(","),
|
|
644
|
+
"data-problem": diagnosis.problem,
|
|
645
|
+
"data-controller": diagnosis.controller,
|
|
646
|
+
children: message
|
|
647
|
+
}), state.readiness !== "unreadable" ? null : /* @__PURE__ */ jsx(Text, {
|
|
648
|
+
as: "p",
|
|
649
|
+
size: "xs",
|
|
650
|
+
tone: "tertiary",
|
|
651
|
+
mono: true,
|
|
652
|
+
"data-diagnosis": "true",
|
|
653
|
+
children: `${diagnosis.problem} · ${diagnosis.controller} · namespaces=[${diagnosis.namespaces.join(", ")}]`
|
|
654
|
+
})]
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
const labels = {
|
|
658
|
+
unavailable: t("unavailable"),
|
|
659
|
+
readOnly: t("readOnly"),
|
|
660
|
+
saveFailed: t("saveFailed"),
|
|
661
|
+
save: t("save"),
|
|
662
|
+
saving: t("saving")
|
|
663
|
+
};
|
|
664
|
+
const toggle = (id) => {
|
|
665
|
+
setExpanded((previous) => {
|
|
666
|
+
const next = new Set(previous);
|
|
667
|
+
if (next.has(id)) next.delete(id);
|
|
668
|
+
else next.add(id);
|
|
669
|
+
return next;
|
|
670
|
+
});
|
|
671
|
+
};
|
|
672
|
+
return /* @__PURE__ */ jsxs(SettingsForm, {
|
|
673
|
+
labels,
|
|
674
|
+
state,
|
|
675
|
+
onSave: face.save,
|
|
676
|
+
onDiscard: face.discard,
|
|
677
|
+
children: [
|
|
678
|
+
/* @__PURE__ */ jsxs(Stack, {
|
|
679
|
+
gap: 16,
|
|
680
|
+
"data-bundle-config": "page",
|
|
681
|
+
children: [/* @__PURE__ */ jsx(Stack, {
|
|
682
|
+
as: "section",
|
|
683
|
+
gap: 8,
|
|
684
|
+
"data-section": "default",
|
|
685
|
+
children: /* @__PURE__ */ jsx(SettingsFieldRow, {
|
|
686
|
+
label: t("default.label"),
|
|
687
|
+
hint: t("default.hint"),
|
|
688
|
+
layout: "inline",
|
|
689
|
+
...state.defaultMode.invalid === void 0 ? {} : { invalid: state.defaultMode.invalid },
|
|
690
|
+
"data-field": "default",
|
|
691
|
+
"data-control": "choice",
|
|
692
|
+
children: /* @__PURE__ */ jsx(SearchSelect, {
|
|
693
|
+
label: t("default.label"),
|
|
694
|
+
value: state.defaultMode.value,
|
|
695
|
+
emptyLabel: t("tri.unset"),
|
|
696
|
+
options: state.defaultMode.options.map((option) => ({
|
|
697
|
+
value: String(option.value),
|
|
698
|
+
...option.label === void 0 ? {} : { label: option.label }
|
|
699
|
+
})),
|
|
700
|
+
searchLabel: t("select.search"),
|
|
701
|
+
noMatchLabel: t("select.noMatch"),
|
|
702
|
+
disabled: !state.writable,
|
|
703
|
+
onSelect: (next) => {
|
|
704
|
+
if (next === "") face.clear(["default"]);
|
|
705
|
+
else face.set(["default"], next);
|
|
706
|
+
}
|
|
707
|
+
})
|
|
708
|
+
})
|
|
709
|
+
}), /* @__PURE__ */ jsxs(Stack, {
|
|
710
|
+
as: "section",
|
|
711
|
+
gap: 8,
|
|
712
|
+
"data-section": "modes",
|
|
713
|
+
children: [/* @__PURE__ */ jsxs(Row, {
|
|
714
|
+
gap: 8,
|
|
715
|
+
justify: "between",
|
|
716
|
+
children: [/* @__PURE__ */ jsxs(Stack, {
|
|
717
|
+
gap: 2,
|
|
718
|
+
grow: true,
|
|
719
|
+
children: [/* @__PURE__ */ jsx(Text, {
|
|
720
|
+
as: "h4",
|
|
721
|
+
size: "md",
|
|
722
|
+
tone: "primary",
|
|
723
|
+
weight: "strong",
|
|
724
|
+
children: t("modes.label")
|
|
725
|
+
}), /* @__PURE__ */ jsx(Text, {
|
|
726
|
+
as: "p",
|
|
727
|
+
size: "sm",
|
|
728
|
+
tone: "tertiary",
|
|
729
|
+
children: t("modes.hint")
|
|
730
|
+
})]
|
|
731
|
+
}), /* @__PURE__ */ jsx(IconButton, {
|
|
732
|
+
label: t("add.label"),
|
|
733
|
+
"data-action": "open-add-mode",
|
|
734
|
+
disabled: !state.writable,
|
|
735
|
+
onClick: () => {
|
|
736
|
+
setAddingId("");
|
|
737
|
+
setAddingOpen(true);
|
|
738
|
+
},
|
|
739
|
+
children: /* @__PURE__ */ jsx(IconPlusOutlineRegular, {})
|
|
740
|
+
})]
|
|
741
|
+
}), state.modes.map((mode) => /* @__PURE__ */ jsx(ModeCard, {
|
|
742
|
+
mode,
|
|
743
|
+
models,
|
|
744
|
+
open: expanded.has(mode.id),
|
|
745
|
+
writable: state.writable,
|
|
746
|
+
t,
|
|
747
|
+
face,
|
|
748
|
+
onRequestRemove: () => {
|
|
749
|
+
setRemoving(mode);
|
|
750
|
+
},
|
|
751
|
+
onToggle: () => {
|
|
752
|
+
toggle(mode.id);
|
|
753
|
+
}
|
|
754
|
+
}, mode.id))]
|
|
755
|
+
})]
|
|
756
|
+
}),
|
|
757
|
+
/* @__PURE__ */ jsx(Modal, {
|
|
758
|
+
open: addingOpen,
|
|
759
|
+
onClose: () => {
|
|
760
|
+
setAddingOpen(false);
|
|
761
|
+
},
|
|
762
|
+
title: t("add.label"),
|
|
763
|
+
closeLabel: t("add.close"),
|
|
764
|
+
description: t("add.hint"),
|
|
765
|
+
footer: /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Button, {
|
|
766
|
+
variant: "outline",
|
|
767
|
+
onClick: () => {
|
|
768
|
+
setAddingOpen(false);
|
|
769
|
+
},
|
|
770
|
+
children: t("add.cancel")
|
|
771
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
772
|
+
variant: "primary",
|
|
773
|
+
"data-action": "add-mode",
|
|
774
|
+
disabled: addingId.trim() === "",
|
|
775
|
+
onClick: () => {
|
|
776
|
+
const id = addingId.trim();
|
|
777
|
+
face.addMode(id);
|
|
778
|
+
setAddingId("");
|
|
779
|
+
setAddingOpen(false);
|
|
780
|
+
setExpanded((previous) => new Set(previous).add(id));
|
|
781
|
+
},
|
|
782
|
+
children: t("add.confirm")
|
|
783
|
+
})] }),
|
|
784
|
+
children: /* @__PURE__ */ jsx(Input, {
|
|
785
|
+
"data-field": "new-mode",
|
|
786
|
+
"data-modal-autofocus": true,
|
|
787
|
+
value: addingId,
|
|
788
|
+
placeholder: t("add.placeholder"),
|
|
789
|
+
"aria-label": t("add.label"),
|
|
790
|
+
onChange: (event) => {
|
|
791
|
+
setAddingId(event.currentTarget.value);
|
|
792
|
+
}
|
|
793
|
+
})
|
|
794
|
+
}),
|
|
795
|
+
/* @__PURE__ */ jsx(Modal, {
|
|
796
|
+
open: removing !== null,
|
|
797
|
+
onClose: () => {
|
|
798
|
+
setRemoving(null);
|
|
799
|
+
},
|
|
800
|
+
title: t("remove.title"),
|
|
801
|
+
closeLabel: t("remove.close"),
|
|
802
|
+
description: t("remove.description", { name: removing?.title ?? "" }),
|
|
803
|
+
footer: /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Button, {
|
|
804
|
+
variant: "outline",
|
|
805
|
+
onClick: () => {
|
|
806
|
+
setRemoving(null);
|
|
807
|
+
},
|
|
808
|
+
children: t("remove.cancel")
|
|
809
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
810
|
+
variant: "primary",
|
|
811
|
+
tone: "danger",
|
|
812
|
+
"data-action": "confirm-remove",
|
|
813
|
+
onClick: () => {
|
|
814
|
+
if (removing !== null) face.removeMode(removing.id);
|
|
815
|
+
setRemoving(null);
|
|
816
|
+
},
|
|
817
|
+
children: t("remove.confirm")
|
|
818
|
+
})] })
|
|
819
|
+
})
|
|
820
|
+
]
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
function ModeCard({ mode, models, open, writable, t, face, onRequestRemove, onToggle }) {
|
|
824
|
+
const hasModel = mode.model !== void 0;
|
|
825
|
+
return /* @__PURE__ */ jsx(Stack, {
|
|
826
|
+
boxed: true,
|
|
827
|
+
pad: "card",
|
|
828
|
+
"data-mode": mode.id,
|
|
829
|
+
"data-deletable": mode.deletable ? "true" : "false",
|
|
830
|
+
children: /* @__PURE__ */ jsx(DisclosureRow, {
|
|
831
|
+
icon: /* @__PURE__ */ jsx(IconAgentPresetOutlineRegular, {}),
|
|
832
|
+
title: mode.title,
|
|
833
|
+
open,
|
|
834
|
+
expandable: true,
|
|
835
|
+
expandOnRowClick: true,
|
|
836
|
+
onToggle,
|
|
837
|
+
keepContentWhenOpen: true,
|
|
838
|
+
collapsedContent: /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Text, {
|
|
839
|
+
as: "code",
|
|
840
|
+
size: "sm",
|
|
841
|
+
tone: "tertiary",
|
|
842
|
+
"data-mode-id": true,
|
|
843
|
+
children: mode.id
|
|
844
|
+
}), /* @__PURE__ */ jsxs(Row, {
|
|
845
|
+
gap: 8,
|
|
846
|
+
push: true,
|
|
847
|
+
children: [/* @__PURE__ */ jsx(Text, {
|
|
848
|
+
as: "span",
|
|
849
|
+
size: "sm",
|
|
850
|
+
tone: "tertiary",
|
|
851
|
+
children: mode.summary
|
|
852
|
+
}), mode.deletable ? /* @__PURE__ */ jsx(IconButton, {
|
|
853
|
+
size: "sm",
|
|
854
|
+
"data-action": "remove-mode",
|
|
855
|
+
disabled: !writable,
|
|
856
|
+
label: t("removeNamed", { name: mode.title }),
|
|
857
|
+
onClick: (event) => {
|
|
858
|
+
event.stopPropagation();
|
|
859
|
+
onRequestRemove();
|
|
860
|
+
},
|
|
861
|
+
children: /* @__PURE__ */ jsx(IconTrashOutlineRegular, {})
|
|
862
|
+
}) : /* @__PURE__ */ jsx(Text, {
|
|
863
|
+
as: "span",
|
|
864
|
+
size: "sm",
|
|
865
|
+
tone: "tertiary",
|
|
866
|
+
"data-protected": "true",
|
|
867
|
+
children: t("protected")
|
|
868
|
+
})]
|
|
869
|
+
})] }),
|
|
870
|
+
children: /* @__PURE__ */ jsx(Stack, {
|
|
871
|
+
gap: 14,
|
|
872
|
+
pad: "body",
|
|
873
|
+
children: mode.groups.map((group) => /* @__PURE__ */ jsxs(Stack, {
|
|
874
|
+
gap: 0,
|
|
875
|
+
"data-group": group.key,
|
|
876
|
+
children: [/* @__PURE__ */ jsx(Row, {
|
|
877
|
+
gap: 8,
|
|
878
|
+
justify: "between",
|
|
879
|
+
children: /* @__PURE__ */ jsx(Text, {
|
|
880
|
+
size: "sm",
|
|
881
|
+
tone: "secondary",
|
|
882
|
+
weight: "strong",
|
|
883
|
+
children: group.label
|
|
884
|
+
})
|
|
885
|
+
}), group.key === "model" ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
886
|
+
/* @__PURE__ */ jsx(SettingsFieldRow, {
|
|
887
|
+
label: t("field.defaultModel"),
|
|
888
|
+
hint: t("hint.defaultModel"),
|
|
889
|
+
layout: "inline",
|
|
890
|
+
"data-control": "default-model",
|
|
891
|
+
children: /* @__PURE__ */ jsx(Switch, {
|
|
892
|
+
checked: hasModel,
|
|
893
|
+
label: t("field.defaultModel"),
|
|
894
|
+
disabled: !writable,
|
|
895
|
+
onChange: (next) => {
|
|
896
|
+
if (next) face.set([
|
|
897
|
+
"modes",
|
|
898
|
+
mode.id,
|
|
899
|
+
"defaultModel"
|
|
900
|
+
], {
|
|
901
|
+
provider: "",
|
|
902
|
+
model: ""
|
|
903
|
+
});
|
|
904
|
+
else face.clear([
|
|
905
|
+
"modes",
|
|
906
|
+
mode.id,
|
|
907
|
+
"defaultModel"
|
|
908
|
+
]);
|
|
909
|
+
}
|
|
910
|
+
})
|
|
911
|
+
}),
|
|
912
|
+
!hasModel ? null : /* @__PURE__ */ jsx(SettingsFieldRow, {
|
|
913
|
+
label: t("field.modelRoute"),
|
|
914
|
+
hint: t("hint.modelRoute"),
|
|
915
|
+
divider: true,
|
|
916
|
+
"data-control": "model-routes",
|
|
917
|
+
children: /* @__PURE__ */ jsx(ModelRouteList, {
|
|
918
|
+
label: t("field.modelRoute"),
|
|
919
|
+
candidates: models.routes,
|
|
920
|
+
selectedKey: mode.model === void 0 || mode.model.provider === "" ? void 0 : routeKey(mode.model.provider, mode.model.model),
|
|
921
|
+
disabled: !writable,
|
|
922
|
+
status: models.status,
|
|
923
|
+
loadingLabel: t("model.loading"),
|
|
924
|
+
errorLabel: t("model.error"),
|
|
925
|
+
emptyLabel: t("model.empty"),
|
|
926
|
+
onSelect: (candidate) => {
|
|
927
|
+
face.set([
|
|
928
|
+
"modes",
|
|
929
|
+
mode.id,
|
|
930
|
+
"defaultModel"
|
|
931
|
+
], {
|
|
932
|
+
provider: candidate.provider,
|
|
933
|
+
model: candidate.model
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
})
|
|
937
|
+
}),
|
|
938
|
+
group.fields.map((field, index) => /* @__PURE__ */ jsx(FieldRow, {
|
|
939
|
+
field,
|
|
940
|
+
writable,
|
|
941
|
+
t,
|
|
942
|
+
face,
|
|
943
|
+
divider: index > 0 || hasModel
|
|
944
|
+
}, field.key))
|
|
945
|
+
] }) : group.fields.map((field, index) => /* @__PURE__ */ jsx(FieldRow, {
|
|
946
|
+
field,
|
|
947
|
+
writable,
|
|
948
|
+
t,
|
|
949
|
+
face,
|
|
950
|
+
divider: index > 0
|
|
951
|
+
}, field.key))]
|
|
952
|
+
}, group.key))
|
|
953
|
+
})
|
|
954
|
+
})
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
function FieldRow({ field, writable, divider, t, face }) {
|
|
958
|
+
if (field.control === "choice" && !field.present) return null;
|
|
959
|
+
return /* @__PURE__ */ jsx(SettingsFieldRow, {
|
|
960
|
+
label: field.label,
|
|
961
|
+
hint: field.hint,
|
|
962
|
+
divider,
|
|
963
|
+
layout: field.control === "switch" || field.control === "tri" || field.control === "roles" ? "inline" : "stack",
|
|
964
|
+
...field.overridden ? { overriddenLabel: t("overridden") } : {},
|
|
965
|
+
...field.control === "text" ? {
|
|
966
|
+
resetLabel: t("field.reset"),
|
|
967
|
+
onReset: () => {
|
|
968
|
+
face.clear(field.path);
|
|
969
|
+
}
|
|
970
|
+
} : {},
|
|
971
|
+
...field.invalid === void 0 ? {} : { invalid: field.invalid },
|
|
972
|
+
"data-field": field.path.join("."),
|
|
973
|
+
"data-control": field.control,
|
|
974
|
+
children: /* @__PURE__ */ jsx(FieldControl, {
|
|
975
|
+
field,
|
|
976
|
+
writable,
|
|
977
|
+
t,
|
|
978
|
+
face
|
|
979
|
+
})
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
function FieldControl({ field, writable, t, face }) {
|
|
983
|
+
const disabled = !writable;
|
|
984
|
+
const id = useId();
|
|
985
|
+
switch (field.control) {
|
|
986
|
+
case "multiline": return /* @__PURE__ */ jsx(MultilineField, {
|
|
987
|
+
value: field.text,
|
|
988
|
+
rows: 3,
|
|
989
|
+
disabled,
|
|
990
|
+
"aria-label": field.label,
|
|
991
|
+
onChange: (event) => {
|
|
992
|
+
face.editText(field.path, event.currentTarget.value);
|
|
993
|
+
}
|
|
994
|
+
});
|
|
995
|
+
case "choice": return /* @__PURE__ */ jsx(SearchSelect, {
|
|
996
|
+
label: field.label,
|
|
997
|
+
value: field.text,
|
|
998
|
+
emptyLabel: t("tri.unset"),
|
|
999
|
+
options: field.options.map((option) => ({
|
|
1000
|
+
value: String(option.value),
|
|
1001
|
+
...option.label === void 0 ? {} : { label: option.label }
|
|
1002
|
+
})),
|
|
1003
|
+
searchLabel: t("select.search"),
|
|
1004
|
+
noMatchLabel: t("select.noMatch"),
|
|
1005
|
+
disabled,
|
|
1006
|
+
onSelect: (next) => {
|
|
1007
|
+
if (next === "") face.clear(field.path);
|
|
1008
|
+
else face.set(field.path, next);
|
|
1009
|
+
}
|
|
1010
|
+
});
|
|
1011
|
+
case "switch": return /* @__PURE__ */ jsx(Switch, {
|
|
1012
|
+
checked: field.value === true,
|
|
1013
|
+
label: field.label,
|
|
1014
|
+
disabled,
|
|
1015
|
+
onChange: (next) => {
|
|
1016
|
+
face.set(field.path, next);
|
|
1017
|
+
}
|
|
1018
|
+
});
|
|
1019
|
+
case "tri": return /* @__PURE__ */ jsx(SegmentedControl, {
|
|
1020
|
+
id: `${id}-tri`,
|
|
1021
|
+
label: field.label,
|
|
1022
|
+
value: triValue(field),
|
|
1023
|
+
disabled,
|
|
1024
|
+
options: [
|
|
1025
|
+
{
|
|
1026
|
+
value: "unset",
|
|
1027
|
+
label: t("tri.unset")
|
|
1028
|
+
},
|
|
1029
|
+
{
|
|
1030
|
+
value: "on",
|
|
1031
|
+
label: t("tri.on")
|
|
1032
|
+
},
|
|
1033
|
+
{
|
|
1034
|
+
value: "off",
|
|
1035
|
+
label: t("tri.off")
|
|
1036
|
+
}
|
|
1037
|
+
],
|
|
1038
|
+
onChange: (next) => {
|
|
1039
|
+
if (next === "unset") face.clear(field.path);
|
|
1040
|
+
else face.set(field.path, next === "on");
|
|
1041
|
+
}
|
|
1042
|
+
});
|
|
1043
|
+
case "roles": return /* @__PURE__ */ jsx(Row, {
|
|
1044
|
+
gap: 6,
|
|
1045
|
+
wrap: true,
|
|
1046
|
+
"data-control": "roles",
|
|
1047
|
+
children: ROLES.map((role) => {
|
|
1048
|
+
const active = Array.isArray(field.value) && field.value.includes(role);
|
|
1049
|
+
return /* @__PURE__ */ jsx(Button, {
|
|
1050
|
+
variant: active ? "primary" : "outline",
|
|
1051
|
+
size: "sm",
|
|
1052
|
+
"data-mode-role": role,
|
|
1053
|
+
disabled,
|
|
1054
|
+
"aria-pressed": active,
|
|
1055
|
+
onClick: () => {
|
|
1056
|
+
const current = Array.isArray(field.value) ? field.value.map(String) : [];
|
|
1057
|
+
const wanted = active ? current.filter((value) => value !== role) : [...current.filter((value) => value !== role), role];
|
|
1058
|
+
face.set(field.path, wanted);
|
|
1059
|
+
},
|
|
1060
|
+
children: t(`role.${role}`)
|
|
1061
|
+
}, role);
|
|
1062
|
+
})
|
|
1063
|
+
});
|
|
1064
|
+
case "tags": return /* @__PURE__ */ jsx(TagInput, {
|
|
1065
|
+
value: Array.isArray(field.value) ? field.value.map(String) : [],
|
|
1066
|
+
options: field.options.map((option) => ({
|
|
1067
|
+
value: String(option.value),
|
|
1068
|
+
...option.label === void 0 ? {} : { label: option.label }
|
|
1069
|
+
})),
|
|
1070
|
+
placeholder: t("tags.placeholder"),
|
|
1071
|
+
label: field.label,
|
|
1072
|
+
noMatchLabel: t("select.noMatch"),
|
|
1073
|
+
removeLabel: (name) => t("tags.remove", { name }),
|
|
1074
|
+
disabled,
|
|
1075
|
+
onChange: (next) => {
|
|
1076
|
+
face.set(field.path, next);
|
|
1077
|
+
}
|
|
1078
|
+
});
|
|
1079
|
+
case "text": return /* @__PURE__ */ jsx(Input, {
|
|
1080
|
+
value: field.text,
|
|
1081
|
+
"aria-label": field.label,
|
|
1082
|
+
disabled,
|
|
1083
|
+
onChange: (event) => {
|
|
1084
|
+
face.editText(field.path, event.currentTarget.value);
|
|
1085
|
+
}
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
function triValue(field) {
|
|
1090
|
+
return field.value === "unset" || field.value === "on" || field.value === "off" ? field.value : "unset";
|
|
1091
|
+
}
|
|
1092
|
+
//#endregion
|
|
1093
|
+
//#region src/client/bundle-locales.ts
|
|
1094
|
+
const BUNDLE_NS = "session-mode-bundle";
|
|
1095
|
+
const bundleZh = {
|
|
1096
|
+
"default.label": "新会话的默认模式",
|
|
1097
|
+
"default.hint": "还没选过模式的会话从这里开始。",
|
|
1098
|
+
"modes.label": "模式",
|
|
1099
|
+
"modes.hint": "展开一个模式,改它的提示词、工具与技能、上游规则和注入内容。",
|
|
1100
|
+
"add.label": "添加模式",
|
|
1101
|
+
"add.placeholder": "模式 id",
|
|
1102
|
+
"add.confirm": "添加",
|
|
1103
|
+
"add.cancel": "取消",
|
|
1104
|
+
"add.close": "关闭",
|
|
1105
|
+
"add.hint": "给新模式一个 id(选择器与列表里也显示它,加好后可以再改名称)。",
|
|
1106
|
+
removeNamed: "删除模式 {name}",
|
|
1107
|
+
protected: "内置模式,不提供删除",
|
|
1108
|
+
untitled: "(未命名)",
|
|
1109
|
+
overridden: "已覆盖",
|
|
1110
|
+
"group.identity": "基本信息",
|
|
1111
|
+
"group.persona": "提示词",
|
|
1112
|
+
"group.tools": "工具与技能",
|
|
1113
|
+
"group.rules": "上游规则",
|
|
1114
|
+
"group.injections": "请求里注入什么",
|
|
1115
|
+
"group.model": "默认模型",
|
|
1116
|
+
"role.main": "会话可选",
|
|
1117
|
+
"role.subagent": "子代理可用",
|
|
1118
|
+
"field.name": "名称",
|
|
1119
|
+
"field.description": "一句话说明",
|
|
1120
|
+
"field.role": "角色",
|
|
1121
|
+
"field.presetsOnly": "允许挂的 preset",
|
|
1122
|
+
"field.prefix": "提示词前缀",
|
|
1123
|
+
"field.suffix": "提示词后缀",
|
|
1124
|
+
"field.allowTools": "允许的工具",
|
|
1125
|
+
"field.denyTools": "禁用的工具",
|
|
1126
|
+
"field.allowSkills": "允许的技能",
|
|
1127
|
+
"field.denySkills": "禁用的技能",
|
|
1128
|
+
"field.allowPolicies": "生效的规则",
|
|
1129
|
+
"field.denyPolicies": "禁用的规则",
|
|
1130
|
+
"field.instructions": "工作区指令",
|
|
1131
|
+
"field.skills": "技能目录",
|
|
1132
|
+
"field.runtimeContext": "动态快照",
|
|
1133
|
+
"field.defaultModel": "设默认模型",
|
|
1134
|
+
"field.modelRoute": "用哪个模型",
|
|
1135
|
+
"field.reasoningEffort": "思考档位",
|
|
1136
|
+
"field.reset": "恢复默认",
|
|
1137
|
+
"hint.name": "选择器和列表里显示的名字。",
|
|
1138
|
+
"hint.description": "一句话说明这个模式做什么。",
|
|
1139
|
+
"hint.role": "「会话可选」= 用户能在会话里选它;「子代理可用」= 可以把子代理跑在这个模式下。",
|
|
1140
|
+
"hint.presetsOnly": "只允许这个模式挂这些 preset(行清单由挂着的那份提供);不填 = 允许全部(哪份 preset 都行,切模式也不动它)。有值时切到这个模式,会话当前那份不在名单里就换成名单第一个。",
|
|
1141
|
+
"hint.prefix": "加在系统提示词开头的一段话。",
|
|
1142
|
+
"hint.suffix": "加在系统提示词结尾的一段话。",
|
|
1143
|
+
"hint.allowTools": "只允许这些工具;不填 = 不限制(用会话当前 preset 的全部工具)。",
|
|
1144
|
+
"hint.denyTools": "从上面那份里去掉这些工具。",
|
|
1145
|
+
"hint.allowSkills": "只允许这些技能;不填 = 不限制。",
|
|
1146
|
+
"hint.denySkills": "这些技能不列进技能目录,`skill` 工具也加载不了。",
|
|
1147
|
+
"hint.allowPolicies": "只让这些上游规则生效;不填 = 全部生效。",
|
|
1148
|
+
"hint.denyPolicies": "禁用这些上游规则,它的判断连拒绝一起丢掉。",
|
|
1149
|
+
"hint.instructions": "每步都把工作区指令(AGENTS.md 之类)发给模型;关掉就不发。",
|
|
1150
|
+
"hint.skills": "每步都把技能目录发给模型;默认按这个模式的工具名单自动判断。",
|
|
1151
|
+
"hint.runtimeContext": "每步都把沙箱与审批策略发给模型。",
|
|
1152
|
+
"hint.defaultModel": "开 = 这个模式自带一个默认模型(会话还没选过模型时用它);关 = 跟随全局模型。",
|
|
1153
|
+
"hint.modelRoute": "从部署里已设置的模型里挑一条:服务商与模型是一对,选它等于两个一起定。",
|
|
1154
|
+
"hint.reasoningEffort": "思考档位;不填用服务商默认。",
|
|
1155
|
+
"model.loading": "正在读部署里已设置的模型…",
|
|
1156
|
+
"model.error": "读不出部署里已设置的模型。",
|
|
1157
|
+
"model.empty": "部署里还没有已设置的模型。",
|
|
1158
|
+
"tags.placeholder": "加一个后回车,或粘贴一串",
|
|
1159
|
+
"tags.add": "添加",
|
|
1160
|
+
"tags.remove": "移除 {name}",
|
|
1161
|
+
"select.search": "搜索候选",
|
|
1162
|
+
"select.noMatch": "没有匹配的候选",
|
|
1163
|
+
"tri.unset": "默认",
|
|
1164
|
+
"tri.on": "开",
|
|
1165
|
+
"tri.off": "关",
|
|
1166
|
+
"remove.title": "删除模式",
|
|
1167
|
+
"remove.description": "删除 {name}?删除只在保存之后生效,保存之前还可以丢弃。",
|
|
1168
|
+
"remove.confirm": "删除",
|
|
1169
|
+
"remove.cancel": "取消",
|
|
1170
|
+
"remove.close": "关闭",
|
|
1171
|
+
"model.set": "设置默认模型",
|
|
1172
|
+
"model.follow": "改为跟随全局",
|
|
1173
|
+
save: "保存",
|
|
1174
|
+
saving: "保存中…",
|
|
1175
|
+
saveFailed: "保存没有生效,请重试。",
|
|
1176
|
+
readOnly: "这份部署的配置是只读的。",
|
|
1177
|
+
unavailable: "这一行当前不可写。",
|
|
1178
|
+
notSaved: "没有保存:{message}",
|
|
1179
|
+
"configured.loading": "正在读取这一行的配置…",
|
|
1180
|
+
"configured.missing": "设置面里没有 `session-mode` 这一行:没装、被禁用,或者还没装配成功(依赖没到齐时会一直 PENDING)。",
|
|
1181
|
+
"configured.unreadable": "`session-mode` 这一行在跑,但这份配置读不出来({problem})。",
|
|
1182
|
+
"violation.modesEmpty": "至少要有一种模式。",
|
|
1183
|
+
"violation.defaultMissing": "默认模式不在模式清单里。",
|
|
1184
|
+
"violation.modelPair": "默认模型要同时给出服务商与模型。",
|
|
1185
|
+
"summary.allTools": "全部工具",
|
|
1186
|
+
"summary.tools": "{n} 件工具",
|
|
1187
|
+
"summary.denyTools": "禁 {n} 件",
|
|
1188
|
+
"summary.globalModel": "跟随全局模型"
|
|
1189
|
+
};
|
|
1190
|
+
const bundleEn = {
|
|
1191
|
+
"default.label": "Mode for new sessions",
|
|
1192
|
+
"default.hint": "Sessions that never picked a mode start here.",
|
|
1193
|
+
"modes.label": "Modes",
|
|
1194
|
+
"modes.hint": "Expand a mode to edit its prompt, tools and skills, upstream rules, and injected content.",
|
|
1195
|
+
"add.label": "Add a mode",
|
|
1196
|
+
"add.placeholder": "mode id",
|
|
1197
|
+
"add.confirm": "Add",
|
|
1198
|
+
"add.cancel": "Cancel",
|
|
1199
|
+
"add.close": "Close",
|
|
1200
|
+
"add.hint": "Give the new mode an id (it also shows in pickers and lists; rename it after adding).",
|
|
1201
|
+
removeNamed: "Delete mode {name}",
|
|
1202
|
+
protected: "Built-in mode: no delete",
|
|
1203
|
+
untitled: "(untitled)",
|
|
1204
|
+
overridden: "Overridden",
|
|
1205
|
+
"group.identity": "Basics",
|
|
1206
|
+
"group.persona": "Prompt",
|
|
1207
|
+
"group.tools": "Tools and skills",
|
|
1208
|
+
"group.rules": "Upstream rules",
|
|
1209
|
+
"group.injections": "Injected into requests",
|
|
1210
|
+
"group.model": "Default model",
|
|
1211
|
+
"role.main": "Selectable in sessions",
|
|
1212
|
+
"role.subagent": "Usable for subagents",
|
|
1213
|
+
"field.name": "Name",
|
|
1214
|
+
"field.description": "One-line description",
|
|
1215
|
+
"field.role": "Roles",
|
|
1216
|
+
"field.presetsOnly": "Presets it may ride on",
|
|
1217
|
+
"field.prefix": "Prompt prefix",
|
|
1218
|
+
"field.suffix": "Prompt suffix",
|
|
1219
|
+
"field.allowTools": "Allowed tools",
|
|
1220
|
+
"field.denyTools": "Denied tools",
|
|
1221
|
+
"field.allowSkills": "Allowed skills",
|
|
1222
|
+
"field.denySkills": "Denied skills",
|
|
1223
|
+
"field.allowPolicies": "Rules in force",
|
|
1224
|
+
"field.denyPolicies": "Rules disabled",
|
|
1225
|
+
"field.instructions": "Workspace instructions",
|
|
1226
|
+
"field.skills": "Skill catalog",
|
|
1227
|
+
"field.runtimeContext": "Runtime snapshot",
|
|
1228
|
+
"field.defaultModel": "Default model",
|
|
1229
|
+
"field.modelRoute": "Which model",
|
|
1230
|
+
"field.reasoningEffort": "Reasoning effort",
|
|
1231
|
+
"field.reset": "Reset",
|
|
1232
|
+
"hint.name": "Shown in the picker and in lists.",
|
|
1233
|
+
"hint.description": "One line saying what this mode is for.",
|
|
1234
|
+
"hint.role": "Selectable in sessions = the user can pick it; usable for subagents = subagents may run in it.",
|
|
1235
|
+
"hint.presetsOnly": "Only these presets may back this mode (the one it rides on supplies the row list); empty = every preset is allowed (and selecting the mode switches nothing). When set, selecting the mode switches to the first entry if the session's current preset is not listed.",
|
|
1236
|
+
"hint.prefix": "Text prepended to the system prompt.",
|
|
1237
|
+
"hint.suffix": "Text appended to the system prompt.",
|
|
1238
|
+
"hint.allowTools": "Only these tools; empty = no restriction (every tool of the session's preset).",
|
|
1239
|
+
"hint.denyTools": "Those tools are removed from the list above.",
|
|
1240
|
+
"hint.allowSkills": "Only these skills; empty = no restriction.",
|
|
1241
|
+
"hint.denySkills": "Those skills leave the catalog, and the `skill` tool cannot load them.",
|
|
1242
|
+
"hint.allowPolicies": "Only these upstream rules stay in force; empty = all of them.",
|
|
1243
|
+
"hint.denyPolicies": "Those upstream rules are disabled, rejection included.",
|
|
1244
|
+
"hint.instructions": "Sends the workspace instructions (AGENTS.md and friends) with every step; off means it is not sent.",
|
|
1245
|
+
"hint.skills": "Sends the skill catalog with every step; default decides from this mode's tool lists.",
|
|
1246
|
+
"hint.runtimeContext": "Sends the sandbox and approval policies with every step.",
|
|
1247
|
+
"hint.defaultModel": "On = this mode carries a default model (used while a session has picked none); off = follow the global model.",
|
|
1248
|
+
"hint.modelRoute": "Pick one model already set up in this deployment: a provider and a model come as one route, so choosing it sets both.",
|
|
1249
|
+
"hint.reasoningEffort": "Reasoning effort; empty uses the provider default.",
|
|
1250
|
+
"model.loading": "Reading the models set up in this deployment…",
|
|
1251
|
+
"model.error": "Could not read the models set up in this deployment.",
|
|
1252
|
+
"model.empty": "No model is set up in this deployment yet.",
|
|
1253
|
+
"tags.placeholder": "Add one and press Enter, or paste a list",
|
|
1254
|
+
"tags.add": "Add",
|
|
1255
|
+
"tags.remove": "Remove {name}",
|
|
1256
|
+
"select.search": "Search candidates",
|
|
1257
|
+
"select.noMatch": "No matching candidate",
|
|
1258
|
+
"tri.unset": "Default",
|
|
1259
|
+
"tri.on": "On",
|
|
1260
|
+
"tri.off": "Off",
|
|
1261
|
+
"remove.title": "Delete mode",
|
|
1262
|
+
"remove.description": "Delete {name}? The deletion applies on save; until then you can discard it.",
|
|
1263
|
+
"remove.confirm": "Delete",
|
|
1264
|
+
"remove.cancel": "Cancel",
|
|
1265
|
+
"remove.close": "Close",
|
|
1266
|
+
"model.set": "Set a default model",
|
|
1267
|
+
"model.follow": "Follow the global model",
|
|
1268
|
+
save: "Save",
|
|
1269
|
+
saving: "Saving…",
|
|
1270
|
+
saveFailed: "The save did not take effect; try again.",
|
|
1271
|
+
readOnly: "This deployment stores settings read-only.",
|
|
1272
|
+
unavailable: "This row is not writable right now.",
|
|
1273
|
+
notSaved: "Not saved: {message}",
|
|
1274
|
+
"configured.loading": "Reading this row's configuration…",
|
|
1275
|
+
"configured.missing": "The settings surface has no `session-mode` row: not installed, disabled, or not composed yet (it stays PENDING while a dependency is missing).",
|
|
1276
|
+
"configured.unreadable": "The `session-mode` row is running, but its configuration cannot be read ({problem}).",
|
|
1277
|
+
"violation.modesEmpty": "At least one mode is required.",
|
|
1278
|
+
"violation.defaultMissing": "The default mode is not in the mode list.",
|
|
1279
|
+
"violation.modelPair": "A default model needs both a provider and a model.",
|
|
1280
|
+
"summary.allTools": "every tool",
|
|
1281
|
+
"summary.tools": "{n} tools",
|
|
1282
|
+
"summary.denyTools": "{n} denied",
|
|
1283
|
+
"summary.globalModel": "global model"
|
|
1284
|
+
};
|
|
1285
|
+
//#endregion
|
|
1286
|
+
//#region src/client/api.ts
|
|
1287
|
+
async function fetchRoster() {
|
|
1288
|
+
const response = await fetch(SESSION_MODE_PATH, {
|
|
1289
|
+
method: "GET",
|
|
1290
|
+
headers: { accept: "application/json" }
|
|
1291
|
+
});
|
|
1292
|
+
if (!response.ok) throw new Error(`HTTP ${String(response.status)}`);
|
|
1293
|
+
return await response.json();
|
|
1294
|
+
}
|
|
1295
|
+
async function selectMode(sessionId, mode) {
|
|
1296
|
+
const response = await fetch(SESSION_MODE_PATH, {
|
|
1297
|
+
method: "POST",
|
|
1298
|
+
headers: {
|
|
1299
|
+
accept: "application/json",
|
|
1300
|
+
"content-type": "application/json"
|
|
1301
|
+
},
|
|
1302
|
+
body: JSON.stringify({
|
|
1303
|
+
sessionId,
|
|
1304
|
+
mode
|
|
1305
|
+
})
|
|
1306
|
+
});
|
|
1307
|
+
const value = await response.json().catch(() => void 0);
|
|
1308
|
+
if (!response.ok) {
|
|
1309
|
+
const error = value?.["error"];
|
|
1310
|
+
throw new Error(typeof error === "string" ? error : `HTTP ${String(response.status)}`);
|
|
1311
|
+
}
|
|
1312
|
+
return value?.mode ?? mode;
|
|
1313
|
+
}
|
|
1314
|
+
//#endregion
|
|
1315
|
+
//#region src/client/use-roster.ts
|
|
1316
|
+
let pending;
|
|
1317
|
+
function useRoster() {
|
|
1318
|
+
const [roster, setRoster] = useState(void 0);
|
|
1319
|
+
useEffect(() => {
|
|
1320
|
+
let alive = true;
|
|
1321
|
+
pending ??= fetchRoster();
|
|
1322
|
+
pending.then((value) => {
|
|
1323
|
+
if (alive) setRoster(value);
|
|
1324
|
+
}, () => {
|
|
1325
|
+
pending = void 0;
|
|
1326
|
+
});
|
|
1327
|
+
return () => {
|
|
1328
|
+
alive = false;
|
|
1329
|
+
};
|
|
1330
|
+
}, []);
|
|
1331
|
+
return roster;
|
|
1332
|
+
}
|
|
1333
|
+
//#endregion
|
|
1334
|
+
//#region src/client/SessionModeSeat.tsx
|
|
1335
|
+
function SessionModeSeat({ sessionId, useSessions, t }) {
|
|
1336
|
+
const roster = useRoster();
|
|
1337
|
+
const selected = useSessions((state) => {
|
|
1338
|
+
const value = sessionId === void 0 ? void 0 : state.byId[sessionId]?.projectionValues?.sessionMode;
|
|
1339
|
+
return typeof value === "string" ? value : void 0;
|
|
1340
|
+
});
|
|
1341
|
+
const editable = useSessions((state) => {
|
|
1342
|
+
const value = sessionId === void 0 ? void 0 : state.byId[sessionId]?.projectionValues?.sessionModeEditable;
|
|
1343
|
+
return typeof value === "boolean" ? value : void 0;
|
|
1344
|
+
});
|
|
1345
|
+
const [open, setOpen] = useState(false);
|
|
1346
|
+
const [busy, setBusy] = useState(false);
|
|
1347
|
+
const [error, setError] = useState(null);
|
|
1348
|
+
if (roster === void 0) return null;
|
|
1349
|
+
const current = selected ?? roster.default;
|
|
1350
|
+
const label = roster.modes.find((mode) => mode.id === current)?.name ?? current;
|
|
1351
|
+
if (editable === false) return /* @__PURE__ */ jsx(SeatButton, {
|
|
1352
|
+
disabled: true,
|
|
1353
|
+
title: t("lockedHint"),
|
|
1354
|
+
icon: /* @__PURE__ */ jsx(IconAgentPresetOutlineRegular, {}),
|
|
1355
|
+
label
|
|
1356
|
+
});
|
|
1357
|
+
return /* @__PURE__ */ jsx(Menu, {
|
|
1358
|
+
open,
|
|
1359
|
+
onClose: () => {
|
|
1360
|
+
setOpen(false);
|
|
1361
|
+
},
|
|
1362
|
+
items: roster.modes.map((mode) => ({
|
|
1363
|
+
id: mode.id,
|
|
1364
|
+
label: /* @__PURE__ */ jsxs(Stack, {
|
|
1365
|
+
gap: 2,
|
|
1366
|
+
children: [/* @__PURE__ */ jsx(Text, {
|
|
1367
|
+
size: "md",
|
|
1368
|
+
children: mode.name
|
|
1369
|
+
}), /* @__PURE__ */ jsx(Text, {
|
|
1370
|
+
size: "sm",
|
|
1371
|
+
tone: "tertiary",
|
|
1372
|
+
children: mode.description ?? t("noDescription")
|
|
1373
|
+
})]
|
|
1374
|
+
})
|
|
1375
|
+
})),
|
|
1376
|
+
selectedId: current,
|
|
1377
|
+
onSelect: (id) => {
|
|
1378
|
+
setOpen(false);
|
|
1379
|
+
if (sessionId === void 0 || id === current) return;
|
|
1380
|
+
setBusy(true);
|
|
1381
|
+
setError(null);
|
|
1382
|
+
selectMode(sessionId, id).catch((cause) => {
|
|
1383
|
+
setError(cause instanceof Error ? cause.message : String(cause));
|
|
1384
|
+
}).finally(() => {
|
|
1385
|
+
setBusy(false);
|
|
1386
|
+
});
|
|
1387
|
+
},
|
|
1388
|
+
align: "start",
|
|
1389
|
+
portal: true,
|
|
1390
|
+
anchor: /* @__PURE__ */ jsx(SeatButton, {
|
|
1391
|
+
"aria-haspopup": "menu",
|
|
1392
|
+
"aria-expanded": open,
|
|
1393
|
+
title: error ?? t("seatHint"),
|
|
1394
|
+
disabled: busy,
|
|
1395
|
+
icon: /* @__PURE__ */ jsx(IconAgentPresetOutlineRegular, {}),
|
|
1396
|
+
label,
|
|
1397
|
+
chevron: /* @__PURE__ */ jsx(IconChevronDownOutlineRegular, {}),
|
|
1398
|
+
onClick: () => {
|
|
1399
|
+
setOpen((value) => !value);
|
|
1400
|
+
}
|
|
1401
|
+
})
|
|
1402
|
+
});
|
|
1403
|
+
}
|
|
1404
|
+
//#endregion
|
|
1405
|
+
//#region src/client/locales.ts
|
|
1406
|
+
const zh = {
|
|
1407
|
+
seatHint: "选择这个会话的模式",
|
|
1408
|
+
lockedHint: "这个会话已经开始:换模式请新开一个会话",
|
|
1409
|
+
noDescription: "(无说明)",
|
|
1410
|
+
provider: "服务商",
|
|
1411
|
+
providerHint: "服务商 id:部署里注册的任意路由——不论哪个适配器插件注册的(官方的、我们自己的都算)。",
|
|
1412
|
+
model: "模型",
|
|
1413
|
+
modelHint: "模型 id。",
|
|
1414
|
+
reasoningEffort: "思考档位",
|
|
1415
|
+
reasoningEffortHint: "默认就跟服务商自己的默认。"
|
|
1416
|
+
};
|
|
1417
|
+
const en = {
|
|
1418
|
+
seatHint: "Pick the mode for this session",
|
|
1419
|
+
lockedHint: "This session has started: start a new one to pick a mode",
|
|
1420
|
+
noDescription: "(no description)",
|
|
1421
|
+
provider: "Provider",
|
|
1422
|
+
providerHint: "Provider id: any route registered in this deployment, whichever adapter plugin supplies it.",
|
|
1423
|
+
model: "Model",
|
|
1424
|
+
modelHint: "Model id.",
|
|
1425
|
+
reasoningEffort: "Reasoning effort",
|
|
1426
|
+
reasoningEffortHint: "Default keeps the provider's own default."
|
|
1427
|
+
};
|
|
1428
|
+
//#endregion
|
|
1429
|
+
//#region src/client/index.ts
|
|
1430
|
+
const NS = "session-mode";
|
|
1431
|
+
const FIELDS = [
|
|
1432
|
+
"provider",
|
|
1433
|
+
"model",
|
|
1434
|
+
"reasoningEffort"
|
|
1435
|
+
];
|
|
1436
|
+
const POLICY_FIELDS = ["allowPolicies", "denyPolicies"];
|
|
1437
|
+
const DYNAMIC = "*";
|
|
1438
|
+
const inject = ["slots", "locale"];
|
|
1439
|
+
function unwrapHintService(scope) {
|
|
1440
|
+
return unwrapService(scope.schemaFormHints);
|
|
1441
|
+
}
|
|
1442
|
+
function apply(ctx) {
|
|
1443
|
+
const t = ctx.locale.bind(NS);
|
|
1444
|
+
ctx.effect(() => ctx.locale.register(NS, {
|
|
1445
|
+
zh,
|
|
1446
|
+
en
|
|
1447
|
+
}), "session-mode: dictionaries");
|
|
1448
|
+
ctx.inject(["schemaFormHints"], (scope) => scope.effect(() => {
|
|
1449
|
+
const hints = unwrapHintService(scope);
|
|
1450
|
+
const offs = FIELDS.map((key) => hints.describe(SESSION_MODE_NS, [
|
|
1451
|
+
"modes",
|
|
1452
|
+
DYNAMIC,
|
|
1453
|
+
"defaultModel",
|
|
1454
|
+
key
|
|
1455
|
+
], () => ({
|
|
1456
|
+
label: t(key),
|
|
1457
|
+
hint: t(`${key}Hint`)
|
|
1458
|
+
})));
|
|
1459
|
+
return () => {
|
|
1460
|
+
for (const off of offs) off();
|
|
1461
|
+
};
|
|
1462
|
+
}, "session-mode: field wording"));
|
|
1463
|
+
ctx.inject(["schemaFormHints"], (scope) => scope.effect(() => {
|
|
1464
|
+
const offs = POLICY_FIELDS.map((key) => unwrapHintService(scope).select(SESSION_MODE_NS, [
|
|
1465
|
+
"modes",
|
|
1466
|
+
DYNAMIC,
|
|
1467
|
+
key,
|
|
1468
|
+
DYNAMIC
|
|
1469
|
+
], { options: () => POLICY_NAMES.map((value) => ({ value })) }));
|
|
1470
|
+
return () => {
|
|
1471
|
+
for (const off of offs) off();
|
|
1472
|
+
};
|
|
1473
|
+
}, "session-mode: policy candidates"));
|
|
1474
|
+
ctx.inject(["schemaFormHints"], (scope) => scope.effect(() => {
|
|
1475
|
+
const hints = unwrapHintService(scope);
|
|
1476
|
+
const remote = scope.get("remote");
|
|
1477
|
+
if (remote === void 0) return () => {};
|
|
1478
|
+
let providers = [];
|
|
1479
|
+
const load = async () => {
|
|
1480
|
+
providers = await loadLlmProviders(scope);
|
|
1481
|
+
hints.refresh();
|
|
1482
|
+
};
|
|
1483
|
+
const offs = [
|
|
1484
|
+
hints.source("llm-providers", { options: () => providers.map((entry) => ({
|
|
1485
|
+
value: entry.value,
|
|
1486
|
+
label: entry.label
|
|
1487
|
+
})) }),
|
|
1488
|
+
hints.source("llm-models", {
|
|
1489
|
+
dependsOn: [["provider"]],
|
|
1490
|
+
options: (read) => {
|
|
1491
|
+
const provider = read(["provider"]);
|
|
1492
|
+
return typeof provider === "string" ? providers.find((entry) => entry.value === provider)?.models ?? [] : [];
|
|
1493
|
+
}
|
|
1494
|
+
}),
|
|
1495
|
+
remote.$on("llm/adapters-updated", () => {
|
|
1496
|
+
load();
|
|
1497
|
+
}),
|
|
1498
|
+
remote.$on("settings/document-updated", () => {
|
|
1499
|
+
load();
|
|
1500
|
+
})
|
|
1501
|
+
];
|
|
1502
|
+
load();
|
|
1503
|
+
return () => {
|
|
1504
|
+
for (const off of offs) off();
|
|
1505
|
+
};
|
|
1506
|
+
}, "session-mode: model candidates"));
|
|
1507
|
+
ctx.inject(["schemaFormHints"], (scope) => scope.effect(() => {
|
|
1508
|
+
const hints = unwrapHintService(scope);
|
|
1509
|
+
const remote = scope.get("remote");
|
|
1510
|
+
if (remote === void 0) return () => {};
|
|
1511
|
+
const roster = remoteNamespace(scope, "agentPresets");
|
|
1512
|
+
if (roster === void 0) return () => {};
|
|
1513
|
+
let presets = [];
|
|
1514
|
+
const load = async () => {
|
|
1515
|
+
const listed = await roster.list();
|
|
1516
|
+
if (!listed.ok) return;
|
|
1517
|
+
presets = listed.value.presets.map((preset) => ({
|
|
1518
|
+
value: preset.id,
|
|
1519
|
+
label: preset.name ?? preset.id
|
|
1520
|
+
}));
|
|
1521
|
+
hints.refresh();
|
|
1522
|
+
};
|
|
1523
|
+
const offs = [hints.source("agent-presets", { options: () => presets }), remote.$on("settings/document-updated", () => {
|
|
1524
|
+
load();
|
|
1525
|
+
})];
|
|
1526
|
+
load();
|
|
1527
|
+
return () => {
|
|
1528
|
+
for (const off of offs) off();
|
|
1529
|
+
};
|
|
1530
|
+
}, "session-mode: preset candidates"));
|
|
1531
|
+
ctx.inject(["schemaFormHints", "remote"], (scope) => scope.effect(() => {
|
|
1532
|
+
const hints = unwrapHintService(scope);
|
|
1533
|
+
let tools = [];
|
|
1534
|
+
let skills = [];
|
|
1535
|
+
const offs = [
|
|
1536
|
+
hints.source("catalog-tools", { options: () => tools.map((value) => ({ value })) }),
|
|
1537
|
+
hints.source("catalog-skills", { options: () => skills.map((value) => ({ value })) }),
|
|
1538
|
+
hints.source("policies", { options: () => POLICY_NAMES.map((value) => ({ value })) })
|
|
1539
|
+
];
|
|
1540
|
+
let disposeMount;
|
|
1541
|
+
if (typeof scope.remote.$mount !== "function") return () => {
|
|
1542
|
+
for (const off of offs) off();
|
|
1543
|
+
};
|
|
1544
|
+
scope.remote.$mount(SESSION_MODE_CATALOG_REMOTE).then(async (dispose) => {
|
|
1545
|
+
disposeMount = dispose;
|
|
1546
|
+
const listed = await remoteNamespace(scope, CATALOG_NS)?.list();
|
|
1547
|
+
if (listed?.ok !== true) return;
|
|
1548
|
+
tools = listed.value.tools;
|
|
1549
|
+
skills = listed.value.skills;
|
|
1550
|
+
hints.refresh();
|
|
1551
|
+
}).catch(() => {});
|
|
1552
|
+
return () => {
|
|
1553
|
+
for (const off of offs) off();
|
|
1554
|
+
disposeMount?.();
|
|
1555
|
+
};
|
|
1556
|
+
}, "session-mode: catalog candidates"));
|
|
1557
|
+
ctx.inject(["slots", "conversation"], (scope) => {
|
|
1558
|
+
scope.effect(() => {
|
|
1559
|
+
const seat = scope.slots.register({
|
|
1560
|
+
name: "conversation.input.left",
|
|
1561
|
+
id: "session-mode",
|
|
1562
|
+
order: 0,
|
|
1563
|
+
locale: NS
|
|
1564
|
+
}, SessionModeSeat);
|
|
1565
|
+
return () => {
|
|
1566
|
+
seat();
|
|
1567
|
+
};
|
|
1568
|
+
}, "session-mode: composer chip");
|
|
1569
|
+
});
|
|
1570
|
+
let refreshPage;
|
|
1571
|
+
let hints;
|
|
1572
|
+
ctx.inject(["schemaFormHints"], (scope) => scope.effect(() => {
|
|
1573
|
+
const service = unwrapHintService(scope);
|
|
1574
|
+
hints = service;
|
|
1575
|
+
const off = service.subscribe(() => {
|
|
1576
|
+
refreshPage?.();
|
|
1577
|
+
});
|
|
1578
|
+
return () => {
|
|
1579
|
+
off();
|
|
1580
|
+
hints = void 0;
|
|
1581
|
+
};
|
|
1582
|
+
}, "session-mode: bundle page candidates"));
|
|
1583
|
+
ctx.inject([
|
|
1584
|
+
"slots",
|
|
1585
|
+
"locale",
|
|
1586
|
+
"configForms",
|
|
1587
|
+
"settingsSchema"
|
|
1588
|
+
], (scope) => {
|
|
1589
|
+
scope.effect(() => {
|
|
1590
|
+
const { face, refresh, dispose } = createBundleConfigFace(scope, scope.locale.bind(BUNDLE_NS), () => hints);
|
|
1591
|
+
refreshPage = refresh;
|
|
1592
|
+
const bundleDictionary = scope.locale.register(BUNDLE_NS, {
|
|
1593
|
+
zh: bundleZh,
|
|
1594
|
+
en: bundleEn
|
|
1595
|
+
});
|
|
1596
|
+
const off = scope.slots.inject("plugins.bundle.config", () => scope.slots.register({
|
|
1597
|
+
name: "plugins.bundle.config",
|
|
1598
|
+
key: BUNDLE_CONFIG_KEY,
|
|
1599
|
+
locale: BUNDLE_NS,
|
|
1600
|
+
inject: () => face
|
|
1601
|
+
}, BundleConfigPage));
|
|
1602
|
+
return () => {
|
|
1603
|
+
off();
|
|
1604
|
+
bundleDictionary();
|
|
1605
|
+
refreshPage = void 0;
|
|
1606
|
+
dispose();
|
|
1607
|
+
};
|
|
1608
|
+
}, "session-mode: bundle configuration page");
|
|
1609
|
+
});
|
|
1610
|
+
}
|
|
1611
|
+
//#endregion
|
|
1612
|
+
export { SESSION_MODE_NS, apply, inject };
|