@klarkxy/dsh-session-manager 0.4.1 → 0.5.1

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