@morlay/dsh-session-mode 0.0.1-alpha.4

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.
@@ -0,0 +1,1008 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@morlay/dsh-session-mode",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let react = require("react");
8
+ let _morlay_dsh_client_ui_primitives_client = require("@morlay/dsh-client-ui-primitives/client");
9
+ let react_jsx_runtime = require("react/jsx-runtime");
10
+ let _deepseek_ai_dsh_client_store = require("@deepseek-ai/dsh-client-store");
11
+ let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
12
+ //#region src/client/locales.ts
13
+ const zh = {
14
+ seatHint: "选择这个会话的模式",
15
+ headerHint: "这个会话运行的模式",
16
+ noDescription: "(无说明)",
17
+ switchRefused: "切换失败:{reason}",
18
+ loadFailed: "读不到模式清单",
19
+ cardTitle: "会话模式",
20
+ cardDescription: "给每个会话模式指定默认模型;不指定就跟全局默认模型。",
21
+ defaultsTitle: "各模式的默认模型",
22
+ defaultsHint: "只在会话还没有模型事实(没选过模型、也还没跑过请求)时生效;留空就是跟全局默认模型。改完按下面的保存写进设置。",
23
+ providerLabel: "服务商",
24
+ modelLabel: "模型",
25
+ effortLabel: "思考档位",
26
+ inheritOption: "跟全局默认",
27
+ chooseModel: "选择模型…",
28
+ effortDefaultOption: "跟模型默认",
29
+ staleOption: "{value}(不在目录里)",
30
+ overridden: "已覆盖",
31
+ reset: "恢复默认",
32
+ modelRequired: "还没选模型",
33
+ readOnly: "本部署的设置为只读。",
34
+ unavailable: "本插件当前未加载,暂时无法配置。",
35
+ save: "保存",
36
+ saving: "保存中…",
37
+ saveFailed: "本部署没有接受这些值,已保留供你修改。",
38
+ rosterFailed: "读不到会话模式清单,暂时列不出模式。",
39
+ directoryFailed: "读不到模型目录,暂时只能看当前值或恢复默认。",
40
+ directoryPartial: "这些服务商的目录没读到:{names}"
41
+ };
42
+ const en = {
43
+ seatHint: "Pick the mode for this session",
44
+ headerHint: "The mode this session runs",
45
+ noDescription: "(no description)",
46
+ switchRefused: "Switch refused: {reason}",
47
+ loadFailed: "Could not load the mode roster",
48
+ cardTitle: "Session mode",
49
+ cardDescription: "Set the default model of each session mode; unset follows the global default.",
50
+ defaultsTitle: "Default model per mode",
51
+ defaultsHint: "Applies only while a session has no model fact yet (no model picked, no request made); unset follows the global default model. Save writes it into the settings document.",
52
+ providerLabel: "Provider",
53
+ modelLabel: "Model",
54
+ effortLabel: "Reasoning effort",
55
+ inheritOption: "Same as global default",
56
+ chooseModel: "Choose a model…",
57
+ effortDefaultOption: "Model default",
58
+ staleOption: "{value} (not in the directory)",
59
+ overridden: "Overridden",
60
+ reset: "Reset to default",
61
+ modelRequired: "No model picked yet",
62
+ readOnly: "This deployment stores settings read-only.",
63
+ unavailable: "This plugin is not loaded, so it cannot be configured right now.",
64
+ save: "Save",
65
+ saving: "Saving…",
66
+ saveFailed: "The deployment did not accept these values; they were left for you to correct.",
67
+ rosterFailed: "Could not load the session mode roster, so no mode can be listed.",
68
+ directoryFailed: "Could not read the model directory, so only the current value or a reset is available.",
69
+ directoryPartial: "These providers could not be read: {names}"
70
+ };
71
+ /**
72
+ * 共享设置表单外框的文案,从本页字典里读。
73
+ * @param t - 本页的字典读取器。
74
+ * @returns `SettingsForm` 渲染用的标签。
75
+ */
76
+ function formLabels(t) {
77
+ return {
78
+ unavailable: t("unavailable"),
79
+ readOnly: t("readOnly"),
80
+ saveFailed: t("saveFailed"),
81
+ save: t("save"),
82
+ saving: t("saving")
83
+ };
84
+ }
85
+ //#endregion
86
+ //#region src/client/ModelDefaultsCard.styles.ts
87
+ const styles = {
88
+ section: {
89
+ display: "flex",
90
+ flexDirection: "column",
91
+ gap: "12px"
92
+ },
93
+ heading: {
94
+ margin: "0",
95
+ fontSize: "14px",
96
+ lineHeight: "20px",
97
+ fontWeight: "600",
98
+ color: _morlay_dsh_client_ui_primitives_client.dsw.alias.label.primary
99
+ },
100
+ hint: {
101
+ margin: "0",
102
+ fontSize: "12px",
103
+ lineHeight: "1.5",
104
+ color: _morlay_dsh_client_ui_primitives_client.dsw.alias.label.tertiary
105
+ },
106
+ notice: {
107
+ margin: "0",
108
+ fontSize: "12px",
109
+ lineHeight: "1.5",
110
+ color: _morlay_dsh_client_ui_primitives_client.dsw.alias.state.warn.label
111
+ },
112
+ rows: {
113
+ display: "flex",
114
+ flexDirection: "column",
115
+ gap: "12px"
116
+ },
117
+ row: {
118
+ display: "flex",
119
+ flexDirection: "column",
120
+ gap: "8px",
121
+ padding: "10px 12px",
122
+ border: `0.5px solid ${String(_morlay_dsh_client_ui_primitives_client.dsw.alias.border.l2)}`,
123
+ borderRadius: "8px"
124
+ },
125
+ rowHead: {
126
+ display: "flex",
127
+ alignItems: "center",
128
+ gap: "8px"
129
+ },
130
+ rowName: {
131
+ fontSize: "13px",
132
+ lineHeight: "1.5",
133
+ fontWeight: "500",
134
+ color: _morlay_dsh_client_ui_primitives_client.dsw.alias.label.primary
135
+ },
136
+ badge: {
137
+ display: "inline-flex",
138
+ alignItems: "center",
139
+ borderRadius: "999px",
140
+ padding: "1px 8px",
141
+ fontSize: "11px",
142
+ lineHeight: "17px",
143
+ background: _morlay_dsh_client_ui_primitives_client.dsw.alias.bg.module.platform,
144
+ color: _morlay_dsh_client_ui_primitives_client.dsw.alias.label.secondary
145
+ },
146
+ reset: {
147
+ marginLeft: "auto",
148
+ border: "none",
149
+ background: "none",
150
+ padding: 0,
151
+ font: "inherit",
152
+ fontSize: "12px",
153
+ lineHeight: "1.5",
154
+ color: _morlay_dsh_client_ui_primitives_client.dsw.alias.label.secondary,
155
+ cursor: "pointer",
156
+ "&:hover:not(:disabled)": { color: _morlay_dsh_client_ui_primitives_client.dsw.alias.label.primary },
157
+ "&:disabled": { cursor: "default" }
158
+ },
159
+ rowHint: {
160
+ margin: "0",
161
+ fontSize: "12px",
162
+ lineHeight: "1.5",
163
+ color: _morlay_dsh_client_ui_primitives_client.dsw.alias.label.tertiary
164
+ },
165
+ fields: {
166
+ display: "grid",
167
+ gridTemplateColumns: "repeat(auto-fit, minmax(180px, 1fr))",
168
+ gap: "12px"
169
+ },
170
+ field: {
171
+ display: "flex",
172
+ flexDirection: "column",
173
+ gap: "4px",
174
+ minWidth: 0
175
+ },
176
+ fieldLabel: {
177
+ fontSize: "12px",
178
+ lineHeight: "1.5",
179
+ color: _morlay_dsh_client_ui_primitives_client.dsw.alias.label.secondary
180
+ },
181
+ select: {
182
+ height: "30px",
183
+ padding: "0 8px",
184
+ border: `0.5px solid ${String(_morlay_dsh_client_ui_primitives_client.dsw.alias.border.l4)}`,
185
+ borderRadius: "8px",
186
+ background: _morlay_dsh_client_ui_primitives_client.dsw.alias.bg.layer["3"],
187
+ font: "inherit",
188
+ fontSize: "13px",
189
+ lineHeight: "1.5",
190
+ color: _morlay_dsh_client_ui_primitives_client.dsw.alias.label.primary,
191
+ "&:focus-visible": {
192
+ outline: "none",
193
+ borderColor: _morlay_dsh_client_ui_primitives_client.dsw.alias.brand.primary
194
+ },
195
+ "&:disabled": {
196
+ color: _morlay_dsh_client_ui_primitives_client.dsw.alias.label.tertiary,
197
+ cursor: "default"
198
+ }
199
+ },
200
+ invalid: {
201
+ fontSize: "12px",
202
+ lineHeight: "1.5",
203
+ color: _morlay_dsh_client_ui_primitives_client.dsw.alias.state.error.primary
204
+ }
205
+ };
206
+ //#endregion
207
+ //#region src/client/ModelDefaultsCard.tsx
208
+ /**
209
+ * 设置页「会话模式」那张卡片:每个模式一行「默认模型」,一段一起保存。
210
+ *
211
+ * 页面(Plugins 列表)画标题与一句话说明:`view: 'summary'` 给那句话,`view: 'page'` 给表单。编辑是 staged
212
+ * 的——选择器只改草稿,按保存才写一次 `mutate`;"恢复默认"(或把 provider 选回「跟全局默认」)把这一行的
213
+ * 用户层清掉,回到装配层那份。控件是本卡自研的:这里的编辑面是一张**动态映射**(模式 → provider / model /
214
+ * 档位),原语的标量字段控件套不进去——只借它的壳(保存 / 丢弃 / 只读 / 失败与 `SettingsFormShell`)。
215
+ */
216
+ /**
217
+ * 选择器的选项:目录里的那些 + 当前值那条(不在目录里时补上)。
218
+ *
219
+ * 补当前值是必须的:`<select>` 的 value 不在选项里时浏览器会显示成别的选项,用户按保存就会把它写成
220
+ * 那个值——配置被悄悄改掉。(目录读不到、或适配器卸了的时候正是这个局面。)
221
+ * @param entries - 目录里的选项(provider 或 model)。
222
+ * @param current - 当前生效值的那一项 id。
223
+ * @param t - 本页字典。
224
+ * @returns 选项列表。
225
+ */
226
+ function options(entries, current, t) {
227
+ if (current === void 0 || entries.some((entry) => entry.id === current)) return [...entries];
228
+ return [...entries, {
229
+ id: current,
230
+ name: t("staleOption", { value: current })
231
+ }];
232
+ }
233
+ /**
234
+ * 画一个模式的默认模型那一行。
235
+ * @param props - 这一行的值、可选项、字典与编辑动作。
236
+ * @returns 一行选择器(provider / model / 档位)与"恢复默认"。
237
+ */
238
+ function ModeRow({ row, groups, disabled, t, onProvider, onModel, onEffort, onReset }) {
239
+ const models = groups.find((group) => group.id === row.value?.provider)?.models ?? [];
240
+ const efforts = models.find((entry) => entry.id === row.value?.model)?.reasoning?.efforts ?? [];
241
+ const withEffort = efforts.length > 0 || row.value?.reasoningEffort !== void 0;
242
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
243
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.row),
244
+ children: [
245
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
246
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.rowHead),
247
+ children: [
248
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
249
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.rowName),
250
+ children: row.name
251
+ }),
252
+ row.overridden ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
253
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.badge),
254
+ children: t("overridden")
255
+ }) : null,
256
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
257
+ type: "button",
258
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.reset),
259
+ disabled: disabled || !row.overridden,
260
+ onClick: onReset,
261
+ children: t("reset")
262
+ })
263
+ ]
264
+ }),
265
+ row.description === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
266
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.rowHint),
267
+ children: row.description
268
+ }),
269
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
270
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.fields),
271
+ children: [
272
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
273
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.field),
274
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
275
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.fieldLabel),
276
+ children: t("providerLabel")
277
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
278
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.select),
279
+ value: row.value?.provider ?? "",
280
+ disabled,
281
+ onChange: (event) => {
282
+ onProvider(event.target.value);
283
+ },
284
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
285
+ value: "",
286
+ children: t("inheritOption")
287
+ }), options(groups.map((group) => ({
288
+ id: group.id,
289
+ name: group.name
290
+ })), row.value?.provider, t).map((option) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
291
+ value: option.id,
292
+ children: option.name
293
+ }, option.id))]
294
+ })]
295
+ }),
296
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
297
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.field),
298
+ children: [
299
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
300
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.fieldLabel),
301
+ children: t("modelLabel")
302
+ }),
303
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
304
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.select),
305
+ value: row.value?.model ?? "",
306
+ disabled: disabled || row.value === void 0,
307
+ onChange: (event) => {
308
+ onModel(event.target.value);
309
+ },
310
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
311
+ value: "",
312
+ children: t("chooseModel")
313
+ }), options(models.map((entry) => ({
314
+ id: entry.id,
315
+ name: entry.name
316
+ })), row.value?.model, t).map((option) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
317
+ value: option.id,
318
+ children: option.name
319
+ }, option.id))]
320
+ }),
321
+ row.invalid ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
322
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.invalid),
323
+ role: "status",
324
+ children: t("modelRequired")
325
+ }) : null
326
+ ]
327
+ }),
328
+ withEffort ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
329
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.field),
330
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
331
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.fieldLabel),
332
+ children: t("effortLabel")
333
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
334
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.select),
335
+ value: row.value?.reasoningEffort ?? "",
336
+ disabled: disabled || row.value?.model === void 0 || row.value.model === "",
337
+ onChange: (event) => {
338
+ onEffort(event.target.value);
339
+ },
340
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
341
+ value: "",
342
+ children: t("effortDefaultOption")
343
+ }), options(efforts.map((effort) => ({
344
+ id: effort.id,
345
+ name: effort.name
346
+ })), row.value?.reasoningEffort, t).map((option) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
347
+ value: option.id,
348
+ children: option.name
349
+ }, option.id))]
350
+ })]
351
+ }) : null
352
+ ]
353
+ })
354
+ ]
355
+ });
356
+ }
357
+ /**
358
+ * 渲染这张卡片(或它的一句话说明)。
359
+ * @param props - 字典、快照,以及编辑与保存 / 丢弃动作。
360
+ * @returns 摘要文本,或每个模式一行的默认模型表单。
361
+ */
362
+ function ModelDefaultsCard(props) {
363
+ const { t } = props;
364
+ const state = props.useModelDefaultsCard((snapshot) => snapshot);
365
+ const headingId = (0, react.useId)();
366
+ if (props.view === "summary") return t("cardDescription");
367
+ const disabled = !state.writable || state.saving;
368
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_morlay_dsh_client_ui_primitives_client.SettingsForm, {
369
+ labels: formLabels(t),
370
+ state,
371
+ onSave: props.save,
372
+ onDiscard: props.discard,
373
+ children: state.available ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
374
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.section),
375
+ "aria-labelledby": headingId,
376
+ children: [
377
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
378
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.heading),
379
+ id: headingId,
380
+ children: t("defaultsTitle")
381
+ }),
382
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
383
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.hint),
384
+ children: t("defaultsHint")
385
+ }),
386
+ state.rosterStatus === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
387
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.notice),
388
+ role: "status",
389
+ children: t("rosterFailed")
390
+ }) : null,
391
+ state.directoryStatus === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
392
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.notice),
393
+ role: "status",
394
+ children: t("directoryFailed")
395
+ }) : null,
396
+ state.directoryFailures.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
397
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.notice),
398
+ role: "status",
399
+ children: t("directoryPartial", { names: state.directoryFailures.join("、") })
400
+ }) : null,
401
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
402
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.rows),
403
+ children: state.rows.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModeRow, {
404
+ row,
405
+ groups: state.directory,
406
+ disabled,
407
+ t,
408
+ onProvider: (provider) => {
409
+ props.setProvider(row.id, provider);
410
+ },
411
+ onModel: (model) => {
412
+ props.setModel(row.id, model);
413
+ },
414
+ onEffort: (effort) => {
415
+ props.setEffort(row.id, effort);
416
+ },
417
+ onReset: () => {
418
+ props.resetMode(row.id);
419
+ }
420
+ }, row.id))
421
+ })
422
+ ]
423
+ }) : null
424
+ });
425
+ }
426
+ //#endregion
427
+ //#region src/client/model-defaults-card-controller.ts
428
+ /** 本包 host 行 id:settings 的命名空间名与 `config` 的 volatile 字段都在它下面。 */
429
+ const SESSION_MODE_NS = "session-mode";
430
+ /** 从 settings 的一层里取 `models`(层是原始 JSON 形状,取不到就是空)。 */
431
+ function layerModels(layer) {
432
+ const models = layer?.models;
433
+ return typeof models === "object" && models !== null ? models : {};
434
+ }
435
+ /** 两份模型是不是同一份(三个字段逐个比;缺省字段与 `undefined` 同义)。 */
436
+ function sameModel(left, right) {
437
+ return right !== void 0 && left.provider === right.provider && left.model === right.model && left.reasoningEffort === right.reasoningEffort;
438
+ }
439
+ /** 草稿是不是半成品(选了 provider 还没选 model)——半成品不许保存。 */
440
+ function incomplete(draft) {
441
+ return draft !== void 0 && draft !== null && draft.model === "";
442
+ }
443
+ /** 把 staged 的三元组写成 config 里的那个值(空档位不写键,别把"跟模型默认"物化成一个值)。 */
444
+ function modelValue(draft) {
445
+ return {
446
+ provider: draft.provider,
447
+ model: draft.model,
448
+ ...draft.reasoningEffort === void 0 ? {} : { reasoningEffort: draft.reasoningEffort }
449
+ };
450
+ }
451
+ /** 把一张动态映射编辑成一次设置的写。 */
452
+ var ModelDefaultsCardController = class {
453
+ scope;
454
+ sources;
455
+ store;
456
+ unsubscribe;
457
+ drafts = /* @__PURE__ */ new Map();
458
+ roster;
459
+ rosterStatus = "loading";
460
+ directory = [];
461
+ directoryFailures = [];
462
+ directoryStatus = "loading";
463
+ saving = false;
464
+ failed = false;
465
+ disposed = false;
466
+ saveGeneration = 0;
467
+ /**
468
+ * @param scope - 本包 host 行(`session-mode`)的共享配置表单。
469
+ * @param sources - 模式清单与模型目录的两条读取。
470
+ */
471
+ constructor(scope, sources) {
472
+ this.scope = scope;
473
+ this.sources = sources;
474
+ this.store = (0, _deepseek_ai_dsh_client_store.createSnapshotStore)(this.projection());
475
+ this.unsubscribe = scope.subscribe(() => {
476
+ this.publish();
477
+ });
478
+ this.loadRoster();
479
+ this.loadDirectory();
480
+ }
481
+ /** 停止观察设置,并让晚到的写入结果不再落到状态上。 */
482
+ dispose() {
483
+ this.disposed = true;
484
+ this.saveGeneration += 1;
485
+ this.unsubscribe();
486
+ }
487
+ /**
488
+ * 把状态与动作绑到槽位渲染器上。
489
+ * @returns 快照与编辑、保存动作。
490
+ */
491
+ inject() {
492
+ return {
493
+ hooks: { modelDefaultsCard: this.store },
494
+ setProvider: (modeId, provider) => {
495
+ this.setProvider(modeId, provider);
496
+ },
497
+ setModel: (modeId, model) => {
498
+ this.setModel(modeId, model);
499
+ },
500
+ setEffort: (modeId, effort) => {
501
+ this.setEffort(modeId, effort);
502
+ },
503
+ resetMode: (modeId) => {
504
+ this.stage(modeId, null);
505
+ },
506
+ save: () => {
507
+ this.save();
508
+ },
509
+ discard: () => {
510
+ this.discard();
511
+ }
512
+ };
513
+ }
514
+ /** 用户层有没有这一条。 */
515
+ overridden(modeId) {
516
+ return Object.hasOwn(layerModels(this.scope.getSnapshot().user), modeId);
517
+ }
518
+ /** 生效值(用户层叠在装配层上)。 */
519
+ current(modeId) {
520
+ return this.scope.getSnapshot().value?.models?.[modeId];
521
+ }
522
+ /** 装配层那份(用户层清掉之后剩下的)。 */
523
+ base(modeId) {
524
+ return layerModels(this.scope.getSnapshot().base)[modeId];
525
+ }
526
+ /**
527
+ * 这一行现在的值:草稿优先,否则是生效值;草稿是"回到默认"时看装配层那份(它清掉的就是用户层)。
528
+ * 同一个值既是选择器显示的东西,也是下一次编辑的起点。
529
+ * @param modeId - 模式 id。
530
+ * @returns 这一行呈现的模型;`undefined` = 跟全局默认。
531
+ */
532
+ shown(modeId) {
533
+ const draft = this.drafts.get(modeId);
534
+ if (draft === null) return this.base(modeId);
535
+ return draft ?? this.current(modeId);
536
+ }
537
+ /**
538
+ * 这一行的草稿算不算一次真的编辑。
539
+ * @param modeId - 模式 id。
540
+ * @returns 保存会不会改变什么。
541
+ */
542
+ dirty(modeId) {
543
+ const draft = this.drafts.get(modeId);
544
+ if (draft === void 0) return false;
545
+ if (draft === null) return this.overridden(modeId);
546
+ return !sameModel(draft, this.current(modeId));
547
+ }
548
+ /** 保存之后这一行会不会留下用户层那一条("已覆盖"与"恢复默认"都看它)。 */
549
+ leavesOverride(modeId) {
550
+ return this.overridden(modeId) || this.drafts.get(modeId) !== void 0;
551
+ }
552
+ setProvider(modeId, provider) {
553
+ if (!this.writable() || provider === this.shown(modeId)?.provider) return;
554
+ if (provider === "") {
555
+ this.stage(modeId, null);
556
+ return;
557
+ }
558
+ this.stage(modeId, {
559
+ provider,
560
+ model: ""
561
+ });
562
+ }
563
+ setModel(modeId, model) {
564
+ const current = this.shown(modeId);
565
+ if (!this.writable() || current === void 0) return;
566
+ const keep = this.efforts(current.provider, model).some((effort) => effort.id === current.reasoningEffort);
567
+ this.stage(modeId, {
568
+ provider: current.provider,
569
+ model,
570
+ ...keep && current.reasoningEffort !== void 0 ? { reasoningEffort: current.reasoningEffort } : {}
571
+ });
572
+ }
573
+ setEffort(modeId, effort) {
574
+ const current = this.shown(modeId);
575
+ if (!this.writable() || current === void 0) return;
576
+ this.stage(modeId, {
577
+ provider: current.provider,
578
+ model: current.model,
579
+ ...effort === "" ? {} : { reasoningEffort: effort }
580
+ });
581
+ }
582
+ stage(modeId, draft) {
583
+ if (!this.writable()) return;
584
+ this.drafts.set(modeId, draft);
585
+ this.failed = false;
586
+ this.publish();
587
+ }
588
+ discard() {
589
+ if (this.saving || this.drafts.size === 0) return;
590
+ this.drafts.clear();
591
+ this.failed = false;
592
+ this.publish();
593
+ }
594
+ /** 可以编辑:这一行被服务到(`status: ready`)且文档接受写入。 */
595
+ writable() {
596
+ const snapshot = this.scope.getSnapshot();
597
+ return !this.disposed && !this.saving && snapshot.status === "ready" && snapshot.writable;
598
+ }
599
+ /** 这一次保存要写的 path op(按编辑顺序;没变的行不写)。 */
600
+ plannedOps() {
601
+ const ops = [];
602
+ for (const [modeId, draft] of this.drafts) {
603
+ if (!this.dirty(modeId)) continue;
604
+ ops.push(draft === null ? {
605
+ op: "unset",
606
+ path: ["models", modeId]
607
+ } : {
608
+ op: "set",
609
+ path: ["models", modeId],
610
+ value: modelValue(draft)
611
+ });
612
+ }
613
+ return ops;
614
+ }
615
+ async save() {
616
+ const snapshot = this.scope.getSnapshot();
617
+ const ops = this.plannedOps();
618
+ if (this.disposed || this.saving || snapshot.status !== "ready" || !snapshot.writable || ops.length === 0 || [...this.drafts.values()].some(incomplete)) return;
619
+ const generation = this.saveGeneration;
620
+ this.saving = true;
621
+ this.failed = false;
622
+ this.publish();
623
+ let landed = false;
624
+ try {
625
+ landed = await this.scope.mutate(ops, snapshot.revision);
626
+ } catch {
627
+ landed = false;
628
+ }
629
+ if (generation !== this.saveGeneration || this.disposed) return;
630
+ this.saving = false;
631
+ this.failed = !landed;
632
+ if (landed) this.drafts.clear();
633
+ this.publish();
634
+ }
635
+ async loadRoster() {
636
+ try {
637
+ this.roster = await this.sources.roster();
638
+ this.rosterStatus = "ready";
639
+ } catch {
640
+ this.rosterStatus = "error";
641
+ }
642
+ if (!this.disposed) this.publish();
643
+ }
644
+ async loadDirectory() {
645
+ try {
646
+ const directory = await this.sources.directory();
647
+ this.directory = directory.groups;
648
+ this.directoryFailures = directory.failures;
649
+ this.directoryStatus = "ready";
650
+ } catch {
651
+ this.directoryStatus = "error";
652
+ }
653
+ if (!this.disposed) this.publish();
654
+ }
655
+ /** 某个 provider 在目录里的模型。 */
656
+ modelsOf(provider) {
657
+ return this.directory.find((group) => group.id === provider)?.models ?? [];
658
+ }
659
+ /** 某个 provider + model 声明的档位(模型自带;没声明就是没有)。 */
660
+ efforts(provider, model) {
661
+ return this.modelsOf(provider).find((entry) => entry.id === model)?.reasoning?.efforts ?? [];
662
+ }
663
+ projection() {
664
+ const snapshot = this.scope.getSnapshot();
665
+ const invalid = [...this.drafts.values()].some(incomplete);
666
+ return {
667
+ available: snapshot.status === "ready",
668
+ writable: snapshot.writable,
669
+ dirty: [...this.drafts.keys()].some((modeId) => this.dirty(modeId)),
670
+ invalid,
671
+ saving: this.saving,
672
+ failed: this.failed,
673
+ rows: (this.roster?.modes ?? []).map((mode) => {
674
+ const value = this.shown(mode.id);
675
+ return {
676
+ id: mode.id,
677
+ name: mode.name,
678
+ ...mode.description === void 0 ? {} : { description: mode.description },
679
+ ...value === void 0 ? {} : { value },
680
+ overridden: this.leavesOverride(mode.id),
681
+ dirty: this.dirty(mode.id),
682
+ invalid: incomplete(this.drafts.get(mode.id))
683
+ };
684
+ }),
685
+ directory: this.directory,
686
+ rosterStatus: this.rosterStatus,
687
+ directoryStatus: this.directoryStatus,
688
+ directoryFailures: this.directoryFailures
689
+ };
690
+ }
691
+ publish() {
692
+ this.store.set(this.projection());
693
+ }
694
+ };
695
+ //#endregion
696
+ //#region src/shared.ts
697
+ /**
698
+ * host 半与 client 半共用的接口面:一条 HTTP 路径、模式行的对外形状、请求与响应体。
699
+ *
700
+ * 为什么不是 Typert Remote:客户端的 remote 清单(`@deepseek-ai/dsh-api-remotes/client`)由上游硬编码,
701
+ * 我们的服务不在里面,`ctx.remote.sessionModes` 解析不到。仓库既有的跨半通路是 HTTP 路由
702
+ * (见 `@morlay/ui-conversation-message-actions` 的 `/session-editor`),这里沿用同一种。
703
+ *
704
+ * 会话当前模式不走这条通路:它是一条 session 投影(`sessionMode`),随会话列表一起到页面。
705
+ */
706
+ /** 模式清单与切换的路由路径:宿主(web 与桌面)在同一张路由表上服务它。 */
707
+ const SESSION_MODE_PATH = "/session-mode";
708
+ //#endregion
709
+ //#region src/client/api.ts
710
+ /**
711
+ * client 半与 host 的**唯一**通路:模式清单(`GET`)与切换(`POST`)。
712
+ *
713
+ * 当前模式不走这里——它是一条会话投影(`sessionMode`),随会话列表一起到页面,组件用 `useSessions`
714
+ * 读它。于是这里只剩两件事:拉清单、发一次切换。
715
+ */
716
+ /** 读一次模式清单;清单在页面生命周期里是静态的(装配事实),调用方自己缓存。 */
717
+ async function fetchRoster() {
718
+ const response = await fetch(SESSION_MODE_PATH, {
719
+ method: "GET",
720
+ headers: { accept: "application/json" }
721
+ });
722
+ if (!response.ok) throw new Error(`HTTP ${String(response.status)}`);
723
+ return await response.json();
724
+ }
725
+ /**
726
+ * 把某个空白会话切到某个模式。
727
+ * @param sessionId - 目标会话。
728
+ * @param mode - 目标模式 id。
729
+ * @returns 提交后的模式 id。
730
+ * @throws 会话已开始、模式不存在、会话不存在——host 给的中文原因。
731
+ */
732
+ async function selectMode(sessionId, mode) {
733
+ const response = await fetch(SESSION_MODE_PATH, {
734
+ method: "POST",
735
+ headers: {
736
+ accept: "application/json",
737
+ "content-type": "application/json"
738
+ },
739
+ body: JSON.stringify({
740
+ sessionId,
741
+ mode
742
+ })
743
+ });
744
+ const value = await response.json().catch(() => void 0);
745
+ if (!response.ok) {
746
+ const error = value?.["error"];
747
+ throw new Error(typeof error === "string" ? error : `HTTP ${String(response.status)}`);
748
+ }
749
+ return value?.mode ?? mode;
750
+ }
751
+ //#endregion
752
+ //#region src/client/use-roster.ts
753
+ /**
754
+ * 模块级缓存:清单是装配事实(`session-mode` 行的 config),页面生命周期里不会变,两次挂载之间不必重拉。
755
+ * 失败不缓存——下次挂载就是一次重试。
756
+ */
757
+ let pending;
758
+ /** 读模式清单;还没读到(或读失败)时返回 undefined,组件据此不渲染。 */
759
+ function useRoster() {
760
+ const [roster, setRoster] = (0, react.useState)(void 0);
761
+ (0, react.useEffect)(() => {
762
+ let alive = true;
763
+ pending ??= fetchRoster();
764
+ pending.then((value) => {
765
+ if (alive) setRoster(value);
766
+ }, () => {
767
+ pending = void 0;
768
+ });
769
+ return () => {
770
+ alive = false;
771
+ };
772
+ }, []);
773
+ return roster;
774
+ }
775
+ //#endregion
776
+ //#region \0dsh-css-modules:/Users/morlay/src/github.com/morlay/dsh-plugin/packages/profile/dsh-session-mode/src/client/SessionModeLabel.module.css.mjs
777
+ const css$1 = ".HBEdcq_label{background:var(--dsw-alias-bg-layer-2);max-width:180px;height:22px;color:var(--dsw-alias-label-secondary);white-space:nowrap;text-overflow:ellipsis;border-radius:6px;align-items:center;gap:4px;padding:0 6px;font-size:12px;line-height:22px;display:inline-flex;overflow:hidden}.HBEdcq_icon{opacity:.7;flex:none}";
778
+ const tagId$1 = "@morlay/dsh-session-mode/SessionModeLabel.module.css";
779
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=\"@morlay/dsh-session-mode/SessionModeLabel.module.css\"]") === null) {
780
+ const tag = document.createElement("style");
781
+ tag.dataset.plugin = "@morlay/dsh-session-mode";
782
+ tag.dataset.pluginCss = tagId$1;
783
+ tag.textContent = css$1;
784
+ document.head.appendChild(tag);
785
+ }
786
+ var SessionModeLabel_module_css_default = {
787
+ "label": "HBEdcq_label",
788
+ "icon": "HBEdcq_icon"
789
+ };
790
+ //#endregion
791
+ //#region src/client/SessionModeLabel.tsx
792
+ /**
793
+ * 会话头部的模式标签:**只读**。
794
+ *
795
+ * 一个会话的装配在它开始第一轮时就定下了,头部又是"跑起来之后"才值得看的东西——在这里放一个能切模式的
796
+ * 控件,等于承诺一次 host 会拒绝的切换。说出这个会话运行的是什么,才是这里诚实的表述;选择本身在
797
+ * 新会话屏幕的 chip 上。
798
+ */
799
+ /**
800
+ * 渲染这个会话的模式名。
801
+ * @param props - 槽位合成的 props。
802
+ * @returns 标签;清单没读到时返回 null(此时名字无从得知)。
803
+ */
804
+ function SessionModeLabel({ sessionId, useSessions, t }) {
805
+ const roster = useRoster();
806
+ const mode = useSessions((state) => {
807
+ const value = state.byId[sessionId]?.projectionValues?.sessionMode;
808
+ return typeof value === "string" ? value : void 0;
809
+ });
810
+ if (roster === void 0) return null;
811
+ const id = mode ?? roster.default;
812
+ const chosen = roster.modes.find((entry) => entry.id === id);
813
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
814
+ className: SessionModeLabel_module_css_default.label,
815
+ title: chosen?.description ?? t("headerHint"),
816
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconAgentPresetOutlineRegular, {
817
+ size: 14,
818
+ className: SessionModeLabel_module_css_default.icon
819
+ }), chosen?.name ?? id]
820
+ });
821
+ }
822
+ //#endregion
823
+ //#region \0dsh-css-modules:/Users/morlay/src/github.com/morlay/dsh-plugin/packages/profile/dsh-session-mode/src/client/SessionModeSeat.module.css.mjs
824
+ const css = ".dDMF8q_menuAnchor{min-width:54px;max-width:100%}.dDMF8q_seat{min-width:0;max-width:min(100%,240px);min-height:28px;color:var(--dsw-alias-label-primary);white-space:nowrap;cursor:pointer;background:0 0;border:none;border-radius:16px;align-items:center;gap:4px;padding:0 8px;font-size:13px;font-weight:500;line-height:20px;display:inline-flex}.dDMF8q_seat:not(:disabled):hover,.dDMF8q_seat[aria-expanded=true]{background:var(--dsw-alias-interactive-bg-hover)}.dDMF8q_seat:disabled{cursor:default;color:var(--dsw-alias-label-quaternary)}.dDMF8q_seatIcon{color:var(--dsw-alias-label-primary);flex:none}.dDMF8q_seatLabel{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.dDMF8q_chevron{color:var(--dsw-alias-label-caption);flex:none}.dDMF8q_item{flex-direction:column;gap:2px;max-width:280px;display:flex}.dDMF8q_itemName{color:var(--dsw-alias-label-primary);font-size:13px;line-height:20px}.dDMF8q_itemDesc{color:var(--dsw-alias-label-caption);white-space:normal;font-size:12px;line-height:16px}";
825
+ const tagId = "@morlay/dsh-session-mode/SessionModeSeat.module.css";
826
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=\"@morlay/dsh-session-mode/SessionModeSeat.module.css\"]") === null) {
827
+ const tag = document.createElement("style");
828
+ tag.dataset.plugin = "@morlay/dsh-session-mode";
829
+ tag.dataset.pluginCss = tagId;
830
+ tag.textContent = css;
831
+ document.head.appendChild(tag);
832
+ }
833
+ var SessionModeSeat_module_css_default = {
834
+ "itemDesc": "dDMF8q_itemDesc",
835
+ "seatLabel": "dDMF8q_seatLabel",
836
+ "menuAnchor": "dDMF8q_menuAnchor",
837
+ "chevron": "dDMF8q_chevron",
838
+ "item": "dDMF8q_item",
839
+ "seatIcon": "dDMF8q_seatIcon",
840
+ "seat": "dDMF8q_seat",
841
+ "itemName": "dDMF8q_itemName"
842
+ };
843
+ //#endregion
844
+ //#region src/client/SessionModeSeat.tsx
845
+ /**
846
+ * 新会话屏幕上的模式 chip:**切换列表**就在这里(点开是 coding / chat 两项)。
847
+ *
848
+ * 为什么住在新会话屏幕而不是输入框上方:选择只在会话开始**之前**有效——一旦跑过 turn,那段历史是在某个
849
+ * 模式的工具与提示词下产生的,host 会拒绝换(与上游 `agentPresets.select` 同一条判据)。一个大半辈子都
850
+ * 该禁用的控件,放在它仍然可用的那块屏幕上更诚实。
851
+ */
852
+ /**
853
+ * 渲染新会话的模式 chip。
854
+ * @param props - 槽位合成的 props。
855
+ * @returns chip;清单没读到、或不是主视图的会话时返回 null。
856
+ */
857
+ function SessionModeSeat({ sessionId, useSessions, useSessionRetainInfo, t }) {
858
+ const roster = useRoster();
859
+ const selected = useSessions((state) => {
860
+ const value = sessionId === void 0 ? void 0 : state.byId[sessionId]?.projectionValues?.sessionMode;
861
+ return typeof value === "string" ? value : void 0;
862
+ });
863
+ const main = useSessionRetainInfo((info) => sessionId === void 0 || (info?.retainedBy.mainView ?? 0) > 0);
864
+ const [open, setOpen] = (0, react.useState)(false);
865
+ const [busy, setBusy] = (0, react.useState)(false);
866
+ const [error, setError] = (0, react.useState)(null);
867
+ if (!main || roster === void 0) return null;
868
+ const current = selected ?? roster.default;
869
+ const label = roster.modes.find((mode) => mode.id === current)?.name ?? current;
870
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
871
+ open,
872
+ onClose: () => {
873
+ setOpen(false);
874
+ },
875
+ items: roster.modes.map((mode) => ({
876
+ id: mode.id,
877
+ label: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
878
+ className: SessionModeSeat_module_css_default.item,
879
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
880
+ className: SessionModeSeat_module_css_default.itemName,
881
+ children: mode.name
882
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
883
+ className: SessionModeSeat_module_css_default.itemDesc,
884
+ children: mode.description ?? t("noDescription")
885
+ })]
886
+ })
887
+ })),
888
+ selectedId: current,
889
+ onSelect: (id) => {
890
+ setOpen(false);
891
+ if (sessionId === void 0 || id === current) return;
892
+ setBusy(true);
893
+ setError(null);
894
+ selectMode(sessionId, id).catch((cause) => {
895
+ setError(cause instanceof Error ? cause.message : String(cause));
896
+ }).finally(() => {
897
+ setBusy(false);
898
+ });
899
+ },
900
+ align: "start",
901
+ portal: true,
902
+ className: SessionModeSeat_module_css_default.menuAnchor,
903
+ anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
904
+ type: "button",
905
+ className: SessionModeSeat_module_css_default.seat,
906
+ "aria-haspopup": "menu",
907
+ "aria-expanded": open,
908
+ title: error ?? t("seatHint"),
909
+ disabled: busy,
910
+ onClick: () => {
911
+ setOpen((value) => !value);
912
+ },
913
+ children: [
914
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconAgentPresetOutlineRegular, { className: SessionModeSeat_module_css_default.seatIcon }),
915
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
916
+ className: SessionModeSeat_module_css_default.seatLabel,
917
+ children: label
918
+ }),
919
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: SessionModeSeat_module_css_default.chevron })
920
+ ]
921
+ })
922
+ });
923
+ }
924
+ //#endregion
925
+ //#region src/client/index.ts
926
+ /** 浏览器半插件的字典命名空间。 */
927
+ const NS = "session-mode";
928
+ /**
929
+ * 本行的配置入口 key:`<bundle 包名>#<行 id>`。
930
+ *
931
+ * 页面侧由 `ui-plugin-manager` 的 `rowConfigKey(bundle, rowId)` 生成同一个串——bundle 是本包
932
+ * (`dsh.profile.bundles` 里那一项),行 id 是本包 patch 里插的 `session-mode`。
933
+ */
934
+ const SESSION_MODE_ROW_CONFIG_KEY = "@morlay/dsh-session-mode#session-mode";
935
+ /** 需要的服务:槽位与字典(会话列表经槽位的标准 props 到达组件,不必自己 inject)。 */
936
+ const inject = ["slots", "locale"];
937
+ /**
938
+ * 装上三个面。
939
+ * @param ctx - 浏览器插件上下文。
940
+ */
941
+ function apply(ctx) {
942
+ ctx.effect(() => ctx.locale.register(NS, {
943
+ zh,
944
+ en
945
+ }), "session-mode: dictionaries");
946
+ ctx.inject(["slots", "conversation"], (scope) => {
947
+ scope.effect(() => {
948
+ const seat = scope.slots.register({
949
+ name: "conversation.hero.agentPreset",
950
+ locale: NS
951
+ }, SessionModeSeat);
952
+ const label = scope.slots.register({
953
+ name: "conversation.session.header.actions",
954
+ id: "session-mode",
955
+ order: -10,
956
+ locale: NS
957
+ }, SessionModeLabel);
958
+ return () => {
959
+ seat();
960
+ label();
961
+ };
962
+ }, "session-mode: hero chip and header label");
963
+ });
964
+ ctx.inject([
965
+ "slots",
966
+ "locale",
967
+ "configForms",
968
+ "remote",
969
+ "remote.session"
970
+ ], (forms) => {
971
+ const t = forms.locale.bind(NS);
972
+ forms.effect(() => {
973
+ const controller = new ModelDefaultsCardController(forms.configForms.get(SESSION_MODE_NS), {
974
+ roster: fetchRoster,
975
+ directory: async () => {
976
+ const response = await forms.remote.session.modelCatalog();
977
+ if (!response.ok) throw new Error("model catalog is unavailable");
978
+ return {
979
+ groups: response.value.groups,
980
+ failures: response.value.failures.map((failure) => failure.name)
981
+ };
982
+ }
983
+ });
984
+ const face = controller.inject();
985
+ const page = forms.configForms.whileServed([SESSION_MODE_NS], () => forms.slots.inject("plugins.row.config", () => forms.slots.register({
986
+ name: "plugins.row.config",
987
+ key: SESSION_MODE_ROW_CONFIG_KEY,
988
+ inject: () => ({
989
+ ...face,
990
+ t
991
+ })
992
+ }, ModelDefaultsCard)));
993
+ return () => {
994
+ page();
995
+ controller.dispose();
996
+ };
997
+ }, "session-mode: settings card");
998
+ });
999
+ }
1000
+ //#endregion
1001
+ exports.ModelDefaultsCardController = ModelDefaultsCardController;
1002
+ exports.SESSION_MODE_NS = SESSION_MODE_NS;
1003
+ exports.SESSION_MODE_ROW_CONFIG_KEY = SESSION_MODE_ROW_CONFIG_KEY;
1004
+ exports.apply = apply;
1005
+ exports.inject = inject;
1006
+ return module.exports;
1007
+ }
1008
+ });