@klarkxy/dsh-session-manager 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -73,29 +73,29 @@ const text = {
73
73
  continueNewHint: "在来源会话的工作区新建会话,将摘要和原会话 ID 填入草稿,不自动发送。",
74
74
  compactNothing: "没有值得压缩的内容。",
75
75
  settingsTitle: "会话管理",
76
- settingsGroupTitle: "会话行为",
77
- settingsGroupHint: "继续灰影、自动继续、交接与会话摘要",
78
- settingsMode: "继续灰影",
76
+ composerGroupTitle: "输入框建议",
77
+ composerGroupHint: "输入框为空时显示的下一句建议",
79
78
  modeFixed: "固定文本",
80
79
  modeAuto: "模型生成",
81
- settingsModeHint: "输入框为空时显示建议;按 Tab 填入,点击直接发送。",
80
+ composerUsageHint: "按 Tab 填入建议,点击建议直接发送。",
81
+ ghostModelLabel: "生成模型",
82
82
  continueSourceLabel: "内容来源",
83
83
  continueTextLabel: "固定文本内容",
84
84
  continueTextPlaceholder: "留空使用默认「继续」",
85
- ghostModelLabel: "生成模型",
86
- ghostModelHint: "每轮结束后生成一次并缓存;不选择时使用默认辅助模型。",
87
- judgeModelLabel: "判定模型",
88
- judgeModelHint: "判断回复是否提前结束;不选择时使用默认辅助模型。",
89
- handoffGroup: "交接摘要",
90
- handoffModelLabel: "交接模型",
91
- handoffModelHint: "工具请求会话交接摘要时使用;不选择时使用默认辅助模型。",
92
- welcomeBackGroup: "回到会话时",
85
+ returnGroupTitle: "回到会话时",
86
+ returnGroupHint: "长时间离开后显示摘要或建议压缩",
87
+ awayMinutesLabel: "离开超过",
93
88
  recapEnabledLabel: "显示会话摘要",
94
89
  recapEnabledHint: "在对话中回顾上次进展。",
95
- recapMinutesLabel: "离开多久后显示摘要",
96
90
  compactEnabledLabel: "建议压缩上下文",
97
91
  compactEnabledHint: "长时间离开后,建议先压缩上下文。",
98
- compactMinutesLabel: "离开多久后显示建议",
92
+ recapModelLabel: "摘要模型",
93
+ judgeModelLabel: "判断模型",
94
+ judgeModelHint: "判断回复是否提前结束,并写出继续消息。",
95
+ handoffGroupTitle: "会话交接",
96
+ handoffGroupHint: "智能体读取其他会话时生成的交接摘要",
97
+ handoffModelLabel: "摘要模型",
98
+ handoffModelHint: "智能体请求会话交接摘要时使用。",
99
99
  autoContinue: "自动继续",
100
100
  autoContinueOff: "关闭",
101
101
  autoContinueCautious: "谨慎",
@@ -105,22 +105,24 @@ const text = {
105
105
  autoContinueStale: "状态已在别处更改,已刷新",
106
106
  autoContinueFailed: "切换失败",
107
107
  autoContinueGroup: "自动继续",
108
- autoContinueDefaultLabel: "新会话默认形态",
109
- autoContinueDefaultHint: "只影响之后新建的会话;发送第一条消息前可在输入框上方单独选择,会话开始后保持锁定。",
108
+ autoContinueGroupHint: "回复提前结束时自动发送继续",
109
+ autoContinueModeHint: "开启方式(关闭/谨慎/奔放)在创建会话时按会话选择。",
110
110
  autoContinueMaxLabel: "续跑次数上限",
111
111
  autoContinueMaxHint: "你每发一条消息之后,最多自动继续的次数(1–10)。",
112
112
  autoContinueMaxInvalid: "请填写 1–10 的整数。",
113
113
  minutesUnit: "分钟",
114
- modelLabel: "摘要模型",
114
+ agentActionsGroup: "代理操作",
115
+ agentActionsGroupHint: "允许 agent 自主操作会话",
116
+ allowSelfForkLabel: "允许自我 fork",
117
+ allowSelfForkHint: "允许 agent 自行 fork 当前会话,创建独立分支。",
118
+ allowSendMessageLabel: "允许发送消息",
119
+ allowSendMessageHint: "允许 agent 向其他未归档会话发送消息;空闲会话会被唤醒,运行中的会话会排队接收。",
115
120
  modelFollowOption: "默认辅助模型",
116
- modelGroupHint: "只用于欢迎回来摘要;留空使用默认辅助模型。",
117
- saveSettings: "保存",
118
121
  settingsSaving: "正在保存",
119
122
  settingsSaved: "已保存",
120
123
  settingsLoading: "正在加载",
121
124
  settingsUnavailable: "当前宿主不支持在线修改配置",
122
- minutesInvalid: "请填写大于等于 1 的整数分钟。",
123
- externalChange: "配置已在别处修改。草稿已保留,请刷新页面后再保存。"
125
+ minutesInvalid: "请填写大于等于 1 的整数分钟。"
124
126
  },
125
127
  en: {
126
128
  tempWorkspace: "Temporary Workspace",
@@ -157,29 +159,29 @@ const text = {
157
159
  continueNewHint: "Create a session in the source session's workspace and fill its draft with the recap and source session ID, without sending.",
158
160
  compactNothing: "There is nothing useful to compact.",
159
161
  settingsTitle: "Session Management",
160
- settingsGroupTitle: "Session behavior",
161
- settingsGroupHint: "Continue ghost, auto-continue, handoff and session recap",
162
- settingsMode: "Continue ghost",
162
+ composerGroupTitle: "Composer suggestion",
163
+ composerGroupHint: "The suggested next message shown in an empty composer",
163
164
  modeFixed: "Fixed text",
164
165
  modeAuto: "Model-generated",
165
- settingsModeHint: "Show a suggestion when the composer is empty. Press Tab to fill it, or click to send.",
166
+ composerUsageHint: "Press Tab to fill the suggestion, or click it to send.",
167
+ ghostModelLabel: "Generation model",
166
168
  continueSourceLabel: "Source",
167
169
  continueTextLabel: "Fixed text",
168
170
  continueTextPlaceholder: "Empty uses the default \"Continue\"",
169
- ghostModelLabel: "Generation model",
170
- ghostModelHint: "Generated once after each turn and cached. The default follows the shared auxiliary model.",
171
- judgeModelLabel: "Judge model",
172
- judgeModelHint: "Decides whether a reply stopped early. The default follows the shared auxiliary model.",
173
- handoffGroup: "Session handoff",
174
- handoffModelLabel: "Handoff model",
175
- handoffModelHint: "Used when an agent asks for a session handoff summary. The default follows the shared auxiliary model.",
176
- welcomeBackGroup: "When you return",
171
+ returnGroupTitle: "When you return",
172
+ returnGroupHint: "Show a recap or suggest compaction after time away",
173
+ awayMinutesLabel: "Away for more than",
177
174
  recapEnabledLabel: "Show session recap",
178
175
  recapEnabledHint: "Review your previous progress in the conversation.",
179
- recapMinutesLabel: "Show recap after",
180
176
  compactEnabledLabel: "Suggest context compaction",
181
177
  compactEnabledHint: "Suggest compacting the context after a long time away.",
182
- compactMinutesLabel: "Show suggestion after",
178
+ recapModelLabel: "Recap model",
179
+ judgeModelLabel: "Judge model",
180
+ judgeModelHint: "Decides whether a reply stopped early and writes the follow-up.",
181
+ handoffGroupTitle: "Session handoff",
182
+ handoffGroupHint: "Handoff summaries an agent generates when reading another session",
183
+ handoffModelLabel: "Summary model",
184
+ handoffModelHint: "Used when an agent asks for a session handoff summary.",
183
185
  autoContinue: "Auto-continue",
184
186
  autoContinueOff: "Off",
185
187
  autoContinueCautious: "Cautious",
@@ -189,22 +191,24 @@ const text = {
189
191
  autoContinueStale: "State changed elsewhere; refreshed",
190
192
  autoContinueFailed: "Could not change Auto-continue",
191
193
  autoContinueGroup: "Auto-continue",
192
- autoContinueDefaultLabel: "Default for new sessions",
193
- autoContinueDefaultHint: "Only affects sessions created later. Choose above the composer before sending the first message; the choice stays locked once the session starts.",
194
+ autoContinueGroupHint: "Send a follow-up when a reply stops early",
195
+ autoContinueModeHint: "The mode (Off / Cautious / Bold) is chosen per session when creating it.",
194
196
  autoContinueMaxLabel: "Maximum continuations",
195
197
  autoContinueMaxHint: "How many times it may continue automatically after each message you send (1–10).",
196
198
  autoContinueMaxInvalid: "Enter a whole number from 1 to 10.",
197
199
  minutesUnit: "minutes",
198
- modelLabel: "Recap model",
200
+ agentActionsGroup: "Agent actions",
201
+ agentActionsGroupHint: "Let the agent act on sessions on its own",
202
+ allowSelfForkLabel: "Allow self-fork",
203
+ allowSelfForkHint: "Let the agent fork the current session into an independent branch.",
204
+ allowSendMessageLabel: "Allow sending messages",
205
+ allowSendMessageHint: "Let the agent message other unarchived sessions; idle sessions are woken, running sessions queue the message.",
199
206
  modelFollowOption: "Default auxiliary model",
200
- modelGroupHint: "Used only for the welcome-back recap; empty follows the default auxiliary model.",
201
- saveSettings: "Save",
202
207
  settingsSaving: "Saving",
203
208
  settingsSaved: "Saved",
204
209
  settingsLoading: "Loading",
205
210
  settingsUnavailable: "This runtime cannot edit the plugin configuration",
206
- minutesInvalid: "Enter a whole number of minutes of at least 1.",
207
- externalChange: "Settings changed elsewhere. The draft was kept; reload this page before saving."
211
+ minutesInvalid: "Enter a whole number of minutes of at least 1."
208
212
  }
209
213
  };
210
214
  function uiText(locale, key) {
@@ -257,8 +261,29 @@ const CONFIG_READ_ENDPOINT = "config.read";
257
261
  const CONFIG_WRITE_ENDPOINT = "config.write";
258
262
  const AUTO_CONTINUE_STATUS_ENDPOINT = "autocontinue.status";
259
263
  const AUTO_CONTINUE_UPDATE_ENDPOINT = "autocontinue.update";
260
- /** This plugin's own row inside the shared Session Management settings list. */
261
- const SESSION_SETTINGS_ITEM_ID = "session-manager";
264
+ /** This plugin's rows inside the shared Session Management settings list. */
265
+ const SESSION_SETTINGS_ITEMS = [
266
+ {
267
+ id: "session-manager.composer",
268
+ order: 60
269
+ },
270
+ {
271
+ id: "session-manager.auto-continue",
272
+ order: 62
273
+ },
274
+ {
275
+ id: "session-manager.return",
276
+ order: 64
277
+ },
278
+ {
279
+ id: "session-manager.handoff",
280
+ order: 66
281
+ },
282
+ {
283
+ id: "session-manager.agent-actions",
284
+ order: 68
285
+ }
286
+ ];
262
287
  /** Card entry id in the chat flow; the recap renders at the latest completed turn's tail. */
263
288
  const WELCOME_BACK_CARD_ID = "@klarkxy/dsh-session-manager.welcome-back";
264
289
  const TURN_TAIL_SLOT = "conversation.chat.turnTail";
@@ -403,7 +428,9 @@ Object.values({
403
428
  "--dsh-scrollbar-thumb-hover",
404
429
  "--dsh-content-font-size-secondary",
405
430
  "--dsh-content-font-delta",
406
- "--dsh-frame-top-clearance"
431
+ "--dsh-frame-top-clearance",
432
+ "--dsh-composer-model-icon-display",
433
+ "--dsh-composer-model-text-display"
407
434
  ]
408
435
  }).flat();
409
436
  /**
@@ -730,18 +757,6 @@ const KEY_SEPARATOR = "";
730
757
  function modelRouteKey(provider, model) {
731
758
  return provider && model ? `${provider}${KEY_SEPARATOR}${model}` : "";
732
759
  }
733
- /** Inverse of modelRouteKey; undefined for malformed keys. */
734
- function parseModelRouteKey(value) {
735
- const index = value.indexOf(KEY_SEPARATOR);
736
- if (index <= 0) return void 0;
737
- const provider = value.slice(0, index);
738
- const model = value.slice(index + 1);
739
- if (!provider || !model) return void 0;
740
- return {
741
- provider,
742
- model
743
- };
744
- }
745
760
  //#endregion
746
761
  //#region ../dsh-model-route/src/catalog.ts
747
762
  /** Both services are required: Cordis checks the nested namespace separately. */
@@ -836,6 +851,7 @@ function parseModelMenuChoices(value, bound) {
836
851
  provider,
837
852
  model,
838
853
  label: `${providerName} / ${name}`,
854
+ providerLabel: providerName,
839
855
  efforts
840
856
  });
841
857
  }
@@ -1054,11 +1070,11 @@ function modelMenuShortName(choice) {
1054
1070
  /** Group catalog choices by provider and filter them by a case-insensitive query. */
1055
1071
  function groupModelMenuChoices(choices, query) {
1056
1072
  const needle = query.trim().toLowerCase();
1057
- const matched = needle ? choices.filter((choice) => choice.label.toLowerCase().includes(needle) || choice.model.toLowerCase().includes(needle) || choice.provider.toLowerCase().includes(needle)) : choices;
1073
+ const matched = needle ? choices.filter((choice) => choice.label.toLowerCase().includes(needle) || choice.model.toLowerCase().includes(needle) || choice.provider.toLowerCase().includes(needle) || (choice.providerLabel?.toLowerCase().includes(needle) ?? false)) : choices;
1058
1074
  const groups = [];
1059
1075
  for (const choice of matched) {
1060
1076
  const sep = choice.label.indexOf(" / ");
1061
- const name = sep > 0 ? choice.label.slice(0, sep) : choice.provider;
1077
+ const name = choice.providerLabel ?? (sep > 0 ? choice.label.slice(0, sep) : choice.provider);
1062
1078
  const last = groups.at(-1);
1063
1079
  if (last && last.provider === choice.provider) last.items.push(choice);
1064
1080
  else groups.push({
@@ -1113,7 +1129,7 @@ function ModelMenu(props) {
1113
1129
  empty: "No matching models",
1114
1130
  clear: "Clear search"
1115
1131
  };
1116
- const { open, onOpenChange, anchor, choices, selected, onPick, leading = [], onPickLeading, efforts = [], selectedEffort, onPickEffort, modelLabel = copy.model, modelValue = "—", effortLabel = copy.effort, effortValue, defaultEffortLabel = copy.default, searchPlaceholder = copy.search, emptyLabel = copy.empty, clearSearchLabel = copy.clear, className, listClassName } = props;
1132
+ const { open, onOpenChange, anchor, choices, selected, onPick, leading = [], onPickLeading, efforts = [], selectedEffort, onPickEffort, modelLabel = copy.model, modelValue = "—", effortLabel = copy.effort, effortValue, defaultEffortLabel = copy.default, searchPlaceholder = copy.search, emptyLabel = copy.empty, clearSearchLabel = copy.clear, initialPane = "root", alwaysShowSearch = false, reasoningSlider = false, menuHeader, disabled = false, busy = false, closeOnPick = true, closeOnDisable = true, className, listClassName } = props;
1117
1133
  const rootRef = (0, react.useRef)(null);
1118
1134
  const openRef = (0, react.useRef)(open);
1119
1135
  openRef.current = open;
@@ -1128,9 +1144,10 @@ function ModelMenu(props) {
1128
1144
  const [pane, setPane] = (0, react.useState)("root");
1129
1145
  const [query, setQuery] = (0, react.useState)("");
1130
1146
  const [highlightedIndex, setHighlightedIndex] = (0, react.useState)(null);
1147
+ const [keyNav, setKeyNav] = (0, react.useState)(0);
1131
1148
  const [menuPos, setMenuPos] = (0, react.useState)(null);
1132
1149
  const id = (0, react.useId)();
1133
- const showSearch = choices.length > 4;
1150
+ const showSearch = alwaysShowSearch || choices.length > 4;
1134
1151
  const effortRows = (0, react.useMemo)(() => [{
1135
1152
  id: "",
1136
1153
  name: defaultEffortLabel
@@ -1147,7 +1164,12 @@ function ModelMenu(props) {
1147
1164
  items: (0, _deepseek_ai_dsh_client_ui_primitives.rankByName)(group.items.map((choice) => ({
1148
1165
  choice,
1149
1166
  name: modelMenuShortName(choice),
1150
- label: choice.model
1167
+ label: [
1168
+ choice.provider,
1169
+ choice.providerLabel,
1170
+ choice.model,
1171
+ choice.label
1172
+ ].filter(Boolean).join(" ")
1151
1173
  })), showSearch ? query.trim() : "")
1152
1174
  })).filter((group) => group.items.length > 0), [
1153
1175
  choices,
@@ -1179,10 +1201,12 @@ function ModelMenu(props) {
1179
1201
  paneFocus.current = "drill";
1180
1202
  setPane(next);
1181
1203
  };
1182
- /** Leave a drilled pane for the root one, handing the keyboard back to its cell. */
1204
+ /** Leave a drilled pane for the entry pane, handing the keyboard back to it.
1205
+ * A root entry refocuses the cell that opened the pane; a search-first
1206
+ * (model) entry refocuses that pane's own intent. */
1183
1207
  const back = (from) => {
1184
- paneFocus.current = from;
1185
- setPane("root");
1208
+ paneFocus.current = initialPane === "root" ? from : "drill";
1209
+ setPane(initialPane);
1186
1210
  };
1187
1211
  const moveFocus = (offset) => {
1188
1212
  const items = itemRefs.current.filter((item) => item !== null && !item.disabled);
@@ -1222,10 +1246,18 @@ function ModelMenu(props) {
1222
1246
  }, [requestedOpen, open]);
1223
1247
  (0, react.useEffect)(() => {
1224
1248
  if (!open) return;
1225
- setPane("root");
1249
+ setPane(initialPane);
1250
+ if (initialPane === "model") paneFocus.current = "drill";
1226
1251
  setQuery("");
1227
1252
  setHighlightedIndex(null);
1228
- }, [open]);
1253
+ }, [open, initialPane]);
1254
+ (0, react.useEffect)(() => {
1255
+ if (disabled && open && closeOnDisable) changeOpen(false);
1256
+ }, [
1257
+ disabled,
1258
+ open,
1259
+ closeOnDisable
1260
+ ]);
1229
1261
  (0, react.useEffect)(() => {
1230
1262
  if (!open) return;
1231
1263
  const closeOutside = (event) => {
@@ -1233,9 +1265,25 @@ function ModelMenu(props) {
1233
1265
  if (rootRef.current?.contains(target) || menuRef.current?.contains(target)) return;
1234
1266
  changeOpen(false);
1235
1267
  };
1268
+ const escapeOutside = (event) => {
1269
+ if (event.key !== "Escape" || event.defaultPrevented || event.isComposing) return;
1270
+ if (!openRef.current) return;
1271
+ event.preventDefault();
1272
+ if (pane !== initialPane) back(pane);
1273
+ else close(true);
1274
+ };
1236
1275
  document.addEventListener("mousedown", closeOutside);
1237
- return () => document.removeEventListener("mousedown", closeOutside);
1238
- }, [open, onOpenChange]);
1276
+ document.addEventListener("keydown", escapeOutside);
1277
+ return () => {
1278
+ document.removeEventListener("mousedown", closeOutside);
1279
+ document.removeEventListener("keydown", escapeOutside);
1280
+ };
1281
+ }, [
1282
+ open,
1283
+ onOpenChange,
1284
+ pane,
1285
+ initialPane
1286
+ ]);
1239
1287
  (0, react.useEffect)(() => {
1240
1288
  const intent = paneFocus.current;
1241
1289
  if (!open || menuPos === null || intent === null) return;
@@ -1265,13 +1313,21 @@ function ModelMenu(props) {
1265
1313
  pane,
1266
1314
  filteredGroups
1267
1315
  ]);
1316
+ const modelPaneOpen = open && pane === "model";
1317
+ const scrollSignal = !modelPaneOpen ? "closed" : keyNav === 0 ? "open" : `key:${keyNav}`;
1318
+ const scrollSignalRef = (0, react.useRef)(null);
1268
1319
  (0, react.useLayoutEffect)(() => {
1269
- if (open && pane === "model" && activeModelIndex >= 0) itemRefs.current[leading.length + activeModelIndex]?.scrollIntoView({ block: "nearest" });
1320
+ if (!modelPaneOpen || activeModelIndex < 0) {
1321
+ if (!modelPaneOpen) scrollSignalRef.current = null;
1322
+ return;
1323
+ }
1324
+ if (scrollSignalRef.current === scrollSignal) return;
1325
+ scrollSignalRef.current = scrollSignal;
1326
+ itemRefs.current[leading.length + activeModelIndex]?.scrollIntoView({ block: "nearest" });
1270
1327
  }, [
1271
- open,
1272
- pane,
1328
+ modelPaneOpen,
1329
+ scrollSignal,
1273
1330
  activeModelIndex,
1274
- visibleModels,
1275
1331
  leading.length
1276
1332
  ]);
1277
1333
  (0, react.useLayoutEffect)(() => {
@@ -1286,7 +1342,7 @@ function ModelMenu(props) {
1286
1342
  let x = rect.right - lw, y = rect.top - 8 - lh;
1287
1343
  if (lw > 0) x = Math.min(Math.max(x, margin), window.innerWidth - lw - margin);
1288
1344
  if (lh > 0) y = Math.min(Math.max(y, margin), window.innerHeight - lh - margin);
1289
- setMenuPos({
1345
+ setMenuPos((prev) => prev && prev.left === x && prev.top === y ? prev : {
1290
1346
  left: x,
1291
1347
  top: y
1292
1348
  });
@@ -1306,6 +1362,13 @@ function ModelMenu(props) {
1306
1362
  ]);
1307
1363
  function onKeyDown(event) {
1308
1364
  if (event.defaultPrevented || event.nativeEvent.isComposing) return;
1365
+ if (event.key === "Escape" && open) {
1366
+ event.preventDefault();
1367
+ if (pane !== initialPane) back(pane);
1368
+ else close(true);
1369
+ return;
1370
+ }
1371
+ if (disabled) return;
1309
1372
  if (!open && [
1310
1373
  "Enter",
1311
1374
  " ",
@@ -1319,17 +1382,12 @@ function ModelMenu(props) {
1319
1382
  requestOpen();
1320
1383
  return;
1321
1384
  }
1322
- if (event.key === "Escape" && open) {
1323
- event.preventDefault();
1324
- if (pane !== "root") back(pane);
1325
- else close(true);
1326
- return;
1327
- }
1328
1385
  if (!open) return;
1329
1386
  if (pane === "model" && showSearch && (event.key === "ArrowDown" || event.key === "ArrowUp")) {
1330
1387
  event.preventDefault();
1331
1388
  if (visibleModels.length > 0) {
1332
1389
  setHighlightedIndex((activeModelIndex + (event.key === "ArrowDown" ? 1 : -1) + visibleModels.length) % visibleModels.length);
1390
+ setKeyNav((n) => n + 1);
1333
1391
  searchRef.current?.focus();
1334
1392
  }
1335
1393
  return;
@@ -1338,19 +1396,19 @@ function ModelMenu(props) {
1338
1396
  if (event.key === "Tab" && visibleModels.length === 0) return;
1339
1397
  event.preventDefault();
1340
1398
  const highlighted = visibleModels[activeModelIndex];
1341
- if (highlighted) {
1399
+ if (highlighted && !busy) {
1342
1400
  onPick({
1343
1401
  provider: highlighted.provider,
1344
1402
  model: highlighted.model
1345
1403
  });
1346
- close(true);
1404
+ if (closeOnPick) close(true);
1347
1405
  }
1348
1406
  return;
1349
1407
  }
1350
1408
  if (event.key === "Tab") {
1351
1409
  if (event.shiftKey) {
1352
1410
  event.preventDefault();
1353
- if (pane !== "root") back(pane);
1411
+ if (pane !== initialPane) back(pane);
1354
1412
  else close(true);
1355
1413
  return;
1356
1414
  }
@@ -1374,7 +1432,10 @@ function ModelMenu(props) {
1374
1432
  }
1375
1433
  }
1376
1434
  function onBlur(event) {
1377
- if (event.relatedTarget instanceof Node && (rootRef.current?.contains(event.relatedTarget) || menuRef.current?.contains(event.relatedTarget))) return;
1435
+ const related = event.relatedTarget;
1436
+ if (related instanceof Node && (rootRef.current?.contains(related) || menuRef.current?.contains(related) || !related.isConnected)) return;
1437
+ if (!closeOnPick && !(related instanceof Node)) return;
1438
+ if (busy && related == null) return;
1378
1439
  if (open) changeOpen(false);
1379
1440
  }
1380
1441
  itemRefs.current = [];
@@ -1395,11 +1456,16 @@ function ModelMenu(props) {
1395
1456
  "aria-label": modelLabel,
1396
1457
  lang: language,
1397
1458
  children: [
1459
+ menuHeader ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1460
+ className: "dsh-model-menu-header",
1461
+ children: menuHeader
1462
+ }) : null,
1398
1463
  pane === "root" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1399
1464
  ref: itemRef(),
1400
1465
  type: "button",
1401
1466
  role: "menuitem",
1402
1467
  className: "dsh-model-menu-cell",
1468
+ disabled,
1403
1469
  onClick: () => drill("model"),
1404
1470
  children: [
1405
1471
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -1412,11 +1478,12 @@ function ModelMenu(props) {
1412
1478
  }),
1413
1479
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutlineRegular, { className: "dsh-model-menu-cellChevron" })
1414
1480
  ]
1415
- }), efforts.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1481
+ }), efforts.length > 0 && !reasoningSlider ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1416
1482
  ref: itemRef(),
1417
1483
  type: "button",
1418
1484
  role: "menuitem",
1419
1485
  className: "dsh-model-menu-cell",
1486
+ disabled,
1420
1487
  onClick: () => drill("effort"),
1421
1488
  children: [
1422
1489
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -1435,6 +1502,7 @@ function ModelMenu(props) {
1435
1502
  className: "dsh-model-menu-searchRow",
1436
1503
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
1437
1504
  ref: searchRef,
1505
+ disabled,
1438
1506
  className: `dsh-model-menu-search${query !== "" ? " dsh-model-menu-searchWithQuery" : ""}`,
1439
1507
  type: "text",
1440
1508
  role: "searchbox",
@@ -1451,6 +1519,7 @@ function ModelMenu(props) {
1451
1519
  type: "button",
1452
1520
  className: "dsh-model-menu-searchClear",
1453
1521
  "aria-label": clearSearchLabel,
1522
+ disabled,
1454
1523
  onClick: () => {
1455
1524
  setQuery("");
1456
1525
  setHighlightedIndex(null);
@@ -1473,10 +1542,12 @@ function ModelMenu(props) {
1473
1542
  type: "button",
1474
1543
  role: "menuitemradio",
1475
1544
  "aria-checked": item.selected === true,
1545
+ disabled: disabled || busy,
1476
1546
  className: `dsh-model-menu-option${item.selected === true ? " dsh-model-menu-selected" : ""}`,
1477
1547
  onClick: () => {
1548
+ if (disabled || busy) return;
1478
1549
  onPickLeading?.(item.id);
1479
- close(true);
1550
+ if (closeOnPick) close(true);
1480
1551
  },
1481
1552
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1482
1553
  className: "dsh-model-menu-optionCopy",
@@ -1502,6 +1573,7 @@ function ModelMenu(props) {
1502
1573
  "aria-checked": checked,
1503
1574
  id: `${id}-model-${index}`,
1504
1575
  tabIndex: showSearch ? -1 : 0,
1576
+ disabled: disabled || busy,
1505
1577
  onFocus: () => setHighlightedIndex(index),
1506
1578
  "data-highlighted": index === activeModelIndex ? "" : void 0,
1507
1579
  className: `dsh-model-menu-option dsh-model-menu-modelOption${checked ? " dsh-model-menu-selected" : ""}${index === activeModelIndex ? " dsh-model-menu-optionActive" : ""}`,
@@ -1511,11 +1583,12 @@ function ModelMenu(props) {
1511
1583
  },
1512
1584
  title: choice.label,
1513
1585
  onClick: () => {
1586
+ if (disabled || busy) return;
1514
1587
  onPick({
1515
1588
  provider: choice.provider,
1516
1589
  model: choice.model
1517
1590
  });
1518
- close(true);
1591
+ if (closeOnPick) close(true);
1519
1592
  },
1520
1593
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1521
1594
  className: "dsh-model-menu-optionCopy",
@@ -1531,6 +1604,14 @@ function ModelMenu(props) {
1531
1604
  })
1532
1605
  }, group.provider))]
1533
1606
  }),
1607
+ reasoningSlider && efforts.length > 0 && filteredGroups.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EffortSlider, {
1608
+ label: effortLabel,
1609
+ rows: effortRows,
1610
+ selected: selectedEffort ?? "",
1611
+ disabled,
1612
+ busy,
1613
+ onPick: (value) => onPickEffort?.(value)
1614
+ }) : null,
1534
1615
  filteredGroups.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1535
1616
  className: "dsh-model-menu-empty",
1536
1617
  role: "status",
@@ -1544,10 +1625,12 @@ function ModelMenu(props) {
1544
1625
  type: "button",
1545
1626
  role: "menuitemradio",
1546
1627
  "aria-checked": checked,
1628
+ disabled: disabled || busy,
1547
1629
  className: `dsh-model-menu-option${checked ? " dsh-model-menu-selected" : ""}`,
1548
1630
  onClick: () => {
1631
+ if (disabled || busy) return;
1549
1632
  onPickEffort?.(row.id || void 0);
1550
- close(true);
1633
+ if (closeOnPick) close(true);
1551
1634
  },
1552
1635
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1553
1636
  className: "dsh-model-menu-optionCopy",
@@ -1566,7 +1649,8 @@ function ModelMenu(props) {
1566
1649
  const trigger = (0, react.isValidElement)(anchor) ? (0, react.cloneElement)(anchor, {
1567
1650
  "aria-haspopup": "menu",
1568
1651
  "aria-expanded": open,
1569
- "aria-controls": open ? `${id}-menu` : void 0
1652
+ "aria-controls": open ? `${id}-menu` : void 0,
1653
+ "aria-disabled": disabled || void 0
1570
1654
  }) : anchor;
1571
1655
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1572
1656
  ref: rootRef,
@@ -1574,7 +1658,7 @@ function ModelMenu(props) {
1574
1658
  onKeyDown,
1575
1659
  onBlur,
1576
1660
  onClick: (event) => {
1577
- if (event.defaultPrevented) return;
1661
+ if (disabled || event.defaultPrevented) return;
1578
1662
  const button = event.target instanceof Element ? event.target.closest("button") : null;
1579
1663
  if (!(button instanceof HTMLButtonElement) || button.disabled || !rootRef.current?.contains(button)) return;
1580
1664
  requestOpen();
@@ -1586,6 +1670,102 @@ function ModelMenu(props) {
1586
1670
  });
1587
1671
  }
1588
1672
  /**
1673
+ * Discrete reasoning-effort slider shown under the model list in enhanced mode.
1674
+ *
1675
+ * A native range input (`step=1`) over exactly the rows the caller supplies —
1676
+ * the default row plus each effort the selected model advertises — so it can
1677
+ * never invent a level. It previews locally while dragging or arrowing and
1678
+ * commits only when the gesture ends (pointer up, key up, or blur), so a drag
1679
+ * does not write on every step. The menu stays open, so adjusting the effort
1680
+ * never loses the model selection or the search query.
1681
+ *
1682
+ * The input itself stays the entire interaction and accessibility surface —
1683
+ * keyboard, ARIA, disabled and the native step snapping all come from it —
1684
+ * while the track, gradient fill and thumb below are decorative layers painted
1685
+ * from the `--_p` percentage the preview position sets on the rail. Value
1686
+ * changes only update that custom property, so the CSS transitions animate
1687
+ * between levels without remounting nodes or restarting an animation.
1688
+ */
1689
+ function EffortSlider(props) {
1690
+ const { label, rows, selected, disabled = false, busy = false, onPick } = props;
1691
+ const active = Math.max(0, rows.findIndex((row) => row.id === selected));
1692
+ const [position, setPosition] = (0, react.useState)(active);
1693
+ const positionRef = (0, react.useRef)(position);
1694
+ (0, react.useEffect)(() => {
1695
+ positionRef.current = active;
1696
+ setPosition(active);
1697
+ }, [active]);
1698
+ const preview = (next) => {
1699
+ positionRef.current = next;
1700
+ setPosition(next);
1701
+ };
1702
+ const commit = () => {
1703
+ if (disabled || busy) return;
1704
+ const row = rows[positionRef.current];
1705
+ if (row !== void 0) onPick(row.id === "" ? void 0 : row.id);
1706
+ };
1707
+ const max = Math.max(0, rows.length - 1);
1708
+ const percent = max > 0 ? Math.round(position / max * 100) : 0;
1709
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1710
+ className: "dsh-model-menu-sliderGroup",
1711
+ role: "group",
1712
+ "aria-label": label,
1713
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1714
+ className: "dsh-model-menu-sliderHead",
1715
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1716
+ className: "dsh-model-menu-sliderTitle",
1717
+ children: label
1718
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1719
+ className: "dsh-model-menu-sliderValue",
1720
+ children: rows[position]?.name
1721
+ })]
1722
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1723
+ className: "dsh-model-menu-sliderRail",
1724
+ style: { "--_p": percent },
1725
+ children: [
1726
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1727
+ type: "range",
1728
+ className: "dsh-model-menu-sliderInput",
1729
+ min: 0,
1730
+ max,
1731
+ step: 1,
1732
+ value: position,
1733
+ disabled,
1734
+ "aria-label": label,
1735
+ "aria-disabled": disabled || busy || void 0,
1736
+ "aria-valuetext": rows[position]?.name,
1737
+ onChange: (event) => {
1738
+ const value = Number(event.target.value);
1739
+ if (Number.isFinite(value)) preview(value);
1740
+ },
1741
+ onPointerUp: commit,
1742
+ onKeyUp: commit,
1743
+ onBlur: commit
1744
+ }),
1745
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1746
+ className: "dsh-model-menu-sliderTrack",
1747
+ "aria-hidden": "true",
1748
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1749
+ className: "dsh-model-menu-sliderFill",
1750
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dsh-model-menu-sliderFillBar" })
1751
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1752
+ className: "dsh-model-menu-sliderNodes",
1753
+ children: rows.map((row, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1754
+ "data-covered": index <= position ? "" : void 0,
1755
+ className: "dsh-model-menu-sliderNode",
1756
+ style: { "--_n": max > 0 ? Math.round(index / max * 100) : 0 }
1757
+ }, row.id || "__default"))
1758
+ })]
1759
+ }),
1760
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1761
+ className: "dsh-model-menu-sliderThumb",
1762
+ "aria-hidden": "true"
1763
+ })
1764
+ ]
1765
+ })]
1766
+ });
1767
+ }
1768
+ /**
1589
1769
  * ModelMenu styles, transcribed from the official ModelSelect.module.css (the
1590
1770
  * published dsh-client-ui-model-selection bundle): card capped at
1591
1771
  * min(360px, 100vh - 96px), 34px option cells at 13/18 with 5px 7px padding,
@@ -1644,6 +1824,7 @@ const modelMenuCss = `
1644
1824
  overflow: hidden;
1645
1825
  }
1646
1826
  .dsh-model-menu-status, .dsh-model-menu-empty { color: var(--dsw-alias-label-tertiary); padding: 8px; font-size: 12px; line-height: 18px; }
1827
+ .dsh-model-menu-header { flex: none; padding: 2px 2px 4px; }
1647
1828
  .dsh-model-menu-searchRow { flex-shrink: 0; margin: 2px 0 3px; position: relative; }
1648
1829
  .dsh-model-menu-searchRow .dsh-model-menu-search { border-radius: var(--dsw-radius-md); background: none; border: 0 solid transparent; height: auto; padding: 5px 7px; display: flex; }
1649
1830
  .dsh-model-menu-searchRow .dsh-model-menu-search:focus-within { border-color: transparent; }
@@ -1722,6 +1903,145 @@ const modelMenuCss = `
1722
1903
  .dsh-model-menu-cellLabel { white-space: nowrap; flex: none; }
1723
1904
  .dsh-model-menu-cellValue { text-overflow: ellipsis; white-space: nowrap; text-align: right; min-width: 0; color: var(--dsw-alias-label-tertiary); flex: auto; overflow: hidden; }
1724
1905
  .dsh-model-menu-cellChevron { width: 12px; height: 12px; color: var(--dsw-alias-menu-icon); flex: none; }
1906
+ /* Discrete reasoning-effort slider under the model list (enhanced mode).
1907
+ * The painted control is layered over a fully functional native range input:
1908
+ * the input stays on top, transparent, and owns pointer/keyboard/ARIA, while
1909
+ * the decorative track, fill and thumb render beneath it at pointer-events:
1910
+ * none. Position lives in one custom property (--_p, 0–100) on the rail, so
1911
+ * the only animated work a level change triggers is a clip-path/left
1912
+ * transition — no keyframes, no element churn. */
1913
+ .dsh-model-menu-sliderGroup { border-top: 0.5px solid var(--dsw-alias-border-l2); flex: none; margin-top: 4px; padding: 6px 8px 8px; }
1914
+ .dsh-model-menu-sliderHead { align-items: baseline; justify-content: space-between; gap: 8px; margin: 0 2px 4px; display: flex; }
1915
+ .dsh-model-menu-sliderTitle { color: var(--dsw-alias-label-tertiary); font-size: 12px; line-height: 18px; }
1916
+ .dsh-model-menu-sliderValue { color: var(--dsw-alias-label-primary); font-size: 13px; font-weight: 600; line-height: 20px; transition: color var(--ds-transition-duration) var(--ds-ease-in-out); }
1917
+ .dsh-model-menu-sliderRail { position: relative; direction: ltr; height: 38px; margin-top: 2px; }
1918
+ .dsh-model-menu-sliderRail:has(:disabled) { opacity: 0.45; }
1919
+ .dsh-model-menu-sliderInput {
1920
+ appearance: none;
1921
+ -webkit-appearance: none;
1922
+ box-sizing: border-box;
1923
+ border: 0;
1924
+ cursor: pointer;
1925
+ direction: ltr;
1926
+ opacity: 0;
1927
+ margin: 0;
1928
+ padding: 0;
1929
+ width: 100%;
1930
+ height: 100%;
1931
+ position: absolute;
1932
+ inset: 0;
1933
+ z-index: 1;
1934
+ touch-action: pan-y;
1935
+ }
1936
+ .dsh-model-menu-sliderInput:disabled { cursor: default; }
1937
+ /* The transparent input must keep the painted thumb's geometry: an
1938
+ * appearance:none webkit thumb defaults to 16px, which would shift the
1939
+ * end-point positions 7px off the decorative 30px disc and misplace the
1940
+ * native drag hit target. Track gets the full rail height so the whole band
1941
+ * maps to values identically. */
1942
+ .dsh-model-menu-sliderInput::-webkit-slider-runnable-track { width: 100%; height: 100%; }
1943
+ .dsh-model-menu-sliderInput::-webkit-slider-thumb { appearance: none; -webkit-appearance: none; width: 30px; height: 30px; margin: 0; }
1944
+ .dsh-model-menu-sliderInput::-moz-range-track { width: 100%; height: 100%; }
1945
+ .dsh-model-menu-sliderInput::-moz-range-thumb { width: 30px; height: 30px; margin: 0; border: 0; }
1946
+ .dsh-model-menu-sliderTrack {
1947
+ pointer-events: none;
1948
+ border-radius: 999px;
1949
+ width: 100%;
1950
+ height: 26px;
1951
+ background: var(--dsw-alias-interactive-bg-hover);
1952
+ box-shadow: inset 0 0 0 0.5px var(--dsw-alias-border-l2);
1953
+ position: absolute;
1954
+ inset-inline: 0;
1955
+ top: 50%;
1956
+ transform: translateY(-50%);
1957
+ /* No overflow:hidden — it would clip the fill's drop-shadow glow; the bar's
1958
+ * own clip-path already supplies the rounded reveal shape. */
1959
+ }
1960
+ /* The accent ramp paints the full track width once and is revealed by an
1961
+ * animated clip-path, so raising the effort unwraps more of the spectrum
1962
+ * instead of squashing the same gradient into a narrower bar. The glow lives
1963
+ * on the un-clipped parent: a filter on the clipped element itself would be
1964
+ * clipped away with it. */
1965
+ .dsh-model-menu-sliderFill {
1966
+ display: block;
1967
+ height: 100%;
1968
+ filter: drop-shadow(0 0 5px var(--dsw-alias-state-business-primary));
1969
+ transition: filter var(--ds-transition-duration) var(--ds-ease-in-out);
1970
+ }
1971
+ .dsh-model-menu-sliderInput:hover:not(:disabled) ~ .dsh-model-menu-sliderTrack .dsh-model-menu-sliderFill,
1972
+ .dsh-model-menu-sliderInput:active:not(:disabled) ~ .dsh-model-menu-sliderTrack .dsh-model-menu-sliderFill {
1973
+ filter: drop-shadow(0 0 9px var(--dsw-alias-state-business-primary));
1974
+ }
1975
+ /* The fill stops at the thumb's center, not at the track edge — at a 30px
1976
+ * thumb the end stops sit 15px in from each side, matching the node dots. */
1977
+ .dsh-model-menu-sliderFillBar {
1978
+ display: block;
1979
+ height: 100%;
1980
+ background: linear-gradient(90deg,
1981
+ var(--dsw-alias-state-business-primary),
1982
+ var(--dsw-alias-state-success-primary),
1983
+ var(--dsw-alias-state-warn-primary),
1984
+ var(--dsw-alias-state-error-primary));
1985
+ clip-path: inset(0 calc(100% - 15px - (100% - 30px) * var(--_p, 0) / 100) 0 0 round 999px);
1986
+ transition: clip-path var(--ds-transition-duration) var(--ds-ease-in-out);
1987
+ }
1988
+ /* Stop dots: one per declared row (default + every advertised effort), evenly
1989
+ * spread across the band between the two thumb-center extremes (inset 15px,
1990
+ * i.e. half the 30px thumb). --_n is 0-100 along that band. A covered dot
1991
+ * reads as a pale pin through the gradient; an uncovered one stays a neutral
1992
+ * notch on the empty track. */
1993
+ .dsh-model-menu-sliderNodes {
1994
+ pointer-events: none;
1995
+ display: block;
1996
+ position: absolute;
1997
+ inset-inline: 15px;
1998
+ inset-block: 0;
1999
+ }
2000
+ .dsh-model-menu-sliderNode {
2001
+ pointer-events: none;
2002
+ box-sizing: border-box;
2003
+ width: 4px;
2004
+ height: 4px;
2005
+ border-radius: 50%;
2006
+ background: color-mix(in srgb, var(--dsw-alias-label-secondary) 35%, transparent);
2007
+ position: absolute;
2008
+ left: calc(var(--_n, 0) * 1%);
2009
+ top: 50%;
2010
+ transform: translate(-50%, -50%);
2011
+ transition: background-color var(--ds-transition-duration) var(--ds-ease-in-out),
2012
+ transform var(--ds-transition-duration) var(--ds-ease-in-out);
2013
+ }
2014
+ .dsh-model-menu-sliderNode[data-covered] { background: color-mix(in srgb, var(--dsw-alias-label-primary) 60%, transparent); }
2015
+ .dsh-model-menu-sliderThumb {
2016
+ pointer-events: none;
2017
+ box-sizing: border-box;
2018
+ border-radius: 50%;
2019
+ width: 30px;
2020
+ height: 30px;
2021
+ background: var(--dsw-alias-button-primary-fill);
2022
+ border: 0.5px solid var(--dsw-alias-border-l4);
2023
+ box-shadow: 0 0 0 3px var(--dsw-alias-interactive-bg-hover), var(--dsw-elevation-soft);
2024
+ position: absolute;
2025
+ left: calc((100% - 30px) * var(--_p, 0) / 100);
2026
+ top: 50%;
2027
+ transform: translateY(-50%) scale(0.92);
2028
+ transition: left var(--ds-transition-duration) var(--ds-ease-in-out),
2029
+ transform var(--ds-transition-duration) var(--ds-ease-in-out),
2030
+ box-shadow var(--ds-transition-duration) var(--ds-ease-in-out);
2031
+ }
2032
+ .dsh-model-menu-sliderInput:hover:not(:disabled) ~ .dsh-model-menu-sliderThumb { transform: translateY(-50%) scale(1); }
2033
+ .dsh-model-menu-sliderInput:active:not(:disabled) ~ .dsh-model-menu-sliderThumb {
2034
+ transform: translateY(-50%) scale(1.1);
2035
+ box-shadow: 0 0 0 4px var(--dsw-alias-interactive-bg-hover), 0 0 12px var(--dsw-alias-state-business-primary);
2036
+ }
2037
+ .dsh-model-menu-sliderInput:focus-visible ~ .dsh-model-menu-sliderThumb {
2038
+ box-shadow: 0 0 0 3px var(--dsw-alias-interactive-bg-hover),
2039
+ 0 0 0 calc(3px + var(--dsw-focus-ring-width)) var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));
2040
+ }
2041
+ @media (prefers-reduced-motion: reduce) {
2042
+ .dsh-model-menu-sliderFill, .dsh-model-menu-sliderFillBar, .dsh-model-menu-sliderThumb,
2043
+ .dsh-model-menu-sliderNode, .dsh-model-menu-sliderValue { transition: none; }
2044
+ }
1725
2045
  `;
1726
2046
  //#endregion
1727
2047
  //#region src/styles.ts
@@ -1888,42 +2208,24 @@ ${modelMenuCss}
1888
2208
  }
1889
2209
  .${FREE_CHAT_ROOT}.dsm-item[open] > .dsm-item-summary .dsm-item-chevron { transform: rotate(180deg); }
1890
2210
  .${FREE_CHAT_ROOT} .dsm-item-body { padding: 0 0 16px; }
1891
- /* Settings group body: one quiet reading surface. Rules, not nested wells,
1892
- * separate sections; dependent controls align with the feature they belong to. */
2211
+ /* Settings row bodies: each collapsible row edits its own keys and saves on
2212
+ * change, so the body is one flat reading surface — a feature block per
2213
+ * switch/section, dependent controls indented underneath. */
1893
2214
  .${FREE_CHAT_ROOT} .dsm-settings {
1894
- display: flex;
1895
- flex-direction: column;
1896
- gap: 20px;
1897
- min-width: 0;
1898
- }
1899
- .${FREE_CHAT_ROOT} .dsm-setting {
1900
2215
  display: flex;
1901
2216
  flex-direction: column;
1902
2217
  gap: 12px;
1903
2218
  min-width: 0;
1904
2219
  }
1905
- .${FREE_CHAT_ROOT} .dsm-setting + .dsm-setting {
1906
- padding-top: 20px;
1907
- border-top: 0.5px solid var(--dsw-alias-border-l2);
1908
- }
1909
- .${FREE_CHAT_ROOT} .dsm-setting-header {
2220
+ .${FREE_CHAT_ROOT} .dsm-feature {
1910
2221
  display: flex;
1911
2222
  flex-direction: column;
1912
- gap: 4px;
1913
- }
1914
- .${FREE_CHAT_ROOT} .dsm-ghost-controls {
1915
- display: grid;
1916
- grid-template-columns: minmax(220px, 0.8fr) minmax(0, 1.2fr);
1917
- align-items: start;
1918
- gap: 16px;
1919
- }
1920
- /* SegmentedTabs is 42px tall; its adjacent text controls share that baseline. */
1921
- .${FREE_CHAT_ROOT} .dsm-ghost-controls .dsh-model-menu-trigger,
1922
- .${FREE_CHAT_ROOT} .dsm-ghost-controls .dsm-ghost-input {
1923
- height: 42px;
2223
+ gap: 8px;
2224
+ min-width: 0;
1924
2225
  }
1925
2226
  .${FREE_CHAT_ROOT} .dsm-feature + .dsm-feature {
1926
2227
  margin-top: 4px;
2228
+ padding-top: 4px;
1927
2229
  border-top: 0.5px solid var(--dsw-alias-border-l2);
1928
2230
  }
1929
2231
  .${FREE_CHAT_ROOT} .dsm-feature > .dsh-ui-toggle-row { padding: 8px 0; }
@@ -1931,20 +2233,44 @@ ${modelMenuCss}
1931
2233
  display: flex;
1932
2234
  flex-direction: column;
1933
2235
  gap: 8px;
1934
- padding: 4px 0 8px;
2236
+ padding: 0 0 8px 24px;
1935
2237
  }
1936
2238
  .${FREE_CHAT_ROOT} .dsm-feature-options[data-disabled='true'] .dsh-ui-label,
1937
2239
  .${FREE_CHAT_ROOT} .dsm-feature-options[data-disabled='true'] .dsh-ui-meta {
1938
2240
  color: var(--dsw-alias-label-tertiary);
1939
2241
  }
1940
- .${FREE_CHAT_ROOT} .dsm-compact-options { max-width: calc(50% - 10px); }
1941
- .${FREE_CHAT_ROOT} .dsm-model-field .dsh-model-menu-root,
1942
- .${FREE_CHAT_ROOT} .dsm-model-field .dsh-model-menu-trigger {
1943
- width: 100%;
1944
- max-width: none;
2242
+ /* A feature's model picker: label on the left, the menu trigger on the right;
2243
+ * narrow widths wrap the trigger under the label. */
2244
+ .${FREE_CHAT_ROOT} .dsm-model-field {
2245
+ display: flex;
2246
+ flex-direction: column;
2247
+ gap: 4px;
2248
+ min-width: 0;
2249
+ }
2250
+ .${FREE_CHAT_ROOT} .dsm-model-choice .dsh-model-menu-trigger:disabled {
2251
+ color: var(--dsw-alias-label-tertiary);
2252
+ cursor: not-allowed;
2253
+ }
2254
+ .${FREE_CHAT_ROOT} .dsm-model-choice {
2255
+ display: flex;
2256
+ flex-wrap: wrap;
2257
+ align-items: center;
2258
+ justify-content: space-between;
2259
+ gap: 4px 16px;
1945
2260
  }
1946
- .${FREE_CHAT_ROOT} .dsm-model-field .dsh-model-menu-trigger {
2261
+ .${FREE_CHAT_ROOT} .dsm-model-choice > .dsh-ui-label {
2262
+ flex: 1 1 120px;
2263
+ min-width: 120px;
2264
+ }
2265
+ .${FREE_CHAT_ROOT} .dsm-model-choice .dsh-model-menu-root {
2266
+ flex: 1 1 200px;
2267
+ min-width: min(200px, 100%);
2268
+ max-width: 280px;
2269
+ }
2270
+ .${FREE_CHAT_ROOT} .dsm-model-choice .dsh-model-menu-trigger {
2271
+ display: flex;
1947
2272
  justify-content: space-between;
2273
+ width: 100%;
1948
2274
  height: 32px;
1949
2275
  padding: 0 10px;
1950
2276
  border: 0.5px solid var(--dsw-alias-border-l4);
@@ -1952,13 +2278,13 @@ ${modelMenuCss}
1952
2278
  background: var(--dsw-alias-bg-layer-3);
1953
2279
  color: var(--dsw-alias-label-primary);
1954
2280
  }
1955
- .${FREE_CHAT_ROOT} .dsm-model-field .dsh-model-menu-triggerLabel {
2281
+ .${FREE_CHAT_ROOT} .dsm-model-choice .dsh-model-menu-triggerLabel {
1956
2282
  flex: 1;
1957
- text-align: left;
1958
- }
1959
- .${FREE_CHAT_ROOT} .dsm-settings-footer .dsh-ui-notice--error {
1960
- flex: 0 0 100%;
1961
2283
  min-width: 0;
2284
+ overflow: hidden;
2285
+ text-align: left;
2286
+ text-overflow: ellipsis;
2287
+ white-space: nowrap;
1962
2288
  }
1963
2289
  .${FREE_CHAT_ROOT} .dsm-minutes-control {
1964
2290
  display: flex;
@@ -1966,10 +2292,7 @@ ${modelMenuCss}
1966
2292
  gap: 8px;
1967
2293
  }
1968
2294
  .${FREE_CHAT_ROOT} .dsm-minutes { width: 88px; }
1969
- @media (max-width: 560px) {
1970
- .${FREE_CHAT_ROOT} .dsm-ghost-controls { grid-template-columns: minmax(0, 1fr); }
1971
- .${FREE_CHAT_ROOT} .dsm-compact-options { max-width: none; }
1972
- }
2295
+ .${FREE_CHAT_ROOT} .dsm-text-input { max-width: 320px; }
1973
2296
  /* Welcome-back card: one flat full-width row at the latest turn's tail in the
1974
2297
  * chat flow — a single bordered surface with the recap laid out inline, no
1975
2298
  * nested box. */
@@ -3225,6 +3548,19 @@ async function callContinueSuggest(host, sessionId, knownSeq, signal) {
3225
3548
  ...value?.fallback === true ? { fallback: true } : {}
3226
3549
  };
3227
3550
  }
3551
+ /**
3552
+ * A host that predates the agent-action flags returns a config view without
3553
+ * them, and a malformed one may carry a non-boolean. Both normalize to
3554
+ * false: an absent or unusable flag must never read as granted. Only a real
3555
+ * boolean true keeps the permission on.
3556
+ */
3557
+ function normalizeConfigView(view) {
3558
+ return {
3559
+ ...view,
3560
+ allowSelfFork: view.allowSelfFork === true,
3561
+ allowSendMessage: view.allowSendMessage === true
3562
+ };
3563
+ }
3228
3564
  function createPresenceConfigModel(host, delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) {
3229
3565
  const events = store();
3230
3566
  const lifetime = new AbortController();
@@ -3235,6 +3571,8 @@ function createPresenceConfigModel(host, delay = (ms) => new Promise((resolve) =
3235
3571
  let failure;
3236
3572
  let snapshot = build();
3237
3573
  let readGeneration = 0;
3574
+ /** Several settings rows share this model; collapse concurrent reads into one call. */
3575
+ let inflightLoad;
3238
3576
  function build() {
3239
3577
  return {
3240
3578
  phase,
@@ -3260,23 +3598,29 @@ function createPresenceConfigModel(host, delay = (ms) => new Promise((resolve) =
3260
3598
  return view[key] === value;
3261
3599
  });
3262
3600
  }
3601
+ async function doLoad() {
3602
+ const generation = ++readGeneration;
3603
+ try {
3604
+ const view = await callPresence(host, CONFIG_READ_ENDPOINT, {}, lifetime.signal);
3605
+ if (lifetime.signal.aborted || generation !== readGeneration) return;
3606
+ values = normalizeConfigView(view);
3607
+ phase = "ready";
3608
+ failure = void 0;
3609
+ } catch (error) {
3610
+ if (lifetime.signal.aborted || generation !== readGeneration) return;
3611
+ phase = "unavailable";
3612
+ failure = readFailure(error);
3613
+ }
3614
+ publish();
3615
+ }
3263
3616
  return {
3264
3617
  subscribe: events.subscribe,
3265
3618
  getSnapshot: () => snapshot,
3266
- async load() {
3267
- const generation = ++readGeneration;
3268
- try {
3269
- const view = await callPresence(host, CONFIG_READ_ENDPOINT, {}, lifetime.signal);
3270
- if (lifetime.signal.aborted || generation !== readGeneration) return;
3271
- values = view;
3272
- phase = "ready";
3273
- failure = void 0;
3274
- } catch (error) {
3275
- if (lifetime.signal.aborted || generation !== readGeneration) return;
3276
- phase = "unavailable";
3277
- failure = readFailure(error);
3278
- }
3279
- publish();
3619
+ load() {
3620
+ inflightLoad ??= doLoad().finally(() => {
3621
+ inflightLoad = void 0;
3622
+ });
3623
+ return inflightLoad;
3280
3624
  },
3281
3625
  async save(patch) {
3282
3626
  if (saving || lifetime.signal.aborted) return;
@@ -3293,14 +3637,14 @@ function createPresenceConfigModel(host, delay = (ms) => new Promise((resolve) =
3293
3637
  await delay(400);
3294
3638
  let view;
3295
3639
  try {
3296
- view = await callPresence(host, CONFIG_READ_ENDPOINT, {}, lifetime.signal);
3640
+ view = normalizeConfigView(await callPresence(host, CONFIG_READ_ENDPOINT, {}, lifetime.signal));
3297
3641
  } catch (error) {
3298
3642
  if (lifetime.signal.aborted || attempt === 9) throw error;
3299
3643
  continue;
3300
3644
  }
3301
3645
  if (same(view, patch)) {
3302
3646
  ++readGeneration;
3303
- values = view;
3647
+ values = normalizeConfigView(view);
3304
3648
  phase = "ready";
3305
3649
  saved = true;
3306
3650
  saving = false;
@@ -3812,6 +4156,95 @@ function registerSessionSettingsItem(ctx, item, component) {
3812
4156
  };
3813
4157
  }
3814
4158
  //#endregion
4159
+ //#region ../dsh-plugin-kit/src/model-picker.tsx
4160
+ /**
4161
+ * Optional shared model picker (`klarkxy.model-picker` Factory).
4162
+ *
4163
+ * `@klarkxy/dsh-model-picker`, when installed, registers one root-scoped
4164
+ * Component Factory under `MODEL_PICKER_FACTORY`: an enhanced model menu with
4165
+ * a searchable provider-grouped list and a reasoning-effort slider, built on
4166
+ * the same ModelMenu recipe. Consumers keep their own ModelMenu as the
4167
+ * `fallback`; when no definition is registered the renderer resolves that
4168
+ * fallback verbatim, so the shared picker is strictly opt-in and every saved
4169
+ * value keeps its existing write path.
4170
+ *
4171
+ * `renderFactorySlot` is a framework-composed prop, never a context: the
4172
+ * component a plugin registers into any slot receives it automatically, but
4173
+ * the plugin must pass it down to whichever child renders the menu (or to the
4174
+ * render callback handed to registerSessionSettingsItem).
4175
+ *
4176
+ * The Factory manages its own open state; a consumer's `open`/`onOpenChange`
4177
+ * pair belongs to the fallback menu only and must not be fed to both.
4178
+ */
4179
+ /**
4180
+ * The trigger every consumer used to hand its own ModelMenu, rebuilt as the
4181
+ * factory's `anchor` local component: identical classes and chevron, the menu
4182
+ * state coming from the occurrence's props instead of local open state.
4183
+ *
4184
+ * `ariaLabel` and `title` are the whole accessible name and tooltip (the
4185
+ * caller formats "field: current route" itself because the factory's resolved
4186
+ * caption may intentionally be a short name). The component identity stays
4187
+ * put across label changes — a new function would unmount the trigger, and
4188
+ * its cleanup would close a menu that `closeOnPick={false}` is keeping open.
4189
+ * The latest label, title, and `onClose` are read from refs.
4190
+ */
4191
+ function useModelMenuAnchor(ariaLabel, title, onClose) {
4192
+ const closeRef = (0, react.useRef)(onClose);
4193
+ const labelRef = (0, react.useRef)(ariaLabel);
4194
+ const titleRef = (0, react.useRef)(title);
4195
+ closeRef.current = onClose;
4196
+ labelRef.current = ariaLabel;
4197
+ titleRef.current = title;
4198
+ return (0, react.useMemo)(() => {
4199
+ const ModelPickerTrigger = (props) => {
4200
+ (0, react.useEffect)(() => () => closeRef.current?.(), []);
4201
+ const record = props;
4202
+ const injected = {};
4203
+ for (const [key, value] of Object.entries(record)) if (key.startsWith("aria-")) injected[key] = value;
4204
+ const extraClass = typeof record.className === "string" ? ` ${record.className}` : "";
4205
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4206
+ type: "button",
4207
+ className: `dsh-model-menu-trigger${extraClass}`,
4208
+ disabled: props.disabled,
4209
+ "aria-haspopup": "menu",
4210
+ "aria-expanded": props.open,
4211
+ ...injected,
4212
+ "aria-label": labelRef.current,
4213
+ title: titleRef.current,
4214
+ children: [
4215
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4216
+ className: "dsh-model-menu-triggerLabel",
4217
+ children: props.label
4218
+ }),
4219
+ props.effortLabel ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4220
+ className: "dsh-model-menu-triggerEffort",
4221
+ children: props.effortLabel
4222
+ }) : null,
4223
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${props.open ? " dsh-model-menu-chevronOpen" : ""}` })
4224
+ ]
4225
+ });
4226
+ };
4227
+ return ModelPickerTrigger;
4228
+ }, []);
4229
+ }
4230
+ /**
4231
+ * Render the shared model picker while it is registered; `options.fallback`
4232
+ * whenever it is not (plugin absent, disabled, or unloaded).
4233
+ *
4234
+ * `options.anchor` is optional: the factory ships a default trigger, so only
4235
+ * consumers that need their own chrome (custom aria-label, tooltip, class)
4236
+ * pass one built with {@link useModelMenuAnchor}. The returned node is never
4237
+ * assumed to be the factory's own output — a defensive `?? fallback` covers a
4238
+ * renderer that yields null for an unregistered definition.
4239
+ */
4240
+ function renderModelPicker(renderFactorySlot, props, options) {
4241
+ if (typeof renderFactorySlot !== "function") return options.fallback;
4242
+ return renderFactorySlot("klarkxy.model-picker", props, {
4243
+ ...options.anchor ? { slots: { ["anchor"]: options.anchor } } : {},
4244
+ fallback: options.fallback
4245
+ }) ?? options.fallback;
4246
+ }
4247
+ //#endregion
3815
4248
  //#region ../dsh-plugin-kit/src/model-settings-client.ts
3816
4249
  /** This stream carries invalidations only; ready is never a settings snapshot. */
3817
4250
  function parseModelSettingsChangeFrame(value) {
@@ -4057,7 +4490,7 @@ async function callAuxiliaryRpc(client, endpoint, payload = {}) {
4057
4490
  * The shared card. All state comes from the shared host service over the
4058
4491
  * plugin-kit RPC channel; the card only renders the stored row.
4059
4492
  */
4060
- function AuxiliaryModelCard({ client, subscribe }) {
4493
+ function AuxiliaryModelCard({ client, subscribe, renderFactorySlot }) {
4061
4494
  const language = useAuxiliaryLocale(client);
4062
4495
  const text = auxiliaryCopy[language];
4063
4496
  const [settings, setSettings] = (0, react.useState)();
@@ -4178,6 +4611,17 @@ function AuxiliaryModelCard({ client, subscribe }) {
4178
4611
  refreshCatalog
4179
4612
  ]);
4180
4613
  const locked = busy;
4614
+ const current = settings?.model ?? {
4615
+ provider: "",
4616
+ model: ""
4617
+ };
4618
+ const follow = !current.provider || !current.model;
4619
+ const selected = follow ? void 0 : choices.find((item) => item.provider === current.provider && item.model === current.model);
4620
+ const efforts = follow ? [] : modelMenuEffortOptions(selected, current.reasoningEffort);
4621
+ const currentLabel = follow ? text.modelDefault : selected?.label ?? `${current.provider} / ${current.model}`;
4622
+ const selectedEffort = efforts.find((item) => item.id === (current.reasoningEffort || ""));
4623
+ const effortName = selectedEffort ? modelMenuEffortLabel(selectedEffort, language) : void 0;
4624
+ const anchor = useModelMenuAnchor(`${text.model}: ${settings ? currentLabel : ""}`, void 0, () => setMenuOpen(false));
4181
4625
  if (!settings) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
4182
4626
  className: "ams-root dsh-ui-card",
4183
4627
  "data-testid": "auxiliary-model-settings",
@@ -4216,16 +4660,6 @@ function AuxiliaryModelCard({ client, subscribe }) {
4216
4660
  })
4217
4661
  ]
4218
4662
  });
4219
- const current = settings.model ?? {
4220
- provider: "",
4221
- model: ""
4222
- };
4223
- const follow = !current.provider || !current.model;
4224
- const selected = follow ? void 0 : choices.find((item) => item.provider === current.provider && item.model === current.model);
4225
- const efforts = follow ? [] : modelMenuEffortOptions(selected, current.reasoningEffort);
4226
- const currentLabel = follow ? text.modelDefault : selected?.label ?? `${current.provider} / ${current.model}`;
4227
- const selectedEffort = efforts.find((item) => item.id === (current.reasoningEffort || ""));
4228
- const effortName = selectedEffort ? modelMenuEffortLabel(selectedEffort, language) : void 0;
4229
4663
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
4230
4664
  className: "ams-root dsh-ui-card",
4231
4665
  "data-testid": "auxiliary-model-settings",
@@ -4249,28 +4683,12 @@ function AuxiliaryModelCard({ client, subscribe }) {
4249
4683
  state: catalogState,
4250
4684
  locale: language
4251
4685
  }),
4252
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelMenu, {
4686
+ renderModelPicker(renderFactorySlot, {
4253
4687
  locale: language,
4254
- open: menuOpen && !locked,
4688
+ disabled: locked,
4689
+ busy: locked,
4255
4690
  onOpenChange: (next) => setMenuOpen(next && !locked),
4256
4691
  listClassName: "ams-root ams-menu-portal",
4257
- anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4258
- type: "button",
4259
- className: "dsh-model-menu-trigger",
4260
- disabled: locked,
4261
- "aria-label": `${text.model}: ${currentLabel}`,
4262
- children: [
4263
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4264
- className: "dsh-model-menu-triggerLabel",
4265
- children: currentLabel
4266
- }),
4267
- !follow && efforts.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4268
- className: "dsh-model-menu-triggerEffort",
4269
- children: effortName ?? text.effortDefault
4270
- }) : null,
4271
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${menuOpen && !locked ? " dsh-model-menu-chevronOpen" : ""}` })
4272
- ]
4273
- }),
4274
4692
  choices,
4275
4693
  selected: follow ? void 0 : {
4276
4694
  provider: current.provider,
@@ -4293,9 +4711,61 @@ function AuxiliaryModelCard({ client, subscribe }) {
4293
4711
  model: current.model,
4294
4712
  ...effort ? { reasoningEffort: effort } : {}
4295
4713
  }),
4714
+ triggerLabel: currentLabel,
4715
+ triggerEffortLabel: !follow && efforts.length ? effortName ?? text.effortDefault : void 0,
4296
4716
  modelLabel: text.model,
4297
4717
  modelValue: currentLabel,
4298
4718
  defaultEffortLabel: text.effortDefault
4719
+ }, {
4720
+ anchor,
4721
+ fallback: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelMenu, {
4722
+ locale: language,
4723
+ open: menuOpen && !locked,
4724
+ onOpenChange: (next) => setMenuOpen(next && !locked),
4725
+ listClassName: "ams-root ams-menu-portal",
4726
+ anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4727
+ type: "button",
4728
+ className: "dsh-model-menu-trigger",
4729
+ disabled: locked,
4730
+ "aria-label": `${text.model}: ${currentLabel}`,
4731
+ children: [
4732
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4733
+ className: "dsh-model-menu-triggerLabel",
4734
+ children: currentLabel
4735
+ }),
4736
+ !follow && efforts.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4737
+ className: "dsh-model-menu-triggerEffort",
4738
+ children: effortName ?? text.effortDefault
4739
+ }) : null,
4740
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${menuOpen && !locked ? " dsh-model-menu-chevronOpen" : ""}` })
4741
+ ]
4742
+ }),
4743
+ choices,
4744
+ selected: follow ? void 0 : {
4745
+ provider: current.provider,
4746
+ model: current.model
4747
+ },
4748
+ onPick: (picked) => save({
4749
+ provider: picked.provider,
4750
+ model: picked.model
4751
+ }),
4752
+ leading: [{
4753
+ id: "default",
4754
+ label: text.modelDefault,
4755
+ selected: follow
4756
+ }],
4757
+ onPickLeading: () => save(null),
4758
+ efforts,
4759
+ selectedEffort: current.reasoningEffort || "",
4760
+ onPickEffort: (effort) => save(follow ? null : {
4761
+ provider: current.provider,
4762
+ model: current.model,
4763
+ ...effort ? { reasoningEffort: effort } : {}
4764
+ }),
4765
+ modelLabel: text.model,
4766
+ modelValue: currentLabel,
4767
+ defaultEffortLabel: text.effortDefault
4768
+ })
4299
4769
  }),
4300
4770
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4301
4771
  className: "dsh-ui-hint",
@@ -4397,9 +4867,10 @@ function acquireAuxiliaryModelCard(root, ctx) {
4397
4867
  let unprovide;
4398
4868
  const watchLease = acquireWatchCoordinator(root).mount();
4399
4869
  const subscriber = createModelSettingsSubscriber(ctx.root, AUXILIARY_MODEL_PACKAGE, AUXILIARY_MODEL_CHANGES_IDENTITY, (ns) => ns === AUXILIARY_MODEL_NS);
4400
- const render = () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AuxiliaryModelCard, {
4870
+ const render = (props) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AuxiliaryModelCard, {
4401
4871
  client: root,
4402
- subscribe: subscriber.subscribe
4872
+ subscribe: subscriber.subscribe,
4873
+ renderFactorySlot: props.renderFactorySlot
4403
4874
  });
4404
4875
  const service = {
4405
4876
  retain() {
@@ -4696,24 +5167,6 @@ function WelcomeBackCard(props) {
4696
5167
  }
4697
5168
  //#endregion
4698
5169
  //#region src/settings-panel.tsx
4699
- function toDraft(values) {
4700
- const recapEnabled = values.recapMinutes > 0;
4701
- const compactEnabled = values.compactMinutes > 0;
4702
- return {
4703
- recapEnabled,
4704
- recapMinutes: String(recapEnabled ? values.recapMinutes : 15),
4705
- compactEnabled,
4706
- compactMinutes: String(compactEnabled ? values.compactMinutes : 60),
4707
- continueText: values.continueText,
4708
- continueMode: values.continueMode,
4709
- autoContinueMode: values.autoContinueMode,
4710
- autoContinueMax: String(values.autoContinueMax),
4711
- model: values.model === null ? "" : modelRouteKey(values.model.provider, values.model.model),
4712
- ghostModel: values.ghostModel === null ? "" : modelRouteKey(values.ghostModel.provider, values.ghostModel.model),
4713
- judgeModel: values.judgeModel === null ? "" : modelRouteKey(values.judgeModel.provider, values.judgeModel.model),
4714
- handoffModel: values.handoffModel === null ? "" : modelRouteKey(values.handoffModel.provider, values.handoffModel.model)
4715
- };
4716
- }
4717
5170
  /** Enabled thresholds are whole minutes of at least 1. 0 stays the stored "off" value and is not a valid enabled draft. */
4718
5171
  function minutesOf(text) {
4719
5172
  if (!/^[1-9]\d*$/.test(text.trim())) return void 0;
@@ -4726,624 +5179,649 @@ function autoMaxOf(text) {
4726
5179
  const value = Number.parseInt(text.trim(), 10);
4727
5180
  return Number.isSafeInteger(value) && value >= 1 && value <= 10 ? value : void 0;
4728
5181
  }
4729
- function sameDraft(left, right) {
4730
- return left.recapEnabled === right.recapEnabled && left.recapMinutes === right.recapMinutes && left.compactEnabled === right.compactEnabled && left.compactMinutes === right.compactMinutes && left.continueText === right.continueText && left.continueMode === right.continueMode && left.autoContinueMode === right.autoContinueMode && left.autoContinueMax === right.autoContinueMax && left.model === right.model && left.ghostModel === right.ghostModel && left.judgeModel === right.judgeModel && left.handoffModel === right.handoffModel;
4731
- }
4732
5182
  const missingCatalog = async () => {
4733
5183
  throw new Error("Model catalog service unavailable");
4734
5184
  };
4735
- /** The bundle config page: edits the cordis plugin config through the host config editor. */
4736
- function SessionManagerSettings(props) {
4737
- const snapshot = (0, react.useSyncExternalStore)(props.model.subscribe, props.model.getSnapshot, props.model.getSnapshot);
4738
- const [draft, setDraft] = (0, react.useState)(void 0);
4739
- const catalog = useModelCatalog(props.modelCatalog ?? missingCatalog, { subscribe: props.subscribeCatalog });
4740
- const [menuOpen, setMenuOpen] = (0, react.useState)(false);
4741
- const [ghostMenuOpen, setGhostMenuOpen] = (0, react.useState)(false);
4742
- const [judgeMenuOpen, setJudgeMenuOpen] = (0, react.useState)(false);
4743
- const [handoffMenuOpen, setHandoffMenuOpen] = (0, react.useState)(false);
4744
- const [externalChange, setExternalChange] = (0, react.useState)(false);
4745
- const dirty = (0, react.useRef)(false);
4746
- const baseline = (0, react.useRef)();
4747
- const tabsId = (0, react.useId)();
4748
- const copy = (key) => uiText(props.locale, key);
4749
- const edit = (next) => {
4750
- dirty.current = true;
4751
- setDraft(next);
5185
+ function copy(locale, key) {
5186
+ return uiText(locale, key);
5187
+ }
5188
+ const writers = /* @__PURE__ */ new WeakMap();
5189
+ function writerFor(model) {
5190
+ let writer = writers.get(model);
5191
+ if (writer !== void 0) return writer;
5192
+ let queue;
5193
+ const flush = () => {
5194
+ if (queue === void 0 || model.getSnapshot().saving) return;
5195
+ const patch = queue;
5196
+ queue = void 0;
5197
+ model.save(patch).finally(flush);
4752
5198
  };
5199
+ writer = {
5200
+ lastRow: void 0,
5201
+ send(patch, row) {
5202
+ writer.lastRow = row;
5203
+ queue = {
5204
+ ...queue,
5205
+ ...patch
5206
+ };
5207
+ flush();
5208
+ }
5209
+ };
5210
+ writers.set(model, writer);
5211
+ return writer;
5212
+ }
5213
+ function useConfigPatch(model) {
5214
+ const row = (0, react.useRef)(Symbol("row")).current;
5215
+ const writer = writerFor(model);
5216
+ return {
5217
+ send: (0, react.useCallback)((patch) => writer.send(patch, row), [writer, row]),
5218
+ owns: () => writer.lastRow === row
5219
+ };
5220
+ }
5221
+ /**
5222
+ * One controlled text/number input bound to a stored string. The draft follows
5223
+ * external writes only while untouched — a focused, edited draft is never
5224
+ * wiped by a settings reload. The commit runs on blur and Enter: valid input
5225
+ * normalizes and saves when it differs; invalid input keeps the draft and its
5226
+ * inline error and is never written.
5227
+ */
5228
+ function useTextSetting(props) {
5229
+ const [draft, setDraft] = (0, react.useState)(props.stored);
5230
+ const seen = (0, react.useRef)(props.stored);
5231
+ const touched = (0, react.useRef)(false);
4753
5232
  (0, react.useEffect)(() => {
4754
- if (snapshot.phase === "loading") props.model.load();
4755
- }, [props.model, snapshot.phase]);
4756
- (0, react.useEffect)(() => {
4757
- if (snapshot.values === void 0) return;
4758
- const next = toDraft(snapshot.values);
5233
+ if (props.stored === seen.current) return;
5234
+ seen.current = props.stored;
4759
5235
  setDraft((current) => {
4760
- if (dirty.current && (current === void 0 || !sameDraft(current, next))) return current;
4761
- dirty.current = false;
4762
- baseline.current = next;
4763
- return next;
5236
+ if (touched.current && current !== props.stored) return current;
5237
+ touched.current = false;
5238
+ return props.stored;
4764
5239
  });
4765
- setExternalChange(dirty.current);
4766
- }, [snapshot.values]);
4767
- (0, react.useEffect)(() => props.subscribeSettings?.(() => {
4768
- props.model.load();
4769
- catalog.refresh();
4770
- }) ?? (() => {}), [
4771
- props.model,
4772
- props.subscribeSettings,
4773
- catalog.refresh
5240
+ }, [props.stored]);
5241
+ const commit = (0, react.useCallback)(() => {
5242
+ const parsed = props.parse(draft);
5243
+ if (parsed === void 0) return;
5244
+ setDraft(parsed.draft);
5245
+ if (parsed.draft === seen.current) {
5246
+ touched.current = false;
5247
+ return;
5248
+ }
5249
+ props.send(parsed.value);
5250
+ }, [
5251
+ draft,
5252
+ props.parse,
5253
+ props.send
4774
5254
  ]);
4775
- if (snapshot.phase === "loading") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4776
- className: `${FREE_CHAT_ROOT} dsh-ui-loading`,
4777
- children: copy("settingsLoading")
5255
+ return {
5256
+ draft,
5257
+ invalid: props.parse(draft) === void 0,
5258
+ inputProps: {
5259
+ value: draft,
5260
+ onChange: (event) => {
5261
+ touched.current = true;
5262
+ setDraft(event.target.value);
5263
+ },
5264
+ onBlur: commit,
5265
+ onKeyDown: (event) => {
5266
+ if (event.key === "Enter") commit();
5267
+ }
5268
+ }
5269
+ };
5270
+ }
5271
+ /** Per-row write status: one line under the controls, only while it has something to say. */
5272
+ function SaveStatus(props) {
5273
+ const { snapshot, locale } = props;
5274
+ if (!props.owns()) return null;
5275
+ if (snapshot.saving) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5276
+ className: "dsh-ui-notice",
5277
+ role: "status",
5278
+ children: copy(locale, "settingsSaving")
4778
5279
  });
4779
- if (snapshot.phase === "unavailable" || draft === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4780
- className: `${FREE_CHAT_ROOT} dsh-ui-error`,
5280
+ if (snapshot.failure) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5281
+ className: "dsh-ui-notice dsh-ui-notice--error",
4781
5282
  role: "alert",
4782
- children: snapshot.failure ? failureText(props.locale, snapshot.failure) : copy("settingsUnavailable")
5283
+ children: failureText(locale, snapshot.failure)
4783
5284
  });
4784
- const selectedRoute = parseModelRouteKey(draft.model);
4785
- const ghostRoute = parseModelRouteKey(draft.ghostModel);
4786
- const judgeRoute = parseModelRouteKey(draft.judgeModel);
4787
- const handoffRoute = parseModelRouteKey(draft.handoffModel);
4788
- const selectableChoices = parseModelMenuChoices(catalog.catalog);
4789
- for (const bound of [
4790
- selectedRoute,
4791
- ghostRoute,
4792
- judgeRoute,
4793
- handoffRoute
4794
- ]) if (bound && !selectableChoices.some((choice) => choice.provider === bound.provider && choice.model === bound.model)) selectableChoices.push({
4795
- provider: bound.provider,
4796
- model: bound.model,
4797
- label: `${bound.provider} / ${bound.model}`,
4798
- efforts: []
5285
+ if (snapshot.saved) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5286
+ className: "dsh-ui-notice",
5287
+ role: "status",
5288
+ children: copy(locale, "settingsSaved")
4799
5289
  });
4800
- const routeLabel = (key) => selectableChoices.find((choice) => modelRouteKey(choice.provider, choice.model) === key)?.label ?? copy("modelFollowOption");
4801
- const selectedLabel = routeLabel(draft.model);
4802
- const ghostLabel = routeLabel(draft.ghostModel);
4803
- const judgeLabel = routeLabel(draft.judgeModel);
4804
- const handoffLabel = routeLabel(draft.handoffModel);
4805
- const recapMinutes = draft.recapEnabled ? minutesOf(draft.recapMinutes) : 0;
4806
- const compactMinutes = draft.compactEnabled ? minutesOf(draft.compactMinutes) : 0;
4807
- const autoContinueMax = autoMaxOf(draft.autoContinueMax);
4808
- const recapInvalid = recapMinutes === void 0;
4809
- const compactInvalid = compactMinutes === void 0;
4810
- const autoMaxInvalid = autoContinueMax === void 0;
4811
- const invalid = recapInvalid || compactInvalid || autoMaxInvalid;
4812
- const fixedPanelId = `${tabsId}-fixed-panel`;
4813
- const autoPanelId = `${tabsId}-auto-panel`;
4814
- const save = () => {
4815
- if (invalid) return;
4816
- const routePatch = (key) => {
4817
- if (draft[key] === baseline.current?.[key]) return {};
4818
- const parsed = parseModelRouteKey(draft[key]);
4819
- return { [key]: parsed === void 0 ? null : {
4820
- provider: parsed.provider,
4821
- model: parsed.model
4822
- } };
4823
- };
4824
- setExternalChange(false);
4825
- props.model.save({
4826
- recapMinutes,
4827
- compactMinutes,
4828
- continueText: draft.continueText,
4829
- continueMode: draft.continueMode,
4830
- autoContinueMode: draft.autoContinueMode,
4831
- autoContinueMax,
4832
- ...routePatch("model"),
4833
- ...routePatch("ghostModel"),
4834
- ...routePatch("judgeModel"),
4835
- ...routePatch("handoffModel")
4836
- });
4837
- };
4838
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4839
- className: `${FREE_CHAT_ROOT} dsh-ui-page`,
4840
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4841
- className: "dsm-settings",
5290
+ return null;
5291
+ }
5292
+ /** Shared loading/unavailable body: every row reads the same config snapshot. */
5293
+ function useRowGate(props) {
5294
+ const snapshot = (0, react.useSyncExternalStore)(props.model.subscribe, props.model.getSnapshot, props.model.getSnapshot);
5295
+ (0, react.useEffect)(() => {
5296
+ if (snapshot.phase === "loading") props.model.load();
5297
+ }, [props.model, snapshot.phase]);
5298
+ if (snapshot.phase === "loading") return {
5299
+ snapshot,
5300
+ gate: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5301
+ className: "dsh-ui-loading",
5302
+ children: copy(props.locale, "settingsLoading")
5303
+ })
5304
+ };
5305
+ if (snapshot.phase === "unavailable" || snapshot.values === void 0) return {
5306
+ snapshot,
5307
+ gate: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5308
+ className: "dsh-ui-error",
5309
+ role: "alert",
5310
+ children: snapshot.failure ? failureText(props.locale, snapshot.failure) : copy(props.locale, "settingsUnavailable")
5311
+ })
5312
+ };
5313
+ return {
5314
+ snapshot,
5315
+ values: snapshot.values
5316
+ };
5317
+ }
5318
+ /** 输入框建议 / Composer suggestion row. */
5319
+ function ComposerSettings(props) {
5320
+ const { snapshot, values, gate } = useRowGate(props);
5321
+ const { send, owns } = useConfigPatch(props.model);
5322
+ const tabsId = (0, react.useId)();
5323
+ const field = useTextSetting({
5324
+ stored: values?.continueText ?? "",
5325
+ parse: (draft) => ({
5326
+ value: draft.trim(),
5327
+ draft: draft.trim()
5328
+ }),
5329
+ send: (0, react.useCallback)((value) => send({ continueText: value }), [send])
5330
+ });
5331
+ if (gate !== void 0 || values === void 0) return gate ?? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, {});
5332
+ const fixedPanelId = `${tabsId}-fixed-panel`;
5333
+ const autoPanelId = `${tabsId}-auto-panel`;
5334
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5335
+ className: "dsm-settings",
5336
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5337
+ className: "dsm-feature",
4842
5338
  children: [
4843
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
4844
- className: "dsm-setting",
4845
- "aria-labelledby": `${tabsId}-ghost-heading`,
4846
- children: [
4847
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
4848
- className: "dsm-setting-header",
4849
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
4850
- id: `${tabsId}-ghost-heading`,
4851
- className: "dsh-ui-title",
4852
- children: copy("settingsMode")
4853
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4854
- className: "dsh-ui-hint",
4855
- children: copy("settingsModeHint")
4856
- })]
4857
- }),
4858
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4859
- className: "dsm-ghost-controls",
4860
- children: [
4861
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4862
- className: "dsh-ui-field",
4863
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4864
- className: "dsh-ui-label",
4865
- children: copy("continueSourceLabel")
4866
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.SegmentedTabs, {
4867
- label: copy("continueSourceLabel"),
4868
- items: [{
4869
- id: `${tabsId}-fixed`,
4870
- panelId: fixedPanelId,
4871
- value: "fixed",
4872
- label: copy("modeFixed")
4873
- }, {
4874
- id: `${tabsId}-auto`,
4875
- panelId: autoPanelId,
4876
- value: "auto",
4877
- label: copy("modeAuto")
4878
- }],
4879
- value: draft.continueMode,
4880
- onChange: (value) => edit({
4881
- ...draft,
4882
- continueMode: value
4883
- })
4884
- })]
4885
- }),
4886
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4887
- role: "tabpanel",
4888
- id: fixedPanelId,
4889
- "aria-labelledby": `${tabsId}-fixed`,
4890
- hidden: draft.continueMode !== "fixed",
4891
- children: draft.continueMode === "fixed" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
4892
- className: "dsh-ui-field",
4893
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4894
- className: "dsh-ui-label",
4895
- children: copy("continueTextLabel")
4896
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
4897
- className: "dsm-ghost-input",
4898
- value: draft.continueText,
4899
- placeholder: copy("continueTextPlaceholder"),
4900
- onChange: (event) => edit({
4901
- ...draft,
4902
- continueText: event.target.value
4903
- })
4904
- })]
4905
- }) : null
4906
- }),
4907
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4908
- role: "tabpanel",
4909
- id: autoPanelId,
4910
- "aria-labelledby": `${tabsId}-auto`,
4911
- hidden: draft.continueMode !== "auto",
4912
- children: draft.continueMode === "auto" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4913
- className: "dsh-ui-field dsm-model-field",
4914
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4915
- className: "dsh-ui-label",
4916
- children: copy("ghostModelLabel")
4917
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelMenu, {
4918
- locale: props.locale,
4919
- open: ghostMenuOpen,
4920
- onOpenChange: setGhostMenuOpen,
4921
- listClassName: FREE_CHAT_ROOT,
4922
- anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4923
- type: "button",
4924
- className: "dsh-model-menu-trigger",
4925
- "aria-label": `${copy("ghostModelLabel")}: ${ghostLabel}`,
4926
- "aria-haspopup": "menu",
4927
- "aria-expanded": ghostMenuOpen,
4928
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4929
- className: "dsh-model-menu-triggerLabel",
4930
- children: ghostLabel
4931
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${ghostMenuOpen ? " dsh-model-menu-chevronOpen" : ""}` })]
4932
- }),
4933
- choices: selectableChoices,
4934
- selected: ghostRoute,
4935
- onPick: (route) => edit({
4936
- ...draft,
4937
- ghostModel: modelRouteKey(route.provider, route.model)
4938
- }),
4939
- leading: [{
4940
- id: "follow",
4941
- label: copy("modelFollowOption"),
4942
- selected: !ghostRoute
4943
- }],
4944
- onPickLeading: () => edit({
4945
- ...draft,
4946
- ghostModel: ""
4947
- }),
4948
- modelValue: ghostLabel
4949
- })]
4950
- }) : null
4951
- })
4952
- ]
4953
- }),
4954
- draft.continueMode === "auto" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4955
- className: "dsh-ui-hint",
4956
- children: copy("ghostModelHint")
4957
- }) : null
4958
- ]
4959
- }),
4960
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
4961
- className: "dsm-setting",
4962
- "aria-labelledby": `${tabsId}-auto-continue-heading`,
4963
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("header", {
4964
- className: "dsm-setting-header",
4965
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
4966
- id: `${tabsId}-auto-continue-heading`,
4967
- className: "dsh-ui-title",
4968
- children: copy("autoContinueGroup")
4969
- })
4970
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4971
- className: "dsm-feature-options",
4972
- children: [
4973
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
4974
- className: "dsh-ui-field",
4975
- children: [
4976
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4977
- className: "dsh-ui-label",
4978
- children: copy("autoContinueDefaultLabel")
4979
- }),
4980
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
4981
- className: "dsh-ui-select",
4982
- value: draft.autoContinueMode,
4983
- onChange: (event) => edit({
4984
- ...draft,
4985
- autoContinueMode: event.target.value
4986
- }),
4987
- children: [
4988
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
4989
- value: "off",
4990
- children: copy("autoContinueOff")
4991
- }),
4992
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
4993
- value: "cautious",
4994
- children: copy("autoContinueCautious")
4995
- }),
4996
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
4997
- value: "bold",
4998
- children: copy("autoContinueBold")
4999
- })
5000
- ]
5001
- }),
5002
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5003
- className: "dsh-ui-hint",
5004
- children: copy("autoContinueDefaultHint")
5005
- })
5006
- ]
5007
- }),
5008
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
5009
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
5010
- className: "dsh-ui-field",
5011
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5012
- className: "dsh-ui-label",
5013
- children: copy("autoContinueMaxLabel")
5014
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5015
- className: "dsm-minutes-control",
5016
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
5017
- className: "dsm-minutes",
5018
- value: draft.autoContinueMax,
5019
- inputMode: "numeric",
5020
- "aria-invalid": autoMaxInvalid || void 0,
5021
- "aria-describedby": autoMaxInvalid ? `${tabsId}-auto-max-error` : void 0,
5022
- onChange: (event) => edit({
5023
- ...draft,
5024
- autoContinueMax: event.target.value
5025
- })
5026
- })
5027
- })]
5028
- }),
5029
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5030
- className: "dsh-ui-hint",
5031
- children: copy("autoContinueMaxHint")
5032
- }),
5033
- autoMaxInvalid ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5034
- id: `${tabsId}-auto-max-error`,
5035
- className: "dsh-ui-error",
5036
- role: "alert",
5037
- children: copy("autoContinueMaxInvalid")
5038
- }) : null
5039
- ] }),
5040
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5041
- className: "dsh-ui-field dsm-model-field",
5042
- children: [
5043
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5044
- className: "dsh-ui-label",
5045
- children: copy("judgeModelLabel")
5046
- }),
5047
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelMenu, {
5048
- locale: props.locale,
5049
- open: judgeMenuOpen,
5050
- onOpenChange: setJudgeMenuOpen,
5051
- listClassName: FREE_CHAT_ROOT,
5052
- anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
5053
- type: "button",
5054
- className: "dsh-model-menu-trigger",
5055
- "aria-label": `${copy("judgeModelLabel")}: ${judgeLabel}`,
5056
- "aria-haspopup": "menu",
5057
- "aria-expanded": judgeMenuOpen,
5058
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5059
- className: "dsh-model-menu-triggerLabel",
5060
- children: judgeLabel
5061
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${judgeMenuOpen ? " dsh-model-menu-chevronOpen" : ""}` })]
5062
- }),
5063
- choices: selectableChoices,
5064
- selected: judgeRoute,
5065
- onPick: (route) => edit({
5066
- ...draft,
5067
- judgeModel: modelRouteKey(route.provider, route.model)
5068
- }),
5069
- leading: [{
5070
- id: "follow",
5071
- label: copy("modelFollowOption"),
5072
- selected: !judgeRoute
5073
- }],
5074
- onPickLeading: () => edit({
5075
- ...draft,
5076
- judgeModel: ""
5077
- }),
5078
- modelValue: judgeLabel
5079
- }),
5080
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5081
- className: "dsh-ui-hint",
5082
- children: copy("judgeModelHint")
5083
- })
5084
- ]
5085
- })
5086
- ]
5339
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5340
+ className: "dsh-ui-field",
5341
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5342
+ className: "dsh-ui-label",
5343
+ children: copy(props.locale, "continueSourceLabel")
5344
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.SegmentedTabs, {
5345
+ label: copy(props.locale, "continueSourceLabel"),
5346
+ items: [{
5347
+ id: `${tabsId}-fixed`,
5348
+ panelId: fixedPanelId,
5349
+ value: "fixed",
5350
+ label: copy(props.locale, "modeFixed")
5351
+ }, {
5352
+ id: `${tabsId}-auto`,
5353
+ panelId: autoPanelId,
5354
+ value: "auto",
5355
+ label: copy(props.locale, "modeAuto")
5356
+ }],
5357
+ value: values.continueMode,
5358
+ onChange: (value) => {
5359
+ if (value !== values.continueMode) send({ continueMode: value });
5360
+ }
5087
5361
  })]
5088
5362
  }),
5089
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
5090
- className: "dsm-setting",
5091
- "aria-labelledby": `${tabsId}-handoff-heading`,
5092
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("header", {
5093
- className: "dsm-setting-header",
5094
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
5095
- id: `${tabsId}-handoff-heading`,
5096
- className: "dsh-ui-title",
5097
- children: copy("handoffGroup")
5098
- })
5099
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5100
- className: "dsm-feature-options",
5101
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5102
- className: "dsh-ui-field dsm-model-field",
5103
- children: [
5104
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5105
- className: "dsh-ui-label",
5106
- children: copy("handoffModelLabel")
5107
- }),
5108
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelMenu, {
5109
- locale: props.locale,
5110
- open: handoffMenuOpen,
5111
- onOpenChange: setHandoffMenuOpen,
5112
- listClassName: FREE_CHAT_ROOT,
5113
- anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
5114
- type: "button",
5115
- className: "dsh-model-menu-trigger",
5116
- "aria-label": `${copy("handoffModelLabel")}: ${handoffLabel}`,
5117
- "aria-haspopup": "menu",
5118
- "aria-expanded": handoffMenuOpen,
5119
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5120
- className: "dsh-model-menu-triggerLabel",
5121
- children: handoffLabel
5122
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${handoffMenuOpen ? " dsh-model-menu-chevronOpen" : ""}` })]
5123
- }),
5124
- choices: selectableChoices,
5125
- selected: handoffRoute,
5126
- onPick: (route) => edit({
5127
- ...draft,
5128
- handoffModel: modelRouteKey(route.provider, route.model)
5129
- }),
5130
- leading: [{
5131
- id: "follow",
5132
- label: copy("modelFollowOption"),
5133
- selected: !handoffRoute
5134
- }],
5135
- onPickLeading: () => edit({
5136
- ...draft,
5137
- handoffModel: ""
5138
- }),
5139
- modelValue: handoffLabel
5140
- }),
5141
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5142
- className: "dsh-ui-hint",
5143
- children: copy("handoffModelHint")
5144
- })
5145
- ]
5146
- })
5363
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5364
+ role: "tabpanel",
5365
+ id: fixedPanelId,
5366
+ "aria-labelledby": `${tabsId}-fixed`,
5367
+ hidden: values.continueMode !== "fixed",
5368
+ children: values.continueMode === "fixed" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
5369
+ className: "dsh-ui-field",
5370
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5371
+ className: "dsh-ui-label",
5372
+ children: copy(props.locale, "continueTextLabel")
5373
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
5374
+ className: "dsm-text-input",
5375
+ placeholder: copy(props.locale, "continueTextPlaceholder"),
5376
+ ...field.inputProps
5377
+ })]
5378
+ }) : null
5379
+ }),
5380
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5381
+ role: "tabpanel",
5382
+ id: autoPanelId,
5383
+ "aria-labelledby": `${tabsId}-auto`,
5384
+ hidden: values.continueMode !== "auto",
5385
+ children: values.continueMode === "auto" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelField, {
5386
+ ...props,
5387
+ field: "ghostModel",
5388
+ labelKey: "ghostModelLabel",
5389
+ route: values.ghostModel,
5390
+ send
5391
+ }) : null
5392
+ }),
5393
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5394
+ className: "dsh-ui-hint",
5395
+ children: copy(props.locale, "composerUsageHint")
5396
+ }),
5397
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SaveStatus, {
5398
+ snapshot,
5399
+ locale: props.locale,
5400
+ owns
5401
+ })
5402
+ ]
5403
+ })
5404
+ });
5405
+ }
5406
+ /** 自动继续 / Auto-continue row. */
5407
+ function AutoContinueSettings(props) {
5408
+ const { snapshot, values, gate } = useRowGate(props);
5409
+ const { send, owns } = useConfigPatch(props.model);
5410
+ const errorId = (0, react.useId)();
5411
+ const field = useTextSetting({
5412
+ stored: String(values?.autoContinueMax ?? ""),
5413
+ parse: (draft) => {
5414
+ const value = autoMaxOf(draft);
5415
+ return value === void 0 ? void 0 : {
5416
+ value,
5417
+ draft: String(value)
5418
+ };
5419
+ },
5420
+ send: (0, react.useCallback)((value) => send({ autoContinueMax: value }), [send])
5421
+ });
5422
+ if (gate !== void 0 || values === void 0) return gate ?? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, {});
5423
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5424
+ className: "dsm-settings",
5425
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5426
+ className: "dsm-feature",
5427
+ children: [
5428
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
5429
+ className: "dsh-ui-field",
5430
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5431
+ className: "dsh-ui-label",
5432
+ children: copy(props.locale, "autoContinueMaxLabel")
5433
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
5434
+ className: "dsm-minutes",
5435
+ inputMode: "numeric",
5436
+ "aria-invalid": field.invalid || void 0,
5437
+ "aria-describedby": field.invalid ? `${errorId}-max-error` : void 0,
5438
+ ...field.inputProps
5147
5439
  })]
5148
5440
  }),
5149
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
5150
- className: "dsm-setting",
5151
- "aria-labelledby": `${tabsId}-return-heading`,
5152
- children: [
5153
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("header", {
5154
- className: "dsm-setting-header",
5155
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
5156
- id: `${tabsId}-return-heading`,
5157
- className: "dsh-ui-title",
5158
- children: copy("welcomeBackGroup")
5159
- })
5160
- }),
5161
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5162
- className: "dsm-feature",
5163
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5164
- className: "dsh-ui-toggle-row",
5165
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
5166
- className: "dsh-ui-toggle-text",
5167
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5168
- className: "dsh-ui-toggle-label",
5169
- children: copy("recapEnabledLabel")
5170
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5171
- className: "dsh-ui-hint",
5172
- children: copy("recapEnabledHint")
5173
- })]
5174
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Switch, {
5175
- label: copy("recapEnabledLabel"),
5176
- checked: draft.recapEnabled,
5177
- onChange: (next) => edit({
5178
- ...draft,
5179
- recapEnabled: next
5180
- })
5181
- })]
5182
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5183
- className: "dsm-feature-options",
5184
- "data-disabled": draft.recapEnabled ? void 0 : "true",
5185
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5186
- className: "dsh-ui-fields-grid",
5187
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
5188
- className: "dsh-ui-field",
5189
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5190
- className: "dsh-ui-label",
5191
- children: copy("recapMinutesLabel")
5192
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
5193
- className: "dsm-minutes-control",
5194
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
5195
- className: "dsm-minutes",
5196
- value: draft.recapMinutes,
5197
- inputMode: "numeric",
5198
- disabled: !draft.recapEnabled,
5199
- "aria-invalid": recapInvalid || void 0,
5200
- "aria-describedby": recapInvalid ? `${tabsId}-recap-error` : void 0,
5201
- onChange: (event) => edit({
5202
- ...draft,
5203
- recapMinutes: event.target.value
5204
- })
5205
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5206
- className: "dsh-ui-meta",
5207
- children: copy("minutesUnit")
5208
- })]
5209
- })]
5210
- }), recapInvalid ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5211
- id: `${tabsId}-recap-error`,
5212
- className: "dsh-ui-error",
5213
- role: "alert",
5214
- children: copy("minutesInvalid")
5215
- }) : null] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5216
- className: "dsh-ui-field dsm-model-field",
5217
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5218
- className: "dsh-ui-label",
5219
- children: copy("modelLabel")
5220
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelMenu, {
5221
- locale: props.locale,
5222
- open: menuOpen,
5223
- onOpenChange: setMenuOpen,
5224
- listClassName: FREE_CHAT_ROOT,
5225
- anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
5226
- type: "button",
5227
- className: "dsh-model-menu-trigger",
5228
- disabled: !draft.recapEnabled,
5229
- "aria-label": `${copy("modelLabel")}: ${selectedLabel}`,
5230
- "aria-haspopup": "menu",
5231
- "aria-expanded": menuOpen,
5232
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5233
- className: "dsh-model-menu-triggerLabel",
5234
- children: selectedLabel
5235
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${menuOpen ? " dsh-model-menu-chevronOpen" : ""}` })]
5236
- }),
5237
- choices: selectableChoices,
5238
- selected: selectedRoute,
5239
- onPick: (route) => edit({
5240
- ...draft,
5241
- model: modelRouteKey(route.provider, route.model)
5242
- }),
5243
- leading: [{
5244
- id: "follow",
5245
- label: copy("modelFollowOption"),
5246
- selected: !selectedRoute
5247
- }],
5248
- onPickLeading: () => edit({
5249
- ...draft,
5250
- model: ""
5251
- }),
5252
- modelValue: selectedLabel
5253
- })]
5254
- })]
5255
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelCatalogStatus, {
5256
- state: catalog,
5257
- locale: props.locale
5258
- })]
5259
- })]
5260
- }),
5261
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5262
- className: "dsm-feature",
5263
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5264
- className: "dsh-ui-toggle-row",
5265
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
5266
- className: "dsh-ui-toggle-text",
5267
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5268
- className: "dsh-ui-toggle-label",
5269
- children: copy("compactEnabledLabel")
5270
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5271
- className: "dsh-ui-hint",
5272
- children: copy("compactEnabledHint")
5273
- })]
5274
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Switch, {
5275
- label: copy("compactEnabledLabel"),
5276
- checked: draft.compactEnabled,
5277
- onChange: (next) => edit({
5278
- ...draft,
5279
- compactEnabled: next
5280
- })
5281
- })]
5282
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5283
- className: "dsm-feature-options dsm-compact-options",
5284
- "data-disabled": draft.compactEnabled ? void 0 : "true",
5285
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
5286
- className: "dsh-ui-field",
5287
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5288
- className: "dsh-ui-label",
5289
- children: copy("compactMinutesLabel")
5290
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
5291
- className: "dsm-minutes-control",
5292
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
5293
- className: "dsm-minutes",
5294
- value: draft.compactMinutes,
5295
- inputMode: "numeric",
5296
- disabled: !draft.compactEnabled,
5297
- "aria-invalid": compactInvalid || void 0,
5298
- "aria-describedby": compactInvalid ? `${tabsId}-compact-error` : void 0,
5299
- onChange: (event) => edit({
5300
- ...draft,
5301
- compactMinutes: event.target.value
5302
- })
5303
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5304
- className: "dsh-ui-meta",
5305
- children: copy("minutesUnit")
5306
- })]
5307
- })]
5308
- }), compactInvalid ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5309
- id: `${tabsId}-compact-error`,
5310
- className: "dsh-ui-error",
5311
- role: "alert",
5312
- children: copy("minutesInvalid")
5313
- }) : null] })
5314
- })]
5315
- })
5316
- ]
5441
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5442
+ className: "dsh-ui-hint",
5443
+ children: copy(props.locale, "autoContinueMaxHint")
5444
+ }),
5445
+ field.invalid ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5446
+ id: `${errorId}-max-error`,
5447
+ className: "dsh-ui-error",
5448
+ role: "alert",
5449
+ children: copy(props.locale, "autoContinueMaxInvalid")
5450
+ }) : null,
5451
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelField, {
5452
+ ...props,
5453
+ field: "judgeModel",
5454
+ labelKey: "judgeModelLabel",
5455
+ route: values.judgeModel,
5456
+ send
5457
+ }),
5458
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5459
+ className: "dsh-ui-hint",
5460
+ children: copy(props.locale, "judgeModelHint")
5461
+ }),
5462
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5463
+ className: "dsh-ui-hint",
5464
+ children: copy(props.locale, "autoContinueModeHint")
5465
+ }),
5466
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SaveStatus, {
5467
+ snapshot,
5468
+ locale: props.locale,
5469
+ owns
5317
5470
  })
5318
5471
  ]
5472
+ })
5473
+ });
5474
+ }
5475
+ /** One return-feature block: a switch row and its gated "away for N minutes" input. */
5476
+ function ThresholdBlock(props) {
5477
+ const enabled = (props.minutes ?? 0) > 0;
5478
+ const errorId = (0, react.useId)();
5479
+ const input = useTextSetting({
5480
+ stored: String(enabled ? props.minutes : props.fallback),
5481
+ parse: (draft) => {
5482
+ const value = minutesOf(draft);
5483
+ return value === void 0 ? void 0 : {
5484
+ value,
5485
+ draft: String(value)
5486
+ };
5487
+ },
5488
+ send: (0, react.useCallback)((value) => props.send({ [props.field]: value }), [props.send, props.field])
5489
+ });
5490
+ const toggle = (next) => {
5491
+ if (next === enabled) return;
5492
+ if (!next) {
5493
+ props.send({ [props.field]: 0 });
5494
+ return;
5495
+ }
5496
+ props.send({ [props.field]: minutesOf(input.draft) ?? props.fallback });
5497
+ };
5498
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5499
+ className: "dsm-feature",
5500
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5501
+ className: "dsh-ui-toggle-row",
5502
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
5503
+ className: "dsh-ui-toggle-text",
5504
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5505
+ className: "dsh-ui-toggle-label",
5506
+ children: copy(props.locale, props.titleKey)
5507
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5508
+ className: "dsh-ui-hint",
5509
+ children: copy(props.locale, props.hintKey)
5510
+ })]
5511
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Switch, {
5512
+ label: copy(props.locale, props.titleKey),
5513
+ checked: enabled,
5514
+ onChange: toggle
5515
+ })]
5319
5516
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5320
- className: "dsh-ui-form-footer dsm-settings-footer",
5517
+ className: "dsm-feature-options",
5518
+ "data-disabled": enabled ? void 0 : "true",
5321
5519
  children: [
5322
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
5323
- variant: "primary",
5324
- disabled: invalid || snapshot.saving,
5325
- onClick: save,
5326
- children: snapshot.saving ? copy("settingsSaving") : copy("saveSettings")
5520
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
5521
+ className: "dsh-ui-field",
5522
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
5523
+ className: "dsm-minutes-control",
5524
+ children: [
5525
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5526
+ className: "dsh-ui-label",
5527
+ children: copy(props.locale, "awayMinutesLabel")
5528
+ }),
5529
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
5530
+ className: "dsm-minutes",
5531
+ inputMode: "numeric",
5532
+ disabled: !enabled,
5533
+ "aria-label": `${copy(props.locale, "awayMinutesLabel")} ${copy(props.locale, "minutesUnit")}`,
5534
+ "aria-invalid": enabled && input.invalid ? true : void 0,
5535
+ "aria-describedby": enabled && input.invalid ? `${errorId}-minutes-error` : void 0,
5536
+ ...input.inputProps
5537
+ }),
5538
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5539
+ className: "dsh-ui-meta",
5540
+ children: copy(props.locale, "minutesUnit")
5541
+ })
5542
+ ]
5543
+ })
5327
5544
  }),
5328
- snapshot.saved ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5329
- className: "dsh-ui-notice",
5330
- role: "status",
5331
- children: copy("settingsSaved")
5332
- }) : null,
5333
- externalChange ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5334
- className: "dsh-ui-notice dsh-ui-notice--error",
5545
+ enabled && input.invalid ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5546
+ id: `${errorId}-minutes-error`,
5547
+ className: "dsh-ui-error",
5335
5548
  role: "alert",
5336
- children: copy("externalChange")
5549
+ children: copy(props.locale, "minutesInvalid")
5337
5550
  }) : null,
5338
- snapshot.failure ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5339
- className: "dsh-ui-notice dsh-ui-notice--error",
5340
- role: "alert",
5341
- children: failureText(props.locale, snapshot.failure)
5342
- }) : null
5551
+ props.extra?.(enabled)
5343
5552
  ]
5344
5553
  })]
5345
5554
  });
5346
5555
  }
5556
+ /** 回到会话时 / When you return row. */
5557
+ function ReturnSettings(props) {
5558
+ const { snapshot, values, gate } = useRowGate(props);
5559
+ const { send, owns } = useConfigPatch(props.model);
5560
+ if (gate !== void 0 || values === void 0) return gate ?? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, {});
5561
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5562
+ className: "dsm-settings",
5563
+ children: [
5564
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ThresholdBlock, {
5565
+ ...props,
5566
+ field: "recapMinutes",
5567
+ titleKey: "recapEnabledLabel",
5568
+ hintKey: "recapEnabledHint",
5569
+ minutes: values.recapMinutes,
5570
+ fallback: 15,
5571
+ send,
5572
+ extra: (enabled) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelField, {
5573
+ ...props,
5574
+ field: "model",
5575
+ labelKey: "recapModelLabel",
5576
+ route: values.model,
5577
+ send,
5578
+ disabled: !enabled
5579
+ })
5580
+ }),
5581
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ThresholdBlock, {
5582
+ ...props,
5583
+ field: "compactMinutes",
5584
+ titleKey: "compactEnabledLabel",
5585
+ hintKey: "compactEnabledHint",
5586
+ minutes: values.compactMinutes,
5587
+ fallback: 60,
5588
+ send
5589
+ }),
5590
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SaveStatus, {
5591
+ snapshot,
5592
+ locale: props.locale,
5593
+ owns
5594
+ })
5595
+ ]
5596
+ });
5597
+ }
5598
+ /**
5599
+ * One feature's model picker. The catalog loads with the row (first expand);
5600
+ * a stored route missing from the catalog stays selectable so a refresh or a
5601
+ * partial outage never hides the current choice.
5602
+ */
5603
+ function ModelField(props) {
5604
+ const catalog = useModelCatalog(props.modelCatalog ?? missingCatalog, { subscribe: props.subscribeCatalog });
5605
+ const [open, setOpen] = (0, react.useState)(false);
5606
+ const label = copy(props.locale, props.labelKey);
5607
+ const selected = props.route === null ? void 0 : {
5608
+ provider: props.route.provider,
5609
+ model: props.route.model
5610
+ };
5611
+ const choices = parseModelMenuChoices(catalog.catalog);
5612
+ if (selected && !choices.some((choice) => choice.provider === selected.provider && choice.model === selected.model)) choices.push({
5613
+ provider: selected.provider,
5614
+ model: selected.model,
5615
+ label: `${selected.provider} / ${selected.model}`,
5616
+ efforts: []
5617
+ });
5618
+ const key = selected === void 0 ? "" : modelRouteKey(selected.provider, selected.model);
5619
+ const value = choices.find((choice) => modelRouteKey(choice.provider, choice.model) === key)?.label ?? copy(props.locale, "modelFollowOption");
5620
+ const anchor = useModelMenuAnchor(`${label}: ${value}`, void 0, () => setOpen(false));
5621
+ const pick = (route) => {
5622
+ if (selected && route.provider === selected.provider && route.model === selected.model) return;
5623
+ props.send({ [props.field]: {
5624
+ provider: route.provider,
5625
+ model: route.model
5626
+ } });
5627
+ };
5628
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5629
+ className: "dsm-model-field",
5630
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5631
+ className: "dsm-model-choice",
5632
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5633
+ className: "dsh-ui-label",
5634
+ children: label
5635
+ }), renderModelPicker(props.renderFactorySlot, {
5636
+ locale: props.locale,
5637
+ disabled: props.disabled,
5638
+ onOpenChange: (next) => setOpen(next && !props.disabled),
5639
+ listClassName: FREE_CHAT_ROOT,
5640
+ choices,
5641
+ selected,
5642
+ onPick: pick,
5643
+ leading: [{
5644
+ id: "follow",
5645
+ label: copy(props.locale, "modelFollowOption"),
5646
+ selected: selected === void 0
5647
+ }],
5648
+ onPickLeading: () => {
5649
+ if (props.route !== null) props.send({ [props.field]: null });
5650
+ },
5651
+ triggerLabel: value,
5652
+ modelValue: value
5653
+ }, {
5654
+ anchor,
5655
+ fallback: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelMenu, {
5656
+ locale: props.locale,
5657
+ open: open && !props.disabled,
5658
+ onOpenChange: (next) => setOpen(next && !props.disabled),
5659
+ listClassName: FREE_CHAT_ROOT,
5660
+ anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
5661
+ type: "button",
5662
+ className: "dsh-model-menu-trigger",
5663
+ disabled: props.disabled,
5664
+ "aria-label": `${label}: ${value}`,
5665
+ "aria-haspopup": "menu",
5666
+ "aria-expanded": open,
5667
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5668
+ className: "dsh-model-menu-triggerLabel",
5669
+ children: value
5670
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${open ? " dsh-model-menu-chevronOpen" : ""}` })]
5671
+ }),
5672
+ choices,
5673
+ selected,
5674
+ onPick: pick,
5675
+ leading: [{
5676
+ id: "follow",
5677
+ label: copy(props.locale, "modelFollowOption"),
5678
+ selected: selected === void 0
5679
+ }],
5680
+ onPickLeading: () => {
5681
+ if (props.route !== null) props.send({ [props.field]: null });
5682
+ },
5683
+ modelValue: value
5684
+ })
5685
+ })]
5686
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelCatalogStatus, {
5687
+ state: catalog,
5688
+ locale: props.locale
5689
+ })]
5690
+ });
5691
+ }
5692
+ /** 会话交接 / Session handoff row: the model behind read_session handoff summaries. */
5693
+ function HandoffSettings(props) {
5694
+ const { snapshot, values, gate } = useRowGate(props);
5695
+ const { send, owns } = useConfigPatch(props.model);
5696
+ if (gate !== void 0 || values === void 0) return gate ?? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, {});
5697
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5698
+ className: "dsm-settings",
5699
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5700
+ className: "dsm-feature",
5701
+ children: [
5702
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelField, {
5703
+ ...props,
5704
+ field: "handoffModel",
5705
+ labelKey: "handoffModelLabel",
5706
+ route: values.handoffModel,
5707
+ send
5708
+ }),
5709
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5710
+ className: "dsh-ui-hint",
5711
+ children: copy(props.locale, "handoffModelHint")
5712
+ }),
5713
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SaveStatus, {
5714
+ snapshot,
5715
+ locale: props.locale,
5716
+ owns
5717
+ })
5718
+ ]
5719
+ })
5720
+ });
5721
+ }
5722
+ /**
5723
+ * 代理操作 / Agent actions row.
5724
+ *
5725
+ * Two independent switches: each one saves only its own field, so toggling
5726
+ * one never rewrites the other. Both default to off, and the agent decides
5727
+ * on its own once a switch is on — there is no per-call approval here.
5728
+ *
5729
+ * The switches share one sparse writer: the config model accepts a single
5730
+ * write at a time, so a second click while a save is in flight must queue
5731
+ * rather than be dropped. The shared writer merges the pending patches and
5732
+ * flushes them once the current save settles.
5733
+ */
5734
+ function AgentActionsSettings(props) {
5735
+ const { snapshot, values, gate } = useRowGate(props);
5736
+ const { send, owns } = useConfigPatch(props.model);
5737
+ if (gate !== void 0 || values === void 0) return gate ?? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, {});
5738
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5739
+ className: "dsm-settings",
5740
+ children: [
5741
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5742
+ className: "dsm-feature",
5743
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5744
+ className: "dsh-ui-toggle-row",
5745
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
5746
+ className: "dsh-ui-toggle-text",
5747
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5748
+ className: "dsh-ui-toggle-label",
5749
+ children: copy(props.locale, "allowSelfForkLabel")
5750
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5751
+ className: "dsh-ui-hint",
5752
+ children: copy(props.locale, "allowSelfForkHint")
5753
+ })]
5754
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Switch, {
5755
+ label: copy(props.locale, "allowSelfForkLabel"),
5756
+ checked: values.allowSelfFork === true,
5757
+ onChange: (next) => {
5758
+ send({ allowSelfFork: next });
5759
+ }
5760
+ })]
5761
+ })
5762
+ }),
5763
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5764
+ className: "dsm-feature",
5765
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5766
+ className: "dsh-ui-toggle-row",
5767
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
5768
+ className: "dsh-ui-toggle-text",
5769
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5770
+ className: "dsh-ui-toggle-label",
5771
+ children: copy(props.locale, "allowSendMessageLabel")
5772
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5773
+ className: "dsh-ui-hint",
5774
+ children: copy(props.locale, "allowSendMessageHint")
5775
+ })]
5776
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Switch, {
5777
+ label: copy(props.locale, "allowSendMessageLabel"),
5778
+ checked: values.allowSendMessage === true,
5779
+ onChange: (next) => {
5780
+ send({ allowSendMessage: next });
5781
+ }
5782
+ })]
5783
+ })
5784
+ }),
5785
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SaveStatus, {
5786
+ snapshot,
5787
+ locale: props.locale,
5788
+ owns
5789
+ })
5790
+ ]
5791
+ });
5792
+ }
5793
+ /**
5794
+ * One collapsible row in the shared Session Management section: a named
5795
+ * details group. The body mounts on the first expand only — opening the page
5796
+ * must not fire every feature's config and model reads — and stays mounted
5797
+ * afterwards so collapsing never drops an in-progress text draft.
5798
+ */
5799
+ function SessionSettingsDetails(props) {
5800
+ const [visited, setVisited] = (0, react.useState)(false);
5801
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
5802
+ className: `${FREE_CHAT_ROOT} dsm-item`,
5803
+ "data-testid": props.testId,
5804
+ onToggle: (event) => {
5805
+ if (event.currentTarget.open) setVisited(true);
5806
+ },
5807
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("summary", {
5808
+ className: "dsm-item-summary",
5809
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5810
+ className: "dsm-item-title",
5811
+ children: copy(props.locale, props.titleKey)
5812
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5813
+ className: "dsm-item-desc",
5814
+ children: copy(props.locale, props.descKey)
5815
+ })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, {
5816
+ className: "dsm-item-chevron",
5817
+ "aria-hidden": "true"
5818
+ })]
5819
+ }), visited ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5820
+ className: "dsm-item-body",
5821
+ children: props.children
5822
+ }) : null]
5823
+ });
5824
+ }
5347
5825
  //#endregion
5348
5826
  //#region src/message-editor.tsx
5349
5827
  function AttachmentPreview(props) {
@@ -6281,16 +6759,48 @@ function apply(ctx) {
6281
6759
  locale: useLiveLocale(client.locale),
6282
6760
  model: welcomeBack
6283
6761
  }))));
6284
- stops.push(registerSessionSettingsItem(ctx, {
6285
- id: SESSION_SETTINGS_ITEM_ID,
6286
- order: 65
6287
- }, () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SessionManagerSettingsItem, {
6288
- model: presenceConfig,
6289
- locale: useLiveLocale(client.locale),
6762
+ const registerSettingsRow = (item, testId, titleKey, descKey, body) => stops.push(registerSessionSettingsItem(ctx, item, (slotProps) => {
6763
+ const locale = useLiveLocale(client.locale);
6764
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SessionSettingsDetails, {
6765
+ testId,
6766
+ titleKey,
6767
+ descKey,
6768
+ locale,
6769
+ children: body(locale, slotProps?.renderFactorySlot)
6770
+ });
6771
+ }));
6772
+ const catalogProps = {
6290
6773
  modelCatalog,
6291
- subscribeCatalog,
6292
- subscribeSettings
6293
- })));
6774
+ subscribeCatalog
6775
+ };
6776
+ registerSettingsRow(SESSION_SETTINGS_ITEMS[0], "session-manager-composer-settings-item", "composerGroupTitle", "composerGroupHint", (locale, renderFactorySlot) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ComposerSettings, {
6777
+ model: presenceConfig,
6778
+ locale,
6779
+ renderFactorySlot,
6780
+ ...catalogProps
6781
+ }));
6782
+ registerSettingsRow(SESSION_SETTINGS_ITEMS[1], "session-manager-auto-continue-settings-item", "autoContinueGroup", "autoContinueGroupHint", (locale, renderFactorySlot) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AutoContinueSettings, {
6783
+ model: presenceConfig,
6784
+ locale,
6785
+ renderFactorySlot,
6786
+ ...catalogProps
6787
+ }));
6788
+ registerSettingsRow(SESSION_SETTINGS_ITEMS[2], "session-manager-return-settings-item", "returnGroupTitle", "returnGroupHint", (locale, renderFactorySlot) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ReturnSettings, {
6789
+ model: presenceConfig,
6790
+ locale,
6791
+ renderFactorySlot,
6792
+ ...catalogProps
6793
+ }));
6794
+ registerSettingsRow(SESSION_SETTINGS_ITEMS[3], "session-manager-handoff-settings-item", "handoffGroupTitle", "handoffGroupHint", (locale, renderFactorySlot) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HandoffSettings, {
6795
+ model: presenceConfig,
6796
+ locale,
6797
+ renderFactorySlot,
6798
+ ...catalogProps
6799
+ }));
6800
+ registerSettingsRow(SESSION_SETTINGS_ITEMS[4], "session-manager-agent-actions-settings-item", "agentActionsGroup", "agentActionsGroupHint", (locale) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AgentActionsSettings, {
6801
+ model: presenceConfig,
6802
+ locale
6803
+ }));
6294
6804
  stops.push(client.slots.inject("shell.overlay", () => client.slots.register({
6295
6805
  name: "shell.overlay",
6296
6806
  id: DELETE_DIALOG_ID,
@@ -6408,49 +6918,10 @@ function DeleteMenuItem(props) {
6408
6918
  children: uiText(locale, "deleteChat")
6409
6919
  });
6410
6920
  }
6411
- /**
6412
- * Session Management item: a named native details group. The body mounts on
6413
- * the first expand only — the settings page must not fire every plugin's
6414
- * config and model reads — and stays mounted afterwards so collapsing never
6415
- * drops an unsaved draft or the settings-change subscription.
6416
- */
6417
- function SessionManagerSettingsItem(props) {
6418
- const [visited, setVisited] = (0, react.useState)(false);
6419
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
6420
- className: `${FREE_CHAT_ROOT} dsm-item`,
6421
- "data-testid": "session-manager-settings-item",
6422
- onToggle: (event) => {
6423
- if (event.currentTarget.open) setVisited(true);
6424
- },
6425
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("summary", {
6426
- className: "dsm-item-summary",
6427
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6428
- className: "dsm-item-title",
6429
- children: uiText(props.locale, "settingsGroupTitle")
6430
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6431
- className: "dsm-item-desc",
6432
- children: uiText(props.locale, "settingsGroupHint")
6433
- })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, {
6434
- className: "dsm-item-chevron",
6435
- "aria-hidden": "true"
6436
- })]
6437
- }), visited ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6438
- className: "dsm-item-body",
6439
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SessionManagerSettings, {
6440
- model: props.model,
6441
- locale: props.locale,
6442
- modelCatalog: props.modelCatalog,
6443
- subscribeCatalog: props.subscribeCatalog,
6444
- subscribeSettings: props.subscribeSettings
6445
- })
6446
- }) : null]
6447
- });
6448
- }
6449
6921
  function useLiveLocale(locale) {
6450
6922
  return copyLocale((0, react.useSyncExternalStore)((listener) => locale?.subscribe(listener) ?? (() => {}), () => locale?.getSnapshot().active ?? "en", () => locale?.getSnapshot().active ?? "en"));
6451
6923
  }
6452
6924
  //#endregion
6453
- exports.SessionManagerSettingsItem = SessionManagerSettingsItem;
6454
6925
  exports.apply = apply;
6455
6926
  exports.inject = inject;
6456
6927
  exports.name = name;