dsh-switchman 1.7.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/client.js CHANGED
@@ -32,6 +32,33 @@ window.__ModuleLoader__.load({
32
32
  const React = require('react');
33
33
  const h = React.createElement;
34
34
 
35
+ // ------------------------------------------------------------------
36
+ // Pin the "Auto" group to the top of the app's model pickers.
37
+ //
38
+ // The composer/command pickers (ui-model-selection ModelSelect and
39
+ // PopupSelectView) order provider groups with a compiled-in
40
+ // comparator (`provider-order.ts`) that pins only the deepseek ids
41
+ // and leaves every other group in catalog registration order — which
42
+ // lands our pseudo provider (registered last, by a plugin) at the
43
+ // bottom. There is no extension point, and the asar cannot be
44
+ // patched (ElectronAsarIntegrity), but the comparator reaches us
45
+ // through Array.prototype.toSorted: wrap it and pre-pin our group
46
+ // whenever THAT comparator (identified by its source) is used.
47
+ // Every other toSorted call passes through untouched.
48
+ // ------------------------------------------------------------------
49
+ (function pinAutoGroupTop() {
50
+ const original = Array.prototype.toSorted;
51
+ if (typeof original !== 'function') return;
52
+ Array.prototype.toSorted = function (comparator, ...rest) {
53
+ if (typeof comparator === 'function' && String(comparator).includes('deepseek-official')) {
54
+ return original.call(this, (left, right) =>
55
+ ((left?.id === 'dsh-switchman-auto' ? -1 : 0) - (right?.id === 'dsh-switchman-auto' ? -1 : 0))
56
+ || comparator(left, right), ...rest);
57
+ }
58
+ return original.call(this, comparator, ...rest);
59
+ };
60
+ })();
61
+
35
62
  /** Locale namespace shared by every dsh-switchman client surface. */
36
63
  const NS = 'dsh-switchman';
37
64
 
@@ -166,10 +193,10 @@ window.__ModuleLoader__.load({
166
193
  /** Numeric watermark fields: settings field, schema minimum, and the
167
194
  * Host-side default (also the fallback for malformed stored values). */
168
195
  const WM_NUMBER_FIELDS = [
169
- { field: 'wmSoftTokens', min: 1000, def: 50000 },
170
- { field: 'wmHardTokens', min: 2000, def: 90000 },
171
- { field: 'wmForceTokens', min: 3000, def: 130000 },
172
- { field: 'wmReadBudgetTokens', min: 200, def: 1500 },
196
+ { field: 'wmSoftTokens', min: 1000, def: 80000 },
197
+ { field: 'wmHardTokens', min: 2000, def: 120000 },
198
+ { field: 'wmForceTokens', min: 3000, def: 150000 },
199
+ { field: 'wmReadBudgetTokens', min: 200, def: 3000 },
173
200
  { field: 'wmSubagentForceTokens', min: 0, def: 0 },
174
201
  ];
175
202
 
@@ -188,6 +215,48 @@ window.__ModuleLoader__.load({
188
215
  },
189
216
  };
190
217
 
218
+ /** Intent-routing classifier kinds (design: intent-routing.md §2;
219
+ * dictionary keys intentClassifierKind + Chat/Systemone). Two
220
+ * explicit channels: chat = llm catalog route, systemone =
221
+ * hand-configured decision endpoint + model. */
222
+ const INTENT_KINDS = ['chat', 'systemone'];
223
+
224
+ /** Numeric intent fields: settings field, schema minimum, and the
225
+ * Host-side default (also the fallback for malformed stored values). */
226
+ const INTENT_NUMBER_FIELDS = [
227
+ { field: 'intentCacheTtlSeconds', min: 0, max: Infinity, def: 300 },
228
+ { field: 'intentSwitchConfidence', min: 0, max: 100, def: 80 },
229
+ { field: 'intentTimeoutMs', min: 200, max: Infinity, def: 60000 },
230
+ { field: 'intentHistoryMessages', min: 1, max: 20, def: 6 },
231
+ { field: 'intentHistoryMaxChars', min: 200, max: 20000, def: 2400 },
232
+ { field: 'intentContextWindow', min: 1024, max: Infinity, def: 200000 },
233
+ ];
234
+
235
+ /** Dictionary keys per numeric intent row. */
236
+ const INTENT_ROW_KEYS = {
237
+ intentCacheTtlSeconds: {
238
+ label: 'intentCacheTtlLabel',
239
+ help: 'intentCacheTtlHelp',
240
+ },
241
+ intentSwitchConfidence: {
242
+ label: 'intentSwitchConfidenceLabel',
243
+ help: 'intentSwitchConfidenceHelp',
244
+ },
245
+ intentTimeoutMs: { label: 'intentTimeoutLabel', help: 'intentTimeoutHelp' },
246
+ intentHistoryMessages: {
247
+ label: 'intentHistoryMessagesLabel',
248
+ help: 'intentHistoryMessagesHelp',
249
+ },
250
+ intentHistoryMaxChars: {
251
+ label: 'intentHistoryMaxCharsLabel',
252
+ help: 'intentHistoryMaxCharsHelp',
253
+ },
254
+ intentContextWindow: {
255
+ label: 'intentContextWindowLabel',
256
+ help: 'intentContextWindowHelp',
257
+ },
258
+ };
259
+
191
260
  /** Every settings field this page edits, in mutate order. */
192
261
  const ALL_FIELDS = [
193
262
  'uiLocale',
@@ -203,6 +272,16 @@ window.__ModuleLoader__.load({
203
272
  'wmSubagentCap',
204
273
  'teamsMode',
205
274
  'syncWhitelist',
275
+ // Intent recognition (design: intent-routing.md §2).
276
+ 'intentEnabled',
277
+ 'intentClassifierRoute',
278
+ 'intentClassifierEffort',
279
+ 'intentClassifierKind',
280
+ 'intentClassifierEndpoint',
281
+ 'intentClassifierSystemoneModel',
282
+ 'intentCandidates',
283
+ ...INTENT_NUMBER_FIELDS.map(({ field }) => field),
284
+ 'intentFallbackRoute',
206
285
  ];
207
286
 
208
287
  /** Stable identity of one provider/model route. */
@@ -240,6 +319,37 @@ window.__ModuleLoader__.load({
240
319
  : null;
241
320
  }
242
321
 
322
+ /** One well-formed intent candidate ({provider, model[, effort][,
323
+ * note]}) — the design's IntentCandidateSchema — or null; blank
324
+ * effort/note fields are dropped rather than kept. */
325
+ function sanitizeIntentCandidate(entry) {
326
+ if (entry === null || typeof entry !== 'object') return null;
327
+ const provider =
328
+ typeof entry.provider === 'string' ? entry.provider.trim() : '';
329
+ const model = typeof entry.model === 'string' ? entry.model.trim() : '';
330
+ const id =
331
+ typeof entry.id === 'string' && entry.id.trim() !== ''
332
+ ? entry.id.trim()
333
+ : '';
334
+ // Only identified rows (preset or custom) may live without a
335
+ // route — a preset still waiting for its model must survive the
336
+ // snapshot round-trip instead of being dropped like junk.
337
+ if (id === '' && (provider === '' || model === '')) return null;
338
+ const candidate = {
339
+ id,
340
+ preset: entry.preset === true,
341
+ enabled: entry.enabled !== false,
342
+ provider,
343
+ model,
344
+ note: typeof entry.note === 'string' ? entry.note : '',
345
+ };
346
+ if (typeof entry.label === 'string' && entry.label !== '')
347
+ candidate.label = entry.label;
348
+ if (typeof entry.effort === 'string' && entry.effort !== '')
349
+ candidate.effort = entry.effort;
350
+ return candidate;
351
+ }
352
+
243
353
  /** Is `value` one of the predefined candidates? */
244
354
  function isCandidate(value) {
245
355
  return LANGUAGES.some((language) => language.value === value);
@@ -327,6 +437,30 @@ window.__ModuleLoader__.load({
327
437
  wmSubagentCap: bool('wmSubagentCap', true),
328
438
  teamsMode: bool('teamsMode', false),
329
439
  syncWhitelist: bool('syncWhitelist', false),
440
+ // Intent recognition (design: intent-routing.md §2): route
441
+ // objects degrade to the frozen empty-route default; the
442
+ // candidate array follows the pool-array sanitizers.
443
+ intentEnabled: bool('intentEnabled', false),
444
+ intentClassifierRoute:
445
+ sanitizeRoute(value?.intentClassifierRoute) ??
446
+ ({ provider: '', model: '' }),
447
+ intentClassifierEffort: string('intentClassifierEffort'),
448
+ intentClassifierKind: INTENT_KINDS.includes(
449
+ value?.intentClassifierKind,
450
+ )
451
+ ? value.intentClassifierKind
452
+ : 'chat',
453
+ intentClassifierEndpoint: string('intentClassifierEndpoint'),
454
+ intentClassifierSystemoneModel: string('intentClassifierSystemoneModel'),
455
+ intentCandidates: Array.isArray(value?.intentCandidates)
456
+ ? value.intentCandidates.map(sanitizeIntentCandidate).filter(Boolean)
457
+ : [],
458
+ ...Object.fromEntries(
459
+ INTENT_NUMBER_FIELDS.map((spec) => [spec.field, number(spec)]),
460
+ ),
461
+ intentFallbackRoute:
462
+ sanitizeRoute(value?.intentFallbackRoute) ??
463
+ ({ provider: '', model: '' }),
330
464
  };
331
465
  }
332
466
 
@@ -377,6 +511,25 @@ window.__ModuleLoader__.load({
377
511
  wmSubagentCap: values.wmSubagentCap,
378
512
  teamsMode: values.teamsMode,
379
513
  syncWhitelist: values.syncWhitelist,
514
+ // Intent recognition: routes compare as route keys, the
515
+ // candidate array as an ordered sequence (list order is the
516
+ // classifier's #1..#n numbering), everything else verbatim.
517
+ intentEnabled: values.intentEnabled,
518
+ intentClassifierRoute: routeKey(values.intentClassifierRoute),
519
+ intentClassifierEffort: values.intentClassifierEffort,
520
+ intentClassifierKind: values.intentClassifierKind,
521
+ intentClassifierEndpoint: values.intentClassifierEndpoint,
522
+ intentClassifierSystemoneModel: values.intentClassifierSystemoneModel,
523
+ intentCandidates: values.intentCandidates
524
+ .map(
525
+ (entry) =>
526
+ `${entry.id ?? ''}\0${entry.enabled === false ? '0' : '1'}\0${routeKey(entry)}\0${entry.effort ?? ''}\0${entry.note ?? ''}`,
527
+ )
528
+ .join('|'),
529
+ ...Object.fromEntries(
530
+ INTENT_NUMBER_FIELDS.map(({ field }) => [field, values[field]]),
531
+ ),
532
+ intentFallbackRoute: routeKey(values.intentFallbackRoute),
380
533
  });
381
534
  }
382
535
 
@@ -388,10 +541,15 @@ window.__ModuleLoader__.load({
388
541
  /** Index one catalog answer: route key → display metadata. Models are
389
542
  * keyed by `id` (the sessionController wire shape: {id, name}) with
390
543
  * the group id as provider; the flattened `model` shape is accepted
391
- * too. Groups keep their wire order. */
544
+ * too. The switchman pseudo "Auto" group pins to the top (stable
545
+ * sort keeps every other group in wire order). */
392
546
  function indexCatalog(groups) {
393
547
  const index = new Map();
394
- for (const group of Array.isArray(groups) ? groups : []) {
548
+ const ordered = (Array.isArray(groups) ? groups : []).slice().sort(
549
+ (a, b) => (a?.id === 'dsh-switchman-auto' || a?.provider === 'dsh-switchman-auto' ? 0 : 1)
550
+ - (b?.id === 'dsh-switchman-auto' || b?.provider === 'dsh-switchman-auto' ? 0 : 1),
551
+ );
552
+ for (const group of ordered) {
395
553
  const providerName = group.name ?? group.id ?? '';
396
554
  for (const model of Array.isArray(group.models) ? group.models : []) {
397
555
  const provider = model.provider ?? group.id;
@@ -652,6 +810,69 @@ window.__ModuleLoader__.load({
652
810
  uiLocaleLabel: '界面语言',
653
811
  uiLocaleHelp: '「自动」跟随 DeepSeek Harness 应用语言;选择具体语言后,本插件界面将固定为该语言。',
654
812
  uiLocaleAuto: '自动(跟随 DSH)',
813
+ intentSectionTitle: '意图识别',
814
+ intentSectionDescription:
815
+ '选择「自动」模型时,按对话内容用分类器小模型自动路由候选模型;含厂商缓存续用窗口与切换置信度门槛。',
816
+ intentEnabledLabel: '启用意图识别',
817
+ intentEnabledHelp:
818
+ '主开关;关闭时选中「自动」直接走兜底链,不发起分类调用。',
819
+ intentClassifierRouteLabel: '分类器模型',
820
+ intentClassifierRouteHelp: '执行分类的小模型路由(provider + model)。',
821
+ intentClassifierEffortLabel: '分类器思考强度',
822
+ intentClassifierEffortHelp:
823
+ '可选固定 reasoning effort,优先于适配器默认。',
824
+ intentClassifierKindLabel: '分类器类型',
825
+ intentClassifierKindHelp:
826
+ 'chat = 直接选 LLM 目录里的对话模型;systemone = 手填端点+模型的决策类型。',
827
+ intentClassifierKindChat: '对话模型 (LLM 目录)',
828
+ intentClassifierKindSystemone: '决策类型 (手配端点+模型)',
829
+ intentClassifierEndpointLabel: '决策端点',
830
+ intentClassifierEndpointHelp: '分类器类型为「决策端点」时直接调用的 /v1/systemone 完整 URL(不经 llm 提供方)',
831
+ intentSystemoneModelLabel: '决策端点模型',
832
+ intentSystemoneModelHelp: '发送到决策端点的 model 参数(如 tev1:4b-mxfp8),自由填写,无需存在于模型供应商列表',
833
+ intentEndpointUnset: '未设置',
834
+ intentCandidatesLabel: '候选模型',
835
+ intentCandidatesHelp:
836
+ '可被自动路由到的模型清单;note 是给分类器看的一句话能力描述。',
837
+ intentCandidatesEmpty: '暂无候选:至少配置 1 个候选,「自动」才能路由。',
838
+ intentCandidatesAdd: '添加候选',
839
+ intentCandidateRemove: '删除候选',
840
+ intentCandidateEnable: '启用',
841
+ intentCandidateUnset: '已启用但未选模型:暂不参与路由,选一个模型即生效',
842
+ intentCandidatePresetBadge: '预置',
843
+ intentCandidateNotePlaceholder: '一句话能力描述(给分类器看)',
844
+ intentCacheTtlLabel: '缓存续用窗口(秒)',
845
+ intentCacheTtlHelp:
846
+ '该窗口内置信度不足时沿用当前模型,避免打断厂商 prompt cache。',
847
+ intentSwitchConfidenceLabel: '切换置信度(0–100)',
848
+ intentSwitchConfidenceHelp:
849
+ '分类置信度达到该值才允许离开当前模型。',
850
+ intentTimeoutLabel: '分类超时(毫秒)',
851
+ intentTimeoutHelp:
852
+ '分类调用预算;超时或失败时按兜底链放行(fail-open)。',
853
+ intentHistoryMessagesLabel: '历史消息条数(1–20)',
854
+ intentHistoryMessagesHelp: '喂给分类器的最近消息条数。',
855
+ intentHistoryMaxCharsLabel: '摘录字符预算',
856
+ intentContextWindowLabel: '上下文窗口',
857
+ intentContextWindowHelp: 'Auto 伪模型声明的上下文窗口(token),用于用量显示与自动压缩水位;当 0.8×窗口低于 force 水位时生效值会自动抬升,保证平滑交接先触发。',
858
+ intentHistoryMaxCharsHelp: '对话摘录的总字符上限(200–20000)。',
859
+ intentFallbackRouteLabel: '兜底模型',
860
+ intentFallbackRouteHelp:
861
+ '分类失败时依次:上一轮实际路由 → 兜底模型 → 候选第一位;留空用候选第一位。',
862
+ intentRouteUnset: '未设置',
863
+ intentEffortFollow: '跟随模型默认',
864
+ intentDockAuto: '自动',
865
+ intentDockUnrouted: '未路由',
866
+ intentDockUsing: '在用',
867
+ intentDockDecided: '决策',
868
+ intentDockUnconfigured: '意图识别未配置完整',
869
+ intentDockCache: '缓存窗口剩余 {seconds} 秒',
870
+ intentDockOverflowRisk: '候选 {model} 窗口 {window}k 低于交接线,近上限可能溢出',
871
+ intentDockTipCandidates: '候选:{count}',
872
+ intentDockTipOn: '开',
873
+ intentDockTipOff: '关',
874
+ intentModelLabelTrigger: '自动',
875
+ intentModelLabelFull: '自动(意图识别)',
655
876
  },
656
877
  en: {
657
878
  badge: 'Team autonomy',
@@ -810,6 +1031,75 @@ window.__ModuleLoader__.load({
810
1031
  uiLocaleLabel: 'Interface language',
811
1032
  uiLocaleHelp: "Auto follows the DeepSeek Harness app language; picking a language forces this plugin's UI into it.",
812
1033
  uiLocaleAuto: 'Auto (follow DSH)',
1034
+ intentSectionTitle: 'Intent routing',
1035
+ intentSectionDescription:
1036
+ "When the 'Auto' pseudo-model is picked, a small classifier model routes each turn to the best candidate by conversation content; with vendor cache-reuse window and a switch-confidence gate.",
1037
+ intentEnabledLabel: 'Enable intent routing',
1038
+ intentEnabledHelp:
1039
+ "Master switch; off = the 'Auto' selection goes straight down the fallback chain without any classifier call.",
1040
+ intentClassifierRouteLabel: 'Classifier model',
1041
+ intentClassifierRouteHelp:
1042
+ 'Route (provider + model) of the small model that classifies each turn.',
1043
+ intentClassifierEffortLabel: 'Classifier reasoning effort',
1044
+ intentClassifierEffortHelp:
1045
+ 'Optional pinned reasoning effort; overrides the adapter default.',
1046
+ intentClassifierKindLabel: 'Classifier kind',
1047
+ intentClassifierKindHelp:
1048
+ 'chat = a chat model from the LLM catalog; systemone = decision type with a hand-configured endpoint + model.',
1049
+ intentClassifierKindChat: 'Chat model (LLM catalog)',
1050
+ intentClassifierKindSystemone: 'Decision (custom endpoint+model)',
1051
+ intentClassifierEndpointLabel: 'Decision endpoint',
1052
+ intentClassifierEndpointHelp: 'Full /v1/systemone URL called directly when the classifier kind is the decision endpoint (no llm provider)',
1053
+ intentSystemoneModelLabel: 'Decision endpoint model',
1054
+ intentSystemoneModelHelp: 'Model name sent to the decision endpoint (e.g. tev1:4b-mxfp8); free text, no provider entry needed',
1055
+ intentEndpointUnset: 'Not set',
1056
+ intentCandidatesLabel: 'Candidates',
1057
+ intentCandidatesHelp:
1058
+ 'Models the router may pick; note is a one-line capability description the classifier reads.',
1059
+ intentCandidatesEmpty:
1060
+ 'No candidates yet: configure at least 1 candidate for routing to work.',
1061
+ intentCandidatesAdd: 'Add candidate',
1062
+ intentCandidateRemove: 'Remove candidate',
1063
+ intentCandidateEnable: 'Enable',
1064
+ intentCandidateUnset: 'Enabled but no model selected — excluded from routing until a model is chosen',
1065
+ intentCandidatePresetBadge: 'Preset',
1066
+ intentCandidateNotePlaceholder:
1067
+ 'one-line capability note (read by the classifier)',
1068
+ intentCacheTtlLabel: 'Cache reuse window (seconds)',
1069
+ intentCacheTtlHelp:
1070
+ 'Inside this window a low-confidence result keeps the current model, preserving the vendor prompt cache.',
1071
+ intentSwitchConfidenceLabel: 'Switch confidence (0–100)',
1072
+ intentSwitchConfidenceHelp:
1073
+ 'The classifier confidence must reach this value to leave the current model.',
1074
+ intentTimeoutLabel: 'Classification timeout (ms)',
1075
+ intentTimeoutHelp:
1076
+ 'Budget for the classifier call; timeout or failure falls back (fail-open).',
1077
+ intentHistoryMessagesLabel: 'History messages (1–20)',
1078
+ intentHistoryMessagesHelp:
1079
+ 'How many recent messages are fed to the classifier.',
1080
+ intentHistoryMaxCharsLabel: 'Excerpt character budget',
1081
+ intentContextWindowLabel: 'Context window',
1082
+ intentContextWindowHelp: 'Context window (tokens) declared by the Auto pseudo-model; drives the usage popover and auto-compaction pressure. When 0.8× the window is below the force watermark the effective value is raised automatically, so the smooth handover fires first.',
1083
+ intentHistoryMaxCharsHelp:
1084
+ 'Total character cap of the conversation excerpt (200–20000).',
1085
+ intentFallbackRouteLabel: 'Fallback model',
1086
+ intentFallbackRouteHelp:
1087
+ 'On classification failure: last routed model → fallback → first candidate; empty = first candidate.',
1088
+ intentRouteUnset: 'Not set',
1089
+ intentEffortFollow: 'model default',
1090
+ intentDockAuto: 'Auto',
1091
+ intentDockUnrouted: 'unrouted',
1092
+ intentDockUsing: 'using',
1093
+ intentDockDecided: 'decided',
1094
+ intentDockUnconfigured:
1095
+ 'Intent routing is not fully configured',
1096
+ intentDockCache: 'Cache window: {seconds}s left',
1097
+ intentDockOverflowRisk: 'candidate {model} window {window}k is below the handover line; may overflow near the cap',
1098
+ intentDockTipCandidates: 'candidates: {count}',
1099
+ intentDockTipOn: 'on',
1100
+ intentDockTipOff: 'off',
1101
+ intentModelLabelTrigger: 'Auto',
1102
+ intentModelLabelFull: 'Auto (intent routing)',
813
1103
  },
814
1104
  };
815
1105
 
@@ -959,6 +1249,69 @@ window.__ModuleLoader__.load({
959
1249
  uiLocaleLabel: '介面語言',
960
1250
  uiLocaleHelp: '「自動」跟隨 DeepSeek Harness 應用程式語言;指定語言後,本外掛的 UI 固定使用該語言。',
961
1251
  uiLocaleAuto: '自動(跟隨 DSH)',
1252
+ intentSectionTitle: '意圖識別',
1253
+ intentSectionDescription:
1254
+ '選擇「自動」模型時,按對話內容用分類器小模型自動路由候選模型;含供應商快取續用視窗與切換信心門檻。',
1255
+ intentEnabledLabel: '啟用意圖識別',
1256
+ intentEnabledHelp:
1257
+ '主開關;關閉時選中「自動」直接走備援鏈,不發起分類呼叫。',
1258
+ intentClassifierRouteLabel: '分類器模型',
1259
+ intentClassifierRouteHelp: '執行分類的小模型路由(provider + model)。',
1260
+ intentClassifierEffortLabel: '分類器思考強度',
1261
+ intentClassifierEffortHelp:
1262
+ '可選固定 reasoning effort,優先於配接器預設。',
1263
+ intentClassifierKindLabel: '分類器類型',
1264
+ intentClassifierKindHelp:
1265
+ 'chat = 直接選 LLM 目錄裡的對話模型;systemone = 手填端點+模型的決策類型。',
1266
+ intentClassifierKindChat: '對話模型 (LLM 目錄)',
1267
+ intentClassifierKindSystemone: '決策類型 (自配端點+模型)',
1268
+ intentClassifierEndpointLabel: '決策端點',
1269
+ intentClassifierEndpointHelp: '分類器類型為「決策端點」時直接呼叫的 /v1/systemone 完整 URL(不經 llm 提供方)',
1270
+ intentSystemoneModelLabel: '決策端點模型',
1271
+ intentSystemoneModelHelp: '傳送到決策端點的 model 參數(如 tev1:4b-mxfp8),自由填寫,無需存在於模型供應商清單',
1272
+ intentEndpointUnset: '未設定',
1273
+ intentCandidatesLabel: '候選模型',
1274
+ intentCandidatesHelp:
1275
+ '可被自動路由到的模型清單;note 是給分類器看的一句話能力描述。',
1276
+ intentCandidatesEmpty: '暫無候選:至少設定 1 個候選,「自動」才能路由。',
1277
+ intentCandidatesAdd: '新增候選',
1278
+ intentCandidateRemove: '刪除候選',
1279
+ intentCandidateEnable: '啟用',
1280
+ intentCandidateUnset: '已啟用但未選模型:暫不參與路由,選一個模型即生效',
1281
+ intentCandidatePresetBadge: '預置',
1282
+ intentCandidateNotePlaceholder: '一句話能力描述(給分類器看)',
1283
+ intentCacheTtlLabel: '快取續用視窗(秒)',
1284
+ intentCacheTtlHelp:
1285
+ '視窗內信心不足時沿用目前模型,避免打斷供應商 prompt cache。',
1286
+ intentSwitchConfidenceLabel: '切換信心(0–100)',
1287
+ intentSwitchConfidenceHelp:
1288
+ '分類信心達到該值才允許離開目前模型。',
1289
+ intentTimeoutLabel: '分類逾時(毫秒)',
1290
+ intentTimeoutHelp:
1291
+ '分類呼叫預算;逾時或失敗時按備援鏈放行(fail-open)。',
1292
+ intentHistoryMessagesLabel: '歷史訊息條數(1–20)',
1293
+ intentHistoryMessagesHelp: '餵給分類器的最近訊息條數。',
1294
+ intentHistoryMaxCharsLabel: '摘錄字元預算',
1295
+ intentContextWindowLabel: '上下文視窗',
1296
+ intentContextWindowHelp: 'Auto 偽模型宣告的上下文視窗(token),用於用量顯示與自動壓縮水位;當 0.8×視窗低於 force 水位時生效值會自動抬升,確保平滑交接先觸發。',
1297
+ intentHistoryMaxCharsHelp: '對話摘錄的總字元上限(200–20000)。',
1298
+ intentFallbackRouteLabel: '備援模型',
1299
+ intentFallbackRouteHelp:
1300
+ '分類失敗時依次:上一輪實際路由 → 備援模型 → 候選第一位;留空用候選第一位。',
1301
+ intentRouteUnset: '未設定',
1302
+ intentEffortFollow: '跟隨模型預設',
1303
+ intentDockAuto: '自動',
1304
+ intentDockUnrouted: '未路由',
1305
+ intentDockUsing: '在用',
1306
+ intentDockDecided: '決策',
1307
+ intentDockUnconfigured: '意圖識別未設定完整',
1308
+ intentDockCache: '快取視窗剩餘 {seconds} 秒',
1309
+ intentDockOverflowRisk: '候選 {model} 視窗 {window}k 低於交接線,接近上限可能溢出',
1310
+ intentDockTipCandidates: '候選:{count}',
1311
+ intentDockTipOn: '開',
1312
+ intentDockTipOff: '關',
1313
+ intentModelLabelTrigger: '自動',
1314
+ intentModelLabelFull: '自動(意圖識別)',
962
1315
  },
963
1316
  ja: {
964
1317
  badge: '自律チーム',
@@ -1096,6 +1449,73 @@ window.__ModuleLoader__.load({
1096
1449
  uiLocaleLabel: 'インターフェース言語',
1097
1450
  uiLocaleHelp: '「自動」は DeepSeek Harness アプリの言語に従います。言語を選ぶと、このプラグインの UI がその言語に固定されます。',
1098
1451
  uiLocaleAuto: '自動(DSH に従う)',
1452
+ intentSectionTitle: 'インテントルーティング',
1453
+ intentSectionDescription:
1454
+ '「自動」モデル選択時、会話内容から小型の分類モデルが各ターンを最適な候補へルーティングします。ベンダーキャッシュ再利用窓と切替信頼度ゲート付き。',
1455
+ intentEnabledLabel: 'インテントルーティングを有効化',
1456
+ intentEnabledHelp:
1457
+ 'マスタースイッチ。オフのとき「自動」は分類呼び出しを行わずフォールバックチェーンへ直行します。',
1458
+ intentClassifierRouteLabel: '分類モデル',
1459
+ intentClassifierRouteHelp:
1460
+ '分類を行う小型モデルのルート(provider + model)。',
1461
+ intentClassifierEffortLabel: '分類の思考強度',
1462
+ intentClassifierEffortHelp:
1463
+ '任意の reasoning effort 固定。アダプターのデフォルトより優先します。',
1464
+ intentClassifierKindLabel: '分類器の種類',
1465
+ intentClassifierKindHelp:
1466
+ 'chat = LLM カタログの対話モデルを直接選択。systemone = エンドポイント+モデルを手動設定する決定タイプ。',
1467
+ intentClassifierKindChat: '対話モデル (LLM カタログ)',
1468
+ intentClassifierKindSystemone: '決定タイプ (手動エンドポイント+モデル)',
1469
+ intentClassifierEndpointLabel: '判定エンドポイント',
1470
+ intentClassifierEndpointHelp: '分類器の種類が判定エンドポイントのとき直接呼び出す /v1/systemone の完全 URL(LLM プロバイダ不要)',
1471
+ intentSystemoneModelLabel: '決定エンドポイントのモデル',
1472
+ intentSystemoneModelHelp: '決定エンドポイントへ送る model パラメータ(例: tev1:4b-mxfp8)。自由入力で、プロバイダー一覧への登録は不要',
1473
+ intentEndpointUnset: '未設定',
1474
+ intentCandidatesLabel: '候補モデル',
1475
+ intentCandidatesHelp:
1476
+ '自動ルーティングされ得るモデル一覧。note は分類器が読む一行の能力説明です。',
1477
+ intentCandidatesEmpty:
1478
+ '候補なし:ルーティングには 1 件以上の候補が必要です。',
1479
+ intentCandidatesAdd: '候補を追加',
1480
+ intentCandidateRemove: '候補を削除',
1481
+ intentCandidateEnable: '有効化',
1482
+ intentCandidateUnset: '有効ですがモデル未選択:ルーティング対象外。モデルを選ぶと有効になります',
1483
+ intentCandidatePresetBadge: 'プリセット',
1484
+ intentCandidateNotePlaceholder: '一行の能力説明(分類器向け)',
1485
+ intentCacheTtlLabel: 'キャッシュ再利用窓(秒)',
1486
+ intentCacheTtlHelp:
1487
+ '窓内で信頼度が不足する場合は現在モデルを維持し、ベンダーの prompt cache を保ちます。',
1488
+ intentSwitchConfidenceLabel: '切替信頼度(0–100)',
1489
+ intentSwitchConfidenceHelp:
1490
+ 'この値に達した信頼度でのみ現在モデルから切り替えます。',
1491
+ intentTimeoutLabel: '分類タイムアウト(ミリ秒)',
1492
+ intentTimeoutHelp:
1493
+ '分類呼び出しの予算。タイムアウトや失敗はフォールバックで継続(fail-open)。',
1494
+ intentHistoryMessagesLabel: '履歴メッセージ数(1–20)',
1495
+ intentHistoryMessagesHelp: '分類器に渡す最近のメッセージ数。',
1496
+ intentHistoryMaxCharsLabel: '抜粋文字数予算',
1497
+ intentContextWindowLabel: 'コンテキストウィンドウ',
1498
+ intentContextWindowHelp: 'Auto 疑似モデルが宣言するコンテキストウィンドウ(トークン)。使用量表示と自動圧縮のしきい値に使用。0.8×ウィンドウが force 水位を下回ると実効値が自動的に引き上げられ、平滑なハンドオーバーが先に発火します。',
1499
+ intentHistoryMaxCharsHelp:
1500
+ '会話抜粋の合計文字上限(200–20000)。',
1501
+ intentFallbackRouteLabel: 'フォールバックモデル',
1502
+ intentFallbackRouteHelp:
1503
+ '分類失敗時:前回ルート → フォールバック → 候補の先頭。空欄は候補の先頭。',
1504
+ intentRouteUnset: '未設定',
1505
+ intentEffortFollow: 'モデルのデフォルト',
1506
+ intentDockAuto: '自動',
1507
+ intentDockUnrouted: '未ルーティング',
1508
+ intentDockUsing: '使用中',
1509
+ intentDockDecided: '判定',
1510
+ intentDockUnconfigured:
1511
+ 'インテントルーティングの設定が不完全です',
1512
+ intentDockCache: 'キャッシュ窓 残り {seconds} 秒',
1513
+ intentDockOverflowRisk: '候補 {model} のウィンドウ {window}k はハンドオーバー線未満。上限付近で溢れる恐れがあります。',
1514
+ intentDockTipCandidates: '候補:{count}',
1515
+ intentDockTipOn: 'オン',
1516
+ intentDockTipOff: 'オフ',
1517
+ intentModelLabelTrigger: '自動',
1518
+ intentModelLabelFull: '自動(インテントルーティング)',
1099
1519
  },
1100
1520
  ko: {
1101
1521
  badge: '자율 팀',
@@ -1233,6 +1653,73 @@ window.__ModuleLoader__.load({
1233
1653
  uiLocaleLabel: '인터페이스 언어',
1234
1654
  uiLocaleHelp: '자동은 DeepSeek Harness 앱 언어를 따르고, 언어를 직접 고르면 이 플러그인의 UI가 그 언어로 고정됩니다.',
1235
1655
  uiLocaleAuto: '자동(DSH 따르기)',
1656
+ intentSectionTitle: '의도 라우팅',
1657
+ intentSectionDescription:
1658
+ '「자동」모델 선택 시 대화 내용을 기반으로 소형 분류 모델이 각 턴을 최적 후보로 라우팅합니다. 벤더 캐시 재사용 창과 전환 신뢰도 게이트 포함.',
1659
+ intentEnabledLabel: '의도 라우팅 사용',
1660
+ intentEnabledHelp:
1661
+ '마스터 스위치. 끄면 「자동」은 분류 호출 없이 폴백 체인으로 바로 진행합니다.',
1662
+ intentClassifierRouteLabel: '분류 모델',
1663
+ intentClassifierRouteHelp:
1664
+ '분류를 수행하는 소형 모델 경로(provider + model)입니다.',
1665
+ intentClassifierEffortLabel: '분류 추론 강도',
1666
+ intentClassifierEffortHelp:
1667
+ '선택적 reasoning effort 고정. 어댑터 기본값보다 우선합니다.',
1668
+ intentClassifierKindLabel: '분류기 종류',
1669
+ intentClassifierKindHelp:
1670
+ 'chat = LLM 카탈로그의 대화 모델 직접 선택. systemone = 엔드포인트+모델을 수동 설정하는 결정 유형.',
1671
+ intentClassifierKindChat: '대화 모델 (LLM 카탈로그)',
1672
+ intentClassifierKindSystemone: '결정 유형 (수동 엔드포인트+모델)',
1673
+ intentClassifierEndpointLabel: '판정 엔드포인트',
1674
+ intentClassifierEndpointHelp: '분류기 종류가 판정 엔드포인트일 때 직접 호출하는 /v1/systemone 전체 URL(LLM 공급자 불필요)',
1675
+ intentSystemoneModelLabel: '결정 엔드포인트 모델',
1676
+ intentSystemoneModelHelp: '결정 엔드포인트로 보내는 model 매개변수(예: tev1:4b-mxfp8). 자유 입력이며 공급자 목록에 없어도 됨',
1677
+ intentEndpointUnset: '미설정',
1678
+ intentCandidatesLabel: '후보 모델',
1679
+ intentCandidatesHelp:
1680
+ '자동 라우팅될 수 있는 모델 목록. note는 분류기가 읽는 한 줄 능력 설명입니다.',
1681
+ intentCandidatesEmpty:
1682
+ '후보 없음: 라우팅하려면 후보를 1개 이상 설정하세요.',
1683
+ intentCandidatesAdd: '후보 추가',
1684
+ intentCandidateRemove: '후보 삭제',
1685
+ intentCandidateEnable: '사용',
1686
+ intentCandidateUnset: '활성화되었으나 모델 미선택: 라우팅에서 제외됩니다. 모델을 선택하면 활성화됩니다',
1687
+ intentCandidatePresetBadge: '프리셋',
1688
+ intentCandidateNotePlaceholder: '한 줄 능력 설명(분류기용)',
1689
+ intentCacheTtlLabel: '캐시 재사용 창(초)',
1690
+ intentCacheTtlHelp:
1691
+ '창 안에서 신뢰도가 부족하면 현재 모델을 유지해 벤더 prompt cache를 보존합니다.',
1692
+ intentSwitchConfidenceLabel: '전환 신뢰도(0–100)',
1693
+ intentSwitchConfidenceHelp:
1694
+ '이 값에 도달한 신뢰도에서만 현재 모델을 떠날 수 있습니다.',
1695
+ intentTimeoutLabel: '분류 타임아웃(밀리초)',
1696
+ intentTimeoutHelp:
1697
+ '분류 호출 예산. 시간 초과나 실패 시 폴백으로 계속합니다(fail-open).',
1698
+ intentHistoryMessagesLabel: '히스토리 메시지 수(1–20)',
1699
+ intentHistoryMessagesHelp: '분류기에 전달할 최근 메시지 수.',
1700
+ intentHistoryMaxCharsLabel: '발췌 문자 예산',
1701
+ intentContextWindowLabel: '컨텍스트 윈도우',
1702
+ intentContextWindowHelp: 'Auto 의사 모델이 선언하는 컨텍스트 윈도우(토큰). 사용량 표시와 자동 압축 기준에 사용. 0.8×윈도우가 force 임계값보다 낮으면 실제 적용 값이 자동으로 상향되어 부드러운 인계가 먼저 실행됩니다.',
1703
+ intentHistoryMaxCharsHelp:
1704
+ '대화 발췌의 총 문자 상한(200–20000).',
1705
+ intentFallbackRouteLabel: '폴백 모델',
1706
+ intentFallbackRouteHelp:
1707
+ '분류 실패 시: 이전 라우트 → 폴백 → 첫 후보. 빈 값은 첫 후보.',
1708
+ intentRouteUnset: '미설정',
1709
+ intentEffortFollow: '모델 기본값',
1710
+ intentDockAuto: '자동',
1711
+ intentDockUnrouted: '미라우팅',
1712
+ intentDockUsing: '사용 중',
1713
+ intentDockDecided: '판정',
1714
+ intentDockUnconfigured:
1715
+ '의도 라우팅 설정이 완전하지 않습니다',
1716
+ intentDockCache: '캐시 창 남은 {seconds}초',
1717
+ intentDockOverflowRisk: '후보 {model} 창 {window}k가 인계선 미만입니다. 상한 근처에서 넘칠 수 있습니다.',
1718
+ intentDockTipCandidates: '후보: {count}',
1719
+ intentDockTipOn: '켬',
1720
+ intentDockTipOff: '끔',
1721
+ intentModelLabelTrigger: '자동',
1722
+ intentModelLabelFull: '자동(의도 라우팅)',
1236
1723
  },
1237
1724
  de: {
1238
1725
  badge: 'Autonomes Team',
@@ -1370,6 +1857,75 @@ window.__ModuleLoader__.load({
1370
1857
  uiLocaleLabel: 'Sprache der Oberfläche',
1371
1858
  uiLocaleHelp: 'Auto folgt der App-Sprache von DeepSeek Harness; wählt man eine Sprache, wird die Oberfläche dieses Plugins darauf festgelegt.',
1372
1859
  uiLocaleAuto: 'Auto (DSH folgen)',
1860
+ intentSectionTitle: 'Intent-Routing',
1861
+ intentSectionDescription:
1862
+ 'Bei Wahl des Pseudo-Modells „Auto“ routet ein kleines Klassifikatormodell jeden Zug anhand des Gesprächsverlaufs zum besten Kandidaten; mit Cache-Wiederverwendungsfenster und Konfidenzschwelle.',
1863
+ intentEnabledLabel: 'Intent-Routing aktivieren',
1864
+ intentEnabledHelp:
1865
+ 'Hauptschalter; aus = die „Auto“-Auswahl geht ohne Klassifikatoraufruf direkt in die Fallback-Kette.',
1866
+ intentClassifierRouteLabel: 'Klassifikator-Modell',
1867
+ intentClassifierRouteHelp:
1868
+ 'Route (Provider + Modell) des kleinen Klassifikationsmodells.',
1869
+ intentClassifierEffortLabel: 'Reasoning-Aufwand des Klassifikators',
1870
+ intentClassifierEffortHelp:
1871
+ 'Optionaler fester Reasoning-Effort; hat Vorrang vor dem Adapter-Standard.',
1872
+ intentClassifierKindLabel: 'Klassifikator-Typ',
1873
+ intentClassifierKindHelp:
1874
+ 'chat = Chat-Modell direkt aus dem LLM-Katalog; systemone = Entscheidung mit eigenem Endpunkt + Modell.',
1875
+ intentClassifierKindChat: 'Chat-Modell (LLM-Katalog)',
1876
+ intentClassifierKindSystemone: 'Entscheidung (eigener Endpunkt+Modell)',
1877
+ intentClassifierEndpointLabel: 'Entscheidungs-Endpunkt',
1878
+ intentClassifierEndpointHelp: 'Vollständige /v1/systemone-URL, die direkt aufgerufen wird, wenn der Klassifikator-Typ der Entscheidungs-Endpunkt ist (ohne LLM-Provider)',
1879
+ intentSystemoneModelLabel: 'Modell des Entscheidungsendpunkts',
1880
+ intentSystemoneModelHelp: 'Modelname, der an den Entscheidungsendpunkt gesendet wird (z. B. tev1:4b-mxfp8); freier Text, kein Anbieter-Eintrag nötig',
1881
+ intentEndpointUnset: 'Nicht festgelegt',
1882
+ intentCandidatesLabel: 'Kandidaten',
1883
+ intentCandidatesHelp:
1884
+ 'Modelle, zu denen geroutet werden darf; note ist eine Einzeilen-Fähigkeitsbeschreibung für den Klassifikator.',
1885
+ intentCandidatesEmpty:
1886
+ 'Keine Kandidaten: Für das Routing ist mindestens 1 Kandidat nötig.',
1887
+ intentCandidatesAdd: 'Kandidat hinzufügen',
1888
+ intentCandidateRemove: 'Kandidat entfernen',
1889
+ intentCandidateEnable: 'Aktivieren',
1890
+ intentCandidateUnset: 'Aktiviert, aber ohne Modell — wird beim Routing übergangen; mit gewähltem Modell aktiv',
1891
+ intentCandidatePresetBadge: 'Preset',
1892
+ intentCandidateNotePlaceholder:
1893
+ 'Einzeilen-Fähigkeitsbeschreibung (für den Klassifikator)',
1894
+ intentCacheTtlLabel: 'Cache-Fenster (Sekunden)',
1895
+ intentCacheTtlHelp:
1896
+ 'Innerhalb des Fensters hält geringe Konfidenz das aktuelle Modell und schont den Anbieter-Prompt-Cache.',
1897
+ intentSwitchConfidenceLabel: 'Wechsel-Konfidenz (0–100)',
1898
+ intentSwitchConfidenceHelp:
1899
+ 'Erst ab dieser Klassifikationskonfidenz darf das aktuelle Modell verlassen werden.',
1900
+ intentTimeoutLabel: 'Klassifikations-Timeout (ms)',
1901
+ intentTimeoutHelp:
1902
+ 'Budget für den Klassifikatoraufruf; Timeout oder Fehler fällt zurück (fail-open).',
1903
+ intentHistoryMessagesLabel: 'Verlaufsnachrichten (1–20)',
1904
+ intentHistoryMessagesHelp:
1905
+ 'Wie viele letzte Nachrichten dem Klassifikator übergeben werden.',
1906
+ intentHistoryMaxCharsLabel: 'Auszug-Zeichenbudget',
1907
+ intentContextWindowLabel: 'Kontextfenster',
1908
+ intentContextWindowHelp: 'Vom Auto-Pseudo-Modell deklariertes Kontextfenster (Token); steuert Verbrauchsanzeige und Kompressionsdruck. Liegt 0,8× des Fensters unter der Force-Schwelle, wird der effektive Wert automatisch angehoben, damit die sanfte Übergabe zuerst auslöst.',
1909
+ intentHistoryMaxCharsHelp:
1910
+ 'Gesamtzeichengrenze des Gesprächsauszugs (200–20000).',
1911
+ intentFallbackRouteLabel: 'Fallback-Modell',
1912
+ intentFallbackRouteHelp:
1913
+ 'Bei Klassifikationsfehler: zuletzt geroutet → Fallback → erster Kandidat; leer = erster Kandidat.',
1914
+ intentRouteUnset: 'Nicht festgelegt',
1915
+ intentEffortFollow: 'Modellstandard',
1916
+ intentDockAuto: 'Auto',
1917
+ intentDockUnrouted: 'nicht geroutet',
1918
+ intentDockUsing: 'aktiv',
1919
+ intentDockDecided: 'entschieden',
1920
+ intentDockUnconfigured:
1921
+ 'Intent-Routing ist nicht vollständig konfiguriert',
1922
+ intentDockCache: 'Cache-Fenster: noch {seconds}s',
1923
+ intentDockOverflowRisk: 'Kandidat {model}: Fenster {window}k liegt unter der Übergabelinie; nahe dem Limit kann es überlaufen.',
1924
+ intentDockTipCandidates: 'Kandidaten: {count}',
1925
+ intentDockTipOn: 'ein',
1926
+ intentDockTipOff: 'aus',
1927
+ intentModelLabelTrigger: 'Auto',
1928
+ intentModelLabelFull: 'Auto (Intent-Routing)',
1373
1929
  },
1374
1930
  es: {
1375
1931
  badge: 'Equipo autónomo',
@@ -1507,6 +2063,75 @@ window.__ModuleLoader__.load({
1507
2063
  uiLocaleLabel: 'Idioma de la interfaz',
1508
2064
  uiLocaleHelp: 'Automático sigue el idioma de la app DeepSeek Harness; al elegir un idioma, la interfaz del plugin se fija en él.',
1509
2065
  uiLocaleAuto: 'Automático (seguir DSH)',
2066
+ intentSectionTitle: 'Enrutado por intención',
2067
+ intentSectionDescription:
2068
+ 'Al elegir el pseudo-modelo «Auto», un clasificador pequeño dirige cada turno al mejor candidato según la conversación; con ventana de reutilización de caché y umbral de confianza.',
2069
+ intentEnabledLabel: 'Activar enrutado por intención',
2070
+ intentEnabledHelp:
2071
+ 'Interruptor maestro; apagado = la selección «Auto» va directa a la cadena de reserva sin llamada al clasificador.',
2072
+ intentClassifierRouteLabel: 'Modelo clasificador',
2073
+ intentClassifierRouteHelp:
2074
+ 'Ruta (provider + model) del modelo pequeño que clasifica cada turno.',
2075
+ intentClassifierEffortLabel: 'Esfuerzo de razonamiento del clasificador',
2076
+ intentClassifierEffortHelp:
2077
+ 'Esfuerzo de razonamiento fijo opcional; prevalece sobre el valor por defecto del adaptador.',
2078
+ intentClassifierKindLabel: 'Tipo de clasificador',
2079
+ intentClassifierKindHelp:
2080
+ 'chat = modelo de chat del catálogo LLM; systemone = tipo de decisión con endpoint + modelo propios.',
2081
+ intentClassifierKindChat: 'Modelo de chat (catálogo LLM)',
2082
+ intentClassifierKindSystemone: 'Decisión (endpoint+modelo propios)',
2083
+ intentClassifierEndpointLabel: 'Endpoint de decisión',
2084
+ intentClassifierEndpointHelp: 'URL /v1/systemone completa que se llama directamente cuando el tipo de clasificador es el endpoint de decisión (sin proveedor LLM)',
2085
+ intentSystemoneModelLabel: 'Modelo del endpoint de decisión',
2086
+ intentSystemoneModelHelp: 'Parámetro model que se envía al endpoint de decisión (p. ej. tev1:4b-mxfp8); texto libre, no necesita estar en la lista de proveedores',
2087
+ intentEndpointUnset: 'Sin establecer',
2088
+ intentCandidatesLabel: 'Candidatos',
2089
+ intentCandidatesHelp:
2090
+ 'Modelos a los que se puede enrutar; note es una descripción de capacidad en una línea que lee el clasificador.',
2091
+ intentCandidatesEmpty:
2092
+ 'Sin candidatos: configure al menos 1 candidato para poder enrutar.',
2093
+ intentCandidatesAdd: 'Añadir candidato',
2094
+ intentCandidateRemove: 'Eliminar candidato',
2095
+ intentCandidateEnable: 'Activar',
2096
+ intentCandidateUnset: 'Activada pero sin modelo: excluida del enrutado; se activa al elegir un modelo',
2097
+ intentCandidatePresetBadge: 'Preset',
2098
+ intentCandidateNotePlaceholder:
2099
+ 'descripción de capacidad en una línea (para el clasificador)',
2100
+ intentCacheTtlLabel: 'Ventana de reutilización de caché (segundos)',
2101
+ intentCacheTtlHelp:
2102
+ 'Dentro de esta ventana una confianza baja mantiene el modelo actual y preserva la caché de prompt del proveedor.',
2103
+ intentSwitchConfidenceLabel: 'Confianza para cambiar (0–100)',
2104
+ intentSwitchConfidenceHelp:
2105
+ 'Solo con esta confianza de clasificación se permite dejar el modelo actual.',
2106
+ intentTimeoutLabel: 'Tiempo máximo de clasificación (ms)',
2107
+ intentTimeoutHelp:
2108
+ 'Presupuesto de la llamada al clasificador; tiempo agotado o fallo usa la reserva (fail-open).',
2109
+ intentHistoryMessagesLabel: 'Mensajes de historial (1–20)',
2110
+ intentHistoryMessagesHelp:
2111
+ 'Cuántos mensajes recientes se entregan al clasificador.',
2112
+ intentHistoryMaxCharsLabel: 'Presupuesto de caracteres del extracto',
2113
+ intentContextWindowLabel: 'Ventana de contexto',
2114
+ intentContextWindowHelp: 'Ventana de contexto (tokens) declarada por el pseudo-modelo Auto; controla la visualización de uso y la presión de compactación. Si 0,8× la ventana queda por debajo del umbral force, el valor efectivo se eleva automáticamente para que el traspaso suave se dispare primero.',
2115
+ intentHistoryMaxCharsHelp:
2116
+ 'Límite total de caracteres del extracto de conversación (200–20000).',
2117
+ intentFallbackRouteLabel: 'Modelo de reserva',
2118
+ intentFallbackRouteHelp:
2119
+ 'Si falla la clasificación: último modelo enrutado → reserva → primer candidato; vacío = primer candidato.',
2120
+ intentRouteUnset: 'Sin establecer',
2121
+ intentEffortFollow: 'predeterminado del modelo',
2122
+ intentDockAuto: 'Auto',
2123
+ intentDockUnrouted: 'sin enrutar',
2124
+ intentDockUsing: 'en uso',
2125
+ intentDockDecided: 'decidido',
2126
+ intentDockUnconfigured:
2127
+ 'El enrutado por intención no está completamente configurado',
2128
+ intentDockCache: 'Ventana de caché: {seconds}s restantes',
2129
+ intentDockOverflowRisk: 'El candidato {model} tiene una ventana de {window}k por debajo de la línea de traspaso; puede desbordarse cerca del límite.',
2130
+ intentDockTipCandidates: 'candidatos: {count}',
2131
+ intentDockTipOn: 'sí',
2132
+ intentDockTipOff: 'no',
2133
+ intentModelLabelTrigger: 'Auto',
2134
+ intentModelLabelFull: 'Auto (enrutado por intención)',
1510
2135
  },
1511
2136
  fr: {
1512
2137
  badge: 'Équipe autonome',
@@ -1644,6 +2269,75 @@ window.__ModuleLoader__.load({
1644
2269
  uiLocaleLabel: 'Langue de l’interface',
1645
2270
  uiLocaleHelp: 'Auto suit la langue de l’application DeepSeek Harness ; choisir une langue force l’interface du plugin dans cette langue.',
1646
2271
  uiLocaleAuto: 'Auto (suivre DSH)',
2272
+ intentSectionTitle: 'Routage par intention',
2273
+ intentSectionDescription:
2274
+ 'Lorsque le pseudo-modèle « Auto » est choisi, un petit classificateur route chaque tour vers le meilleur candidat d’après la conversation ; avec fenêtre de réutilisation du cache et seuil de confiance.',
2275
+ intentEnabledLabel: 'Activer le routage par intention',
2276
+ intentEnabledHelp:
2277
+ 'Interrupteur principal ; désactivé, la sélection « Auto » passe directement par la chaîne de secours sans appel au classificateur.',
2278
+ intentClassifierRouteLabel: 'Modèle classificateur',
2279
+ intentClassifierRouteHelp:
2280
+ 'Route (provider + model) du petit modèle qui classe chaque tour.',
2281
+ intentClassifierEffortLabel: 'Effort de raisonnement du classificateur',
2282
+ intentClassifierEffortHelp:
2283
+ 'Effort de raisonnement épinglé facultatif ; prime sur la valeur par défaut de l’adaptateur.',
2284
+ intentClassifierKindLabel: 'Type de classificateur',
2285
+ intentClassifierKindHelp:
2286
+ 'chat = modèle de chat du catalogue LLM ; systemone = type de décision avec endpoint + modèle personnalisés.',
2287
+ intentClassifierKindChat: 'Modèle de chat (catalogue LLM)',
2288
+ intentClassifierKindSystemone: 'Décision (endpoint+modèle personnalisés)',
2289
+ intentClassifierEndpointLabel: 'Endpoint de décision',
2290
+ intentClassifierEndpointHelp: 'URL /v1/systemone complète appelée directement quand le type de classificateur est l\'endpoint de décision (sans fournisseur LLM)',
2291
+ intentSystemoneModelLabel: 'Modèle du point de décision',
2292
+ intentSystemoneModelHelp: 'Paramètre model envoyé au point de décision (p. ex. tev1:4b-mxfp8) ; texte libre, inutile qu\'il figure dans la liste des fournisseurs',
2293
+ intentEndpointUnset: 'Non défini',
2294
+ intentCandidatesLabel: 'Candidats',
2295
+ intentCandidatesHelp:
2296
+ 'Modèles vers lesquels le routage peut aller ; note est une description de capacité d’une ligne lue par le classificateur.',
2297
+ intentCandidatesEmpty:
2298
+ 'Aucun candidat : configurez au moins 1 candidat pour permettre le routage.',
2299
+ intentCandidatesAdd: 'Ajouter un candidat',
2300
+ intentCandidateRemove: 'Supprimer le candidat',
2301
+ intentCandidateEnable: 'Activer',
2302
+ intentCandidateUnset: 'Activée sans modèle — exclue du routage ; choisir un modèle l\'active',
2303
+ intentCandidatePresetBadge: 'Preset',
2304
+ intentCandidateNotePlaceholder:
2305
+ 'description de capacité en une ligne (pour le classificateur)',
2306
+ intentCacheTtlLabel: 'Fenêtre de réutilisation du cache (secondes)',
2307
+ intentCacheTtlHelp:
2308
+ 'Dans cette fenêtre, une confiance insuffisante conserve le modèle actuel et préserve le cache de prompt du fournisseur.',
2309
+ intentSwitchConfidenceLabel: 'Confiance de bascule (0–100)',
2310
+ intentSwitchConfidenceHelp:
2311
+ 'Seule une confiance de classification atteignant cette valeur permet de quitter le modèle actuel.',
2312
+ intentTimeoutLabel: 'Délai de classification (ms)',
2313
+ intentTimeoutHelp:
2314
+ 'Budget de l’appel au classificateur ; dépassé ou en échec, le secours prend le relais (fail-open).',
2315
+ intentHistoryMessagesLabel: 'Messages d’historique (1–20)',
2316
+ intentHistoryMessagesHelp:
2317
+ 'Nombre de messages récents transmis au classificateur.',
2318
+ intentHistoryMaxCharsLabel: 'Budget de caractères de l’extrait',
2319
+ intentContextWindowLabel: 'Fenêtre de contexte',
2320
+ intentContextWindowHelp: 'Fenêtre de contexte (jetons) déclarée par le pseudo-modèle Auto ; pilote l\'affichage d\'utilisation et le seuil de compression. Si 0,8× la fenêtre passe sous le seuil force, la valeur effective est relevée automatiquement pour que la passation douce se déclenche d\'abord.',
2321
+ intentHistoryMaxCharsHelp:
2322
+ 'Plafond total de caractères de l’extrait de conversation (200–20000).',
2323
+ intentFallbackRouteLabel: 'Modèle de secours',
2324
+ intentFallbackRouteHelp:
2325
+ 'En cas d’échec : dernier modèle routé → secours → premier candidat ; vide = premier candidat.',
2326
+ intentRouteUnset: 'Non défini',
2327
+ intentEffortFollow: 'par défaut du modèle',
2328
+ intentDockAuto: 'Auto',
2329
+ intentDockUnrouted: 'non routé',
2330
+ intentDockUsing: 'en cours',
2331
+ intentDockDecided: 'décidé',
2332
+ intentDockUnconfigured:
2333
+ 'Le routage par intention n’est pas entièrement configuré',
2334
+ intentDockCache: 'Fenêtre de cache : {seconds}s restantes',
2335
+ intentDockOverflowRisk: 'Le candidat {model} a une fenêtre de {window}k sous la ligne de passation ; risque de débordement près du plafond.',
2336
+ intentDockTipCandidates: 'candidats : {count}',
2337
+ intentDockTipOn: 'activé',
2338
+ intentDockTipOff: 'désactivé',
2339
+ intentModelLabelTrigger: 'Auto',
2340
+ intentModelLabelFull: 'Auto (routage par intention)',
1647
2341
  },
1648
2342
  it: {
1649
2343
  badge: 'Squadra autonoma',
@@ -1781,6 +2475,75 @@ window.__ModuleLoader__.load({
1781
2475
  uiLocaleLabel: 'Lingua dell’interfaccia',
1782
2476
  uiLocaleHelp: 'Auto segue la lingua dell’app DeepSeek Harness; scegliere una lingua forza l’interfaccia del plugin in quella lingua.',
1783
2477
  uiLocaleAuto: 'Auto (segui DSH)',
2478
+ intentSectionTitle: 'Instradamento per intento',
2479
+ intentSectionDescription:
2480
+ 'Quando si sceglie il pseudo-modello «Auto», un piccolo classificatore instrada ogni turno al candidato migliore in base alla conversazione; con finestra di riutilizzo della cache e soglia di confidenza.',
2481
+ intentEnabledLabel: 'Attiva instradamento per intento',
2482
+ intentEnabledHelp:
2483
+ 'Interruttore principale; spento = la selezione «Auto» va direttamente alla catena di riserva senza chiamate di classificazione.',
2484
+ intentClassifierRouteLabel: 'Modello classificatore',
2485
+ intentClassifierRouteHelp:
2486
+ 'Route (provider + model) del piccolo modello che classifica ogni turno.',
2487
+ intentClassifierEffortLabel: 'Sforzo di ragionamento del classificatore',
2488
+ intentClassifierEffortHelp:
2489
+ 'Sforzo di ragionamento fissato facoltativo; ha precedenza sul valore predefinito dell’adattatore.',
2490
+ intentClassifierKindLabel: 'Tipo di classificatore',
2491
+ intentClassifierKindHelp:
2492
+ 'chat = modello di chat dal catalogo LLM; systemone = tipo di decisione con endpoint + modello personalizzati.',
2493
+ intentClassifierKindChat: 'Modello di chat (catalogo LLM)',
2494
+ intentClassifierKindSystemone: 'Decisione (endpoint+modello personalizzati)',
2495
+ intentClassifierEndpointLabel: 'Endpoint di decisione',
2496
+ intentClassifierEndpointHelp: 'URL /v1/systemone completa chiamata direttamente quando il tipo di classificatore è l\'endpoint di decisione (senza provider LLM)',
2497
+ intentSystemoneModelLabel: 'Modello dell\'endpoint di decisione',
2498
+ intentSystemoneModelHelp: 'Parametro model inviato all\'endpoint di decisione (es. tev1:4b-mxfp8); testo libero, non deve stare nell\'elenco dei provider',
2499
+ intentEndpointUnset: 'Non impostato',
2500
+ intentCandidatesLabel: 'Candidati',
2501
+ intentCandidatesHelp:
2502
+ 'Modelli verso cui è possibile instradare; note è una descrizione di capacità in una riga letta dal classificatore.',
2503
+ intentCandidatesEmpty:
2504
+ 'Nessun candidato: configura almeno 1 candidato per consentire l’instradamento.',
2505
+ intentCandidatesAdd: 'Aggiungi candidato',
2506
+ intentCandidateRemove: 'Rimuovi candidato',
2507
+ intentCandidateEnable: 'Attiva',
2508
+ intentCandidateUnset: 'Attiva ma senza modello: esclusa dal routing; si attiva scegliendo un modello',
2509
+ intentCandidatePresetBadge: 'Preset',
2510
+ intentCandidateNotePlaceholder:
2511
+ 'descrizione di capacità in una riga (per il classificatore)',
2512
+ intentCacheTtlLabel: 'Finestra di riutilizzo cache (secondi)',
2513
+ intentCacheTtlHelp:
2514
+ 'In questa finestra una confidenza bassa mantiene il modello attuale e preserva la cache dei prompt del fornitore.',
2515
+ intentSwitchConfidenceLabel: 'Confidenza di cambio (0–100)',
2516
+ intentSwitchConfidenceHelp:
2517
+ 'Solo con questa confidenza di classificazione è possibile abbandonare il modello attuale.',
2518
+ intentTimeoutLabel: 'Timeout di classificazione (ms)',
2519
+ intentTimeoutHelp:
2520
+ 'Budget per la chiamata al classificatore; timeout o errore passano alla riserva (fail-open).',
2521
+ intentHistoryMessagesLabel: 'Messaggi di cronologia (1–20)',
2522
+ intentHistoryMessagesHelp:
2523
+ 'Quanti messaggi recenti vengono passati al classificatore.',
2524
+ intentHistoryMaxCharsLabel: 'Budget di caratteri dell’estratto',
2525
+ intentContextWindowLabel: 'Finestra di contesto',
2526
+ intentContextWindowHelp: 'Finestra di contesto (token) dichiarata dal pseudo-modello Auto; determina la visualizzazione dell\'uso e la soglia di compressione. Se 0,8× la finestra scende sotto la soglia force, il valore effettivo viene innalzato automaticamente così la consegna fluida scatta prima.',
2527
+ intentHistoryMaxCharsHelp:
2528
+ 'Limite totale di caratteri dell’estratto della conversazione (200–20000).',
2529
+ intentFallbackRouteLabel: 'Modello di riserva',
2530
+ intentFallbackRouteHelp:
2531
+ 'In caso di errore: ultimo modello instradato → riserva → primo candidato; vuoto = primo candidato.',
2532
+ intentRouteUnset: 'Non impostato',
2533
+ intentEffortFollow: 'predefinito del modello',
2534
+ intentDockAuto: 'Auto',
2535
+ intentDockUnrouted: 'non instradato',
2536
+ intentDockUsing: 'in uso',
2537
+ intentDockDecided: 'deciso',
2538
+ intentDockUnconfigured:
2539
+ 'L’instradamento per intento non è completamente configurato',
2540
+ intentDockCache: 'Finestra cache: {seconds}s rimanenti',
2541
+ intentDockOverflowRisk: 'Il candidato {model} ha una finestra di {window}k sotto la linea di consegna; può traboccare vicino al limite.',
2542
+ intentDockTipCandidates: 'candidati: {count}',
2543
+ intentDockTipOn: 'on',
2544
+ intentDockTipOff: 'off',
2545
+ intentModelLabelTrigger: 'Auto',
2546
+ intentModelLabelFull: 'Auto (instradamento per intento)',
1784
2547
  },
1785
2548
  pt: {
1786
2549
  badge: 'Equipe autônoma',
@@ -1918,6 +2681,75 @@ window.__ModuleLoader__.load({
1918
2681
  uiLocaleLabel: 'Idioma da interface',
1919
2682
  uiLocaleHelp: 'Auto segue o idioma do aplicativo DeepSeek Harness; escolher um idioma força a interface do plugin para ele.',
1920
2683
  uiLocaleAuto: 'Auto (seguir DSH)',
2684
+ intentSectionTitle: 'Roteamento por intenção',
2685
+ intentSectionDescription:
2686
+ 'Ao escolher o pseudo-modelo «Auto», um classificador pequeno direciona cada rodada ao melhor candidato conforme a conversa; com janela de reuso de cache e limite de confiança.',
2687
+ intentEnabledLabel: 'Ativar roteamento por intenção',
2688
+ intentEnabledHelp:
2689
+ 'Interruptor mestre; desligado = a seleção «Auto» vai direto à cadeia de reserva sem chamada ao classificador.',
2690
+ intentClassifierRouteLabel: 'Modelo classificador',
2691
+ intentClassifierRouteHelp:
2692
+ 'Rota (provider + model) do modelo pequeno que classifica cada rodada.',
2693
+ intentClassifierEffortLabel: 'Esforço de raciocínio do classificador',
2694
+ intentClassifierEffortHelp:
2695
+ 'Esforço de raciocínio fixo opcional; prevalece sobre o padrão do adaptador.',
2696
+ intentClassifierKindLabel: 'Tipo de classificador',
2697
+ intentClassifierKindHelp:
2698
+ 'chat = modelo de conversa do catálogo LLM; systemone = tipo de decisão com endpoint + modelo personalizados.',
2699
+ intentClassifierKindChat: 'Modelo de conversa (catálogo LLM)',
2700
+ intentClassifierKindSystemone: 'Decisão (endpoint+modelo personalizados)',
2701
+ intentClassifierEndpointLabel: 'Endpoint de decisão',
2702
+ intentClassifierEndpointHelp: 'URL /v1/systemone completa chamada diretamente quando o tipo de classificador é o endpoint de decisão (sem provedor LLM)',
2703
+ intentSystemoneModelLabel: 'Modelo do endpoint de decisão',
2704
+ intentSystemoneModelHelp: 'Parâmetro model enviado ao endpoint de decisão (ex.: tev1:4b-mxfp8); texto livre, não precisa estar na lista de provedores',
2705
+ intentEndpointUnset: 'Não definido',
2706
+ intentCandidatesLabel: 'Candidatos',
2707
+ intentCandidatesHelp:
2708
+ 'Modelos para os quais o roteamento pode ir; note é uma descrição de capacidade em uma linha lida pelo classificador.',
2709
+ intentCandidatesEmpty:
2710
+ 'Sem candidatos: configure pelo menos 1 candidato para permitir o roteamento.',
2711
+ intentCandidatesAdd: 'Adicionar candidato',
2712
+ intentCandidateRemove: 'Remover candidato',
2713
+ intentCandidateEnable: 'Ativar',
2714
+ intentCandidateUnset: 'Ativada sem modelo: excluída do roteamento; escolher um modelo ativa',
2715
+ intentCandidatePresetBadge: 'Preset',
2716
+ intentCandidateNotePlaceholder:
2717
+ 'descrição de capacidade em uma linha (para o classificador)',
2718
+ intentCacheTtlLabel: 'Janela de reuso de cache (segundos)',
2719
+ intentCacheTtlHelp:
2720
+ 'Dentro desta janela, confiança baixa mantém o modelo atual e preserva o cache de prompt do fornecedor.',
2721
+ intentSwitchConfidenceLabel: 'Confiança para troca (0–100)',
2722
+ intentSwitchConfidenceHelp:
2723
+ 'Só com essa confiança de classificação é permitido sair do modelo atual.',
2724
+ intentTimeoutLabel: 'Tempo limite da classificação (ms)',
2725
+ intentTimeoutHelp:
2726
+ 'Orçamento da chamada ao classificador; tempo esgotado ou falha usa a reserva (fail-open).',
2727
+ intentHistoryMessagesLabel: 'Mensagens de histórico (1–20)',
2728
+ intentHistoryMessagesHelp:
2729
+ 'Quantas mensagens recentes são entregues ao classificador.',
2730
+ intentHistoryMaxCharsLabel: 'Orçamento de caracteres do trecho',
2731
+ intentContextWindowLabel: 'Janela de contexto',
2732
+ intentContextWindowHelp: 'Janela de contexto (tokens) declarada pelo pseudo-modelo Auto; controla a exibição de uso e o limiar de compressão. Se 0,8× a janela ficar abaixo do limiar force, o valor efetivo é elevado automaticamente para que o handover suave dispare primeiro.',
2733
+ intentHistoryMaxCharsHelp:
2734
+ 'Limite total de caracteres do trecho da conversa (200–20000).',
2735
+ intentFallbackRouteLabel: 'Modelo de reserva',
2736
+ intentFallbackRouteHelp:
2737
+ 'Se a classificação falhar: último modelo roteado → reserva → primeiro candidato; vazio = primeiro candidato.',
2738
+ intentRouteUnset: 'Não definido',
2739
+ intentEffortFollow: 'padrão do modelo',
2740
+ intentDockAuto: 'Auto',
2741
+ intentDockUnrouted: 'sem rota',
2742
+ intentDockUsing: 'em uso',
2743
+ intentDockDecided: 'decidido',
2744
+ intentDockUnconfigured:
2745
+ 'O roteamento por intenção não está totalmente configurado',
2746
+ intentDockCache: 'Janela de cache: {seconds}s restantes',
2747
+ intentDockOverflowRisk: 'O candidato {model} tem uma janela de {window}k abaixo da linha de handover; pode transbordar perto do limite.',
2748
+ intentDockTipCandidates: 'candidatos: {count}',
2749
+ intentDockTipOn: 'ligado',
2750
+ intentDockTipOff: 'desligado',
2751
+ intentModelLabelTrigger: 'Auto',
2752
+ intentModelLabelFull: 'Auto (roteamento por intenção)',
1921
2753
  },
1922
2754
  ru: {
1923
2755
  badge: 'Автономная команда',
@@ -2055,6 +2887,75 @@ window.__ModuleLoader__.load({
2055
2887
  uiLocaleLabel: 'Язык интерфейса',
2056
2888
  uiLocaleHelp: '«Авто» следует языку приложения DeepSeek Harness; выбор конкретного языка принудительно переводит на него интерфейс плагина.',
2057
2889
  uiLocaleAuto: 'Авто (следовать DSH)',
2890
+ intentSectionTitle: 'Маршрутизация по намерению',
2891
+ intentSectionDescription:
2892
+ 'При выборе псевдомодели «Авто» небольшой классификатор направляет каждый ход к лучшему кандидату по содержанию диалога; с окном повторного использования кэша и порогом уверенности.',
2893
+ intentEnabledLabel: 'Включить маршрутизацию по намерению',
2894
+ intentEnabledHelp:
2895
+ 'Главный выключатель; выкл. = выбор «Авто» сразу идёт по резервной цепочке без вызова классификатора.',
2896
+ intentClassifierRouteLabel: 'Модель-классификатор',
2897
+ intentClassifierRouteHelp:
2898
+ 'Маршрут (provider + model) небольшой модели, классифицирующей каждый ход.',
2899
+ intentClassifierEffortLabel: 'Усилие рассуждения классификатора',
2900
+ intentClassifierEffortHelp:
2901
+ 'Необязательное фиксированное reasoning effort; важнее значения адаптера по умолчанию.',
2902
+ intentClassifierKindLabel: 'Тип классификатора',
2903
+ intentClassifierKindHelp:
2904
+ 'chat = диалоговая модель из каталога LLM; systemone = тип решения с собственными endpoint + моделью.',
2905
+ intentClassifierKindChat: 'Диалоговая модель (каталог LLM)',
2906
+ intentClassifierKindSystemone: 'Решение (свой endpoint+модель)',
2907
+ intentClassifierEndpointLabel: 'Endpoint решений',
2908
+ intentClassifierEndpointHelp: 'Полный URL /v1/systemone, вызываемый напрямую, когда тип классификатора — endpoint решений (без LLM-провайдера)',
2909
+ intentSystemoneModelLabel: 'Модель endpoint решений',
2910
+ intentSystemoneModelHelp: 'Параметр model, отправляемый на endpoint решений (напр. tev1:4b-mxfp8); свободный текст, запись в списке провайдеров не нужна',
2911
+ intentEndpointUnset: 'Не задано',
2912
+ intentCandidatesLabel: 'Кандидаты',
2913
+ intentCandidatesHelp:
2914
+ 'Модели, к которым возможна маршрутизация; note — описание возможностей одной строкой для классификатора.',
2915
+ intentCandidatesEmpty:
2916
+ 'Нет кандидатов: для маршрутизации нужен хотя бы 1 кандидат.',
2917
+ intentCandidatesAdd: 'Добавить кандидата',
2918
+ intentCandidateRemove: 'Удалить кандидата',
2919
+ intentCandidateEnable: 'Включить',
2920
+ intentCandidateUnset: 'Включено, но модель не выбрана: исключено из маршрутизации; активируется после выбора модели',
2921
+ intentCandidatePresetBadge: 'Пресет',
2922
+ intentCandidateNotePlaceholder:
2923
+ 'описание возможностей одной строкой (для классификатора)',
2924
+ intentCacheTtlLabel: 'Окно повторного использования кэша (секунды)',
2925
+ intentCacheTtlHelp:
2926
+ 'Внутри окна при низкой уверенности сохраняется текущая модель — prompt-кэш провайдера не разрывается.',
2927
+ intentSwitchConfidenceLabel: 'Уверенность переключения (0–100)',
2928
+ intentSwitchConfidenceHelp:
2929
+ 'Только при такой уверенности классификации можно покинуть текущую модель.',
2930
+ intentTimeoutLabel: 'Тайм-аут классификации (мс)',
2931
+ intentTimeoutHelp:
2932
+ 'Бюджет вызова классификатора; тайм-аут или сбой — резервная цепочка (fail-open).',
2933
+ intentHistoryMessagesLabel: 'Сообщений истории (1–20)',
2934
+ intentHistoryMessagesHelp:
2935
+ 'Сколько последних сообщений передаётся классификатору.',
2936
+ intentHistoryMaxCharsLabel: 'Лимит символов выборки',
2937
+ intentContextWindowLabel: 'Окно контекста',
2938
+ intentContextWindowHelp: 'Окно контекста (токены), объявляемое псевдомоделью Auto; определяет отображение использования и порог сжатия. Если 0,8× окна опускается ниже порога force, действующее значение автоматически поднимается, чтобы плавная передача сработала раньше.',
2939
+ intentHistoryMaxCharsHelp:
2940
+ 'Общий лимит символов выборки диалога (200–20000).',
2941
+ intentFallbackRouteLabel: 'Резервная модель',
2942
+ intentFallbackRouteHelp:
2943
+ 'При сбое классификации: прошлая модель → резерв → первый кандидат; пусто = первый кандидат.',
2944
+ intentRouteUnset: 'Не задано',
2945
+ intentEffortFollow: 'по умолчанию модели',
2946
+ intentDockAuto: 'Авто',
2947
+ intentDockUnrouted: 'не маршрутизировано',
2948
+ intentDockUsing: 'в работе',
2949
+ intentDockDecided: 'решено',
2950
+ intentDockUnconfigured:
2951
+ 'Маршрутизация по намерению настроена не полностью',
2952
+ intentDockCache: 'Окно кэша: осталось {seconds} с',
2953
+ intentDockOverflowRisk: 'Кандидат {model} с окном {window}k ниже линии передачи; возможно переполнение у лимита.',
2954
+ intentDockTipCandidates: 'кандидаты: {count}',
2955
+ intentDockTipOn: 'вкл',
2956
+ intentDockTipOff: 'выкл',
2957
+ intentModelLabelTrigger: 'Авто',
2958
+ intentModelLabelFull: 'Авто (маршрутизация по намерению)',
2058
2959
  },
2059
2960
  };
2060
2961
 
@@ -2307,11 +3208,17 @@ window.__ModuleLoader__.load({
2307
3208
  * 1. Guidance chip — DSH refuses to send pasted images while the
2308
3209
  * session model has no image input; switchman's unlock is the
2309
3210
  * /vision command backed by the vision pool. The chip renders
2310
- * whenever image support is not positively confirmed (probe
2311
- * false, or null = unresolved — the common live case: the
2312
- * Host-side model-info resolution often cannot resolve custom
2313
- * routes), polling the Host route at a slow fixed cadence;
2314
- * any failure renders nothing.
3211
+ * ONLY when the Host probe positively confirms a text-only
3212
+ * model (imageCapable === false); null (unresolved — no llm
3213
+ * service, unknown session, fresh auto session with no routing
3214
+ * decision yet) stays silent, because claiming "not supported"
3215
+ * without a verdict was this feature's own bug (it warned on
3216
+ * vision-capable models too). Refreshes IMMEDIATELY on a model
3217
+ * switch (the modelSelection projection's `next` flips the moment
3218
+ * the dropdown pick lands) and after each send (an auto session's
3219
+ * decided route may change) via staggered catch-up fetches, with
3220
+ * the slow 15s poll only as a fallback; any failure renders
3221
+ * nothing.
2315
3222
  * 2. Reactive auto-conversion (v1) — when a plain send is refused
2316
3223
  * host-side (session/attachment-invalid), rewrite the draft as
2317
3224
  * "/vision <original text>" and resubmit once; the claimed
@@ -2320,13 +3227,19 @@ window.__ModuleLoader__.load({
2320
3227
  * the authoritative image-gate signal; the probe's
2321
3228
  * imageCapable only brakes the conversion when it positively
2322
3229
  * says true. Session-scoped dock entries receive the standard
2323
- * kit (inputActions, useInput, useSession) automatically;
2324
- * loop safety = one conversion per promptError object
2325
- * identity + never converting a draft that already starts
2326
- * with "/". */
2327
- function VisionDockEntry({ t, sessionId, inputActions, useInput, useSession }) {
3230
+ * kit (inputActions, useInput, useSession, useProjection)
3231
+ * automatically; loop safety = one conversion per promptError
3232
+ * object identity + never converting a draft that already
3233
+ * starts with "/". */
3234
+ function VisionDockEntry({ t, sessionId, inputActions, useInput, useSession, useProjection }) {
2328
3235
  const [vision, setVision] = React.useState(null);
2329
3236
  const [notice, setNotice] = React.useState(false);
3237
+ // Refresh pulse — bumped by model-selection changes (composer
3238
+ // dropdown) and send bursts (draft dropping to empty), mirroring
3239
+ // IntentDockEntry's burst mechanism so the verdict updates
3240
+ // immediately on a model switch instead of waiting out the
3241
+ // 15s fallback poll. Restarts the probe effect below.
3242
+ const [pulse, setPulse] = React.useState(0);
2330
3243
  const handled = React.useRef(null);
2331
3244
  const latest = React.useRef({ vision: null, draft: '', attachmentIds: [] });
2332
3245
  React.useEffect(() => {
@@ -2337,12 +3250,22 @@ window.__ModuleLoader__.load({
2337
3250
  // imageCapable from that session's model selection (subagent
2338
3251
  // sessions included); without it the Host falls back to the
2339
3252
  // root session and mislabels text-only children. Restarting
2340
- // the loop on `sessionId` re-probes immediately on a switch.
3253
+ // the loop on `sessionId`/`pulse` re-probes immediately on a
3254
+ // switch or a model-selection change.
2341
3255
  const query =
2342
3256
  typeof sessionId === 'string' && sessionId !== ''
2343
3257
  ? `?session=${encodeURIComponent(sessionId)}`
2344
3258
  : '';
2345
3259
  const tick = async () => {
3260
+ // One fetch at a time; a catch-up landing mid-flight
3261
+ // queues exactly one short-delay re-run instead of
3262
+ // forking a second poll loop (same guard as the intent
3263
+ // chip).
3264
+ if (inflight) {
3265
+ rerun = true;
3266
+ return;
3267
+ }
3268
+ inflight = true;
2346
3269
  let next = null;
2347
3270
  try {
2348
3271
  const reply = await api.get(`vision-state${query}`);
@@ -2354,16 +3277,32 @@ window.__ModuleLoader__.load({
2354
3277
  } catch {
2355
3278
  next = null;
2356
3279
  }
3280
+ inflight = false;
2357
3281
  if (stopped) return;
2358
3282
  setVision(next);
2359
- timer = setTimeout(tick, 15_000);
3283
+ timer = setTimeout(tick, rerun ? 400 : 15_000);
3284
+ rerun = false;
2360
3285
  };
3286
+ let inflight = false;
3287
+ let rerun = false;
2361
3288
  void tick();
3289
+ // Catch-up fetches after the trigger — the model/selection
3290
+ // event needs one append round-trip before the Host's
3291
+ // observeSession read reflects it, and auto sessions may
3292
+ // re-decide the route during prepareCall; 4s covers both.
3293
+ const catchups = [400, 1800, 4000].map((ms) =>
3294
+ setTimeout(() => {
3295
+ if (stopped) return;
3296
+ if (timer !== null) clearTimeout(timer);
3297
+ void tick();
3298
+ }, ms),
3299
+ );
2362
3300
  return () => {
2363
3301
  stopped = true;
2364
3302
  if (timer !== null) clearTimeout(timer);
3303
+ for (const c of catchups) clearTimeout(c);
2365
3304
  };
2366
- }, [sessionId]);
3305
+ }, [sessionId, pulse]);
2367
3306
  // Standard kit props are guaranteed by the session scope, but the
2368
3307
  // hooks must be called unconditionally — degrade to no-op sources
2369
3308
  // if a future shell ever mounts this entry outside that scope.
@@ -2371,11 +3310,45 @@ window.__ModuleLoader__.load({
2371
3310
  typeof useInput === 'function' ? useInput : () => () => ({ draft: '', attachmentIds: [] });
2372
3311
  const sessionHook =
2373
3312
  typeof useSession === 'function' ? useSession : () => () => null;
3313
+ const projectionHook =
3314
+ typeof useProjection === 'function' ? useProjection : () => () => undefined;
2374
3315
  const input = inputHook((s) => s) ?? {};
2375
3316
  const promptError = sessionHook((s) => s.promptError);
3317
+ // Wire view of the durable model-selection fold (same source the
3318
+ // composer dropdown reads); `next` flips the moment a
3319
+ // model/selection event lands, before any request runs.
3320
+ const selection = projectionHook('modelSelection');
3321
+ const draft = typeof input.draft === 'string' ? input.draft : '';
3322
+ // Pulse trigger 1: any model-selection change (dropdown pick).
3323
+ // The key string covers next+lastUsed so a re-render with a new
3324
+ // object identity but the same selection does not pulse; the ref
3325
+ // seeds from the first render's key so mounting with an existing
3326
+ // selection does not pulse either (same seeding pattern as
3327
+ // prevDraft below and IntentDockEntry's burst ref).
3328
+ const selectionKey =
3329
+ selection && typeof selection === 'object'
3330
+ ? [
3331
+ selection.next?.provider ?? '',
3332
+ selection.next?.model ?? '',
3333
+ selection.next?.reasoningEffort ?? '',
3334
+ selection.lastUsed?.provider ?? '',
3335
+ selection.lastUsed?.model ?? '',
3336
+ ].join('|')
3337
+ : null;
3338
+ const prevSelectionKey = React.useRef(selectionKey);
3339
+ // Pulse trigger 2: send burst (draft dropping to empty) — an
3340
+ // auto session's decided route may have changed.
3341
+ const prevDraft = React.useRef(draft);
3342
+ React.useEffect(() => {
3343
+ const selectionChanged = prevSelectionKey.current !== selectionKey;
3344
+ const sendBurst = prevDraft.current !== '' && draft === '';
3345
+ prevSelectionKey.current = selectionKey;
3346
+ prevDraft.current = draft;
3347
+ if (selectionChanged || sendBurst) setPulse((n) => n + 1);
3348
+ }, [selectionKey, draft]);
2376
3349
  latest.current = {
2377
3350
  vision,
2378
- draft: typeof input.draft === 'string' ? input.draft : '',
3351
+ draft,
2379
3352
  attachmentIds: Array.isArray(input.attachmentIds) ? input.attachmentIds : [],
2380
3353
  };
2381
3354
  React.useEffect(() => {
@@ -2450,11 +3423,13 @@ window.__ModuleLoader__.load({
2450
3423
  t('visionAutoConverted'),
2451
3424
  )
2452
3425
  : null,
2453
- // Hint whenever the probe cannot positively confirm image
2454
- // input — false (confirmed text-only) or null (unresolved,
2455
- // the common live case): /vision is harmless either way and
2456
- // this chip is the only composer-surface affordance for it.
2457
- vision !== null && vision.imageCapable !== true
3426
+ // Hint ONLY on a positively confirmed text-only model
3427
+ // (imageCapable === false). null means "unknown" (no llm
3428
+ // service, unreadable session, auto session with no routing
3429
+ // decision yet) — render nothing rather than a false
3430
+ // "not supported" claim; the refusal-driven auto-conversion
3431
+ // above still covers genuinely text-only sends.
3432
+ vision !== null && vision.imageCapable === false
2458
3433
  ? h(
2459
3434
  'p',
2460
3435
  {
@@ -2474,6 +3449,248 @@ window.__ModuleLoader__.load({
2474
3449
  );
2475
3450
  }
2476
3451
 
3452
+ /** Composer dock chip for intent routing (conversation.input.dock,
3453
+ * order 89 — one slot above the vision entry's 90): shows where the
3454
+ * session's auto-selected pseudo-model currently routes AND the
3455
+ * last decision's recommendation with its confidence as two labeled
3456
+ * segments (they coincide — decide-and-apply — but the labels make
3457
+ * "in use" vs "decided" explicit). Polls the Host intent-state
3458
+ * route every 15s exactly like the vision entry; a failed fetch or
3459
+ * a non-auto session renders null (fail-silent).
3460
+ * Pure display — no interactive controls (reset arrives later). */
3461
+ function IntentDockEntry({ t, sessionId, useInput }) {
3462
+ const [intent, setIntent] = React.useState(null);
3463
+ // Send-burst detection: the composer draft dropping from non-empty
3464
+ // to empty marks the moment a message leaves — the Host classifies
3465
+ // DURING that turn's prepareCall, so a few staggered catch-up
3466
+ // fetches right after make the chip flip in well under a second
3467
+ // instead of waiting for the 15s poll.
3468
+ const inputHook =
3469
+ typeof useInput === 'function' ? useInput : () => () => ({ draft: '' });
3470
+ const input = inputHook((s) => s) ?? {};
3471
+ const draft = typeof input.draft === 'string' ? input.draft : '';
3472
+ const prevDraft = React.useRef(draft);
3473
+ const [burst, setBurst] = React.useState(0);
3474
+ React.useEffect(() => {
3475
+ const was = prevDraft.current;
3476
+ prevDraft.current = draft;
3477
+ if (was !== '' && draft === '') setBurst((n) => n + 1);
3478
+ }, [draft]);
3479
+ React.useEffect(() => {
3480
+ let stopped = false;
3481
+ let timer = null;
3482
+ let inflight = false;
3483
+ let rerun = false;
3484
+ // Session-scoped slot: pass the scope's identity so the Host
3485
+ // resolves intent-state for THIS session (restart on switch).
3486
+ const query =
3487
+ typeof sessionId === 'string' && sessionId !== ''
3488
+ ? `?session=${encodeURIComponent(sessionId)}`
3489
+ : '';
3490
+ const tick = async () => {
3491
+ // One fetch at a time; a catch-up landing mid-flight
3492
+ // queues exactly one short-delay re-run instead of
3493
+ // forking a second poll loop.
3494
+ if (inflight) {
3495
+ rerun = true;
3496
+ return;
3497
+ }
3498
+ inflight = true;
3499
+ let next = null;
3500
+ try {
3501
+ const reply = await api.get(`intent-state${query}`);
3502
+ const value = reply?.ok ? reply.value : null;
3503
+ // Shape per design §5; anything unexpected degrades to
3504
+ // null (no chip), never to a wrong label.
3505
+ if (
3506
+ value &&
3507
+ typeof value === 'object' &&
3508
+ value.autoSelected === true
3509
+ )
3510
+ next = {
3511
+ enabled: value.enabled === true,
3512
+ configured: value.configured === true,
3513
+ model:
3514
+ value.current &&
3515
+ typeof value.current === 'object' &&
3516
+ typeof value.current.model === 'string' &&
3517
+ value.current.model !== ''
3518
+ ? value.current.model
3519
+ : null,
3520
+ confidence:
3521
+ value.lastDecision &&
3522
+ typeof value.lastDecision === 'object' &&
3523
+ typeof value.lastDecision.confidence === 'number'
3524
+ ? value.lastDecision.confidence
3525
+ : null,
3526
+ decisionModel:
3527
+ // lastDecision.best is "provider/model"; display the
3528
+ // model part only, matching the routed model above.
3529
+ value.lastDecision &&
3530
+ typeof value.lastDecision === 'object' &&
3531
+ typeof value.lastDecision.best === 'string' &&
3532
+ value.lastDecision.best !== ''
3533
+ ? value.lastDecision.best.slice(value.lastDecision.best.indexOf('/') + 1)
3534
+ : null,
3535
+ cacheSeconds:
3536
+ typeof value.cacheWindowRemainingMs === 'number' &&
3537
+ value.cacheWindowRemainingMs > 0
3538
+ ? Math.round(value.cacheWindowRemainingMs / 1000)
3539
+ : null,
3540
+ // context.overflowRisk passthrough: keep the
3541
+ // risk line only when the Host also named the
3542
+ // narrowest candidate and its window (the
3543
+ // tooltip needs both); anything missing
3544
+ // degrades to null like every other segment.
3545
+ overflowRisk:
3546
+ value.context &&
3547
+ typeof value.context === 'object' &&
3548
+ value.context.overflowRisk === true &&
3549
+ typeof value.context.minCandidateWindow === 'number' &&
3550
+ value.context.minCandidateWindow > 0 &&
3551
+ typeof value.context.minCandidateModel === 'string' &&
3552
+ value.context.minCandidateModel !== ''
3553
+ ? {
3554
+ model: value.context.minCandidateModel,
3555
+ windowK: Math.round(
3556
+ value.context.minCandidateWindow / 1000,
3557
+ ),
3558
+ }
3559
+ : null,
3560
+ candidates: Array.isArray(value.candidates)
3561
+ ? value.candidates.length
3562
+ : 0,
3563
+ };
3564
+ } catch {
3565
+ next = null;
3566
+ }
3567
+ inflight = false;
3568
+ if (stopped) return;
3569
+ setIntent(next);
3570
+ timer = setTimeout(tick, rerun ? 400 : 15_000);
3571
+ rerun = false;
3572
+ };
3573
+ void tick();
3574
+ // Catch-up fetches after each send (burst) — the decision
3575
+ // lands during prepareCall, typically within milliseconds
3576
+ // for a local classifier; 4s covers a slow chat-channel one.
3577
+ const catchups = [400, 1800, 4000].map((ms) =>
3578
+ setTimeout(() => {
3579
+ if (stopped) return;
3580
+ if (timer !== null) clearTimeout(timer);
3581
+ void tick();
3582
+ }, ms),
3583
+ );
3584
+ return () => {
3585
+ stopped = true;
3586
+ if (timer !== null) clearTimeout(timer);
3587
+ for (const c of catchups) clearTimeout(c);
3588
+ };
3589
+ }, [sessionId, burst]);
3590
+ if (intent === null) return null;
3591
+ const tipLines = [
3592
+ `${t('intentEnabledLabel')}: ${t(
3593
+ intent.enabled ? 'intentDockTipOn' : 'intentDockTipOff',
3594
+ )}`,
3595
+ intent.configured ? null : t('intentDockUnconfigured'),
3596
+ t('intentDockTipCandidates', { count: intent.candidates }),
3597
+ intent.cacheSeconds !== null
3598
+ ? t('intentDockCache', { seconds: intent.cacheSeconds })
3599
+ : null,
3600
+ intent.overflowRisk !== null
3601
+ ? t('intentDockOverflowRisk', {
3602
+ model: intent.overflowRisk.model,
3603
+ window: intent.overflowRisk.windowK,
3604
+ })
3605
+ : null,
3606
+ ].filter((line) => line !== null);
3607
+ // Same dock-column formula as VisionDockEntry: keep the chip's
3608
+ // left edge tracking the centered composer card in both shell
3609
+ // states (hero narrows the stack, active does not).
3610
+ return h(
3611
+ 'div',
3612
+ {
3613
+ 'data-dsh-switchman': 'intent-dock-column',
3614
+ style: {
3615
+ boxSizing: 'border-box',
3616
+ width: 'calc(100% - var(--dsh-composer-side-clearance, 16px) - var(--dsh-composer-side-clearance, 16px))',
3617
+ maxWidth: 'var(--dsh-composer-card-max-width, 780px)',
3618
+ margin: '0 auto',
3619
+ flex: 'none',
3620
+ display: 'flex',
3621
+ flexDirection: 'column',
3622
+ gap: 'var(--dsh-composer-stack-gap, 6px)',
3623
+ },
3624
+ },
3625
+ h(
3626
+ 'p',
3627
+ {
3628
+ 'data-dsh-switchman': 'intent-dock-chip',
3629
+ title: tipLines.join(' · '),
3630
+ style: {
3631
+ margin: '0',
3632
+ padding: '2px 0',
3633
+ color: intent.configured
3634
+ ? 'var(--dsw-alias-label-secondary)'
3635
+ : 'var(--dsw-alias-state-error-primary)',
3636
+ fontSize: '12px',
3637
+ lineHeight: '1.6',
3638
+ },
3639
+ },
3640
+ '⚡ ',
3641
+ t('intentDockAuto'),
3642
+ ' · ',
3643
+ // Dual display: the route in use RIGHT NOW and the last
3644
+ // decision's recommendation with its confidence — they
3645
+ // coincide after every send (decide-and-apply), but the
3646
+ // explicit labels answer "is this what I'm using or what
3647
+ // it suggests?" without guessing.
3648
+ intent.model !== null
3649
+ ? [
3650
+ `${t('intentDockUsing')} ${intent.model}`,
3651
+ intent.decisionModel !== null
3652
+ ? `${t('intentDockDecided')} ${intent.decisionModel}${
3653
+ intent.confidence !== null ? ` (${intent.confidence}%)` : ''
3654
+ }`
3655
+ : null,
3656
+ ]
3657
+ .filter((part) => part !== null)
3658
+ .join(' · ')
3659
+ : t('intentDockUnrouted'),
3660
+ intent.configured ? null : ` · ${t('intentDockUnconfigured')}`,
3661
+ ),
3662
+ );
3663
+ }
3664
+
3665
+ /** Composer model-label override (conversation.input.model.label):
3666
+ * SINGLE seat — once registered, this component renders EVERY
3667
+ * model's label in trigger, menu, and option rows; the owner's
3668
+ * `fallback` (model.name) applies only while the seat is
3669
+ * unregistered. So: the pseudo provider 'dsh-switchman-auto'
3670
+ * presents as the short 「自动」 in the trigger caption and
3671
+ * 「自动(意图识别)」 in the root menu's current value and each
3672
+ * option row; every other provider renders the catalog model name
3673
+ * verbatim. Returning null here would blank the label (single
3674
+ * seats have no per-dispatch fallback), which is why the fallback
3675
+ * body is rendered explicitly. Fail-silent by contract. */
3676
+ function IntentModelLabel({ provider, model, placement, t }) {
3677
+ const fallback =
3678
+ typeof model?.name === 'string' && model.name !== ''
3679
+ ? model.name
3680
+ : '';
3681
+ try {
3682
+ if (provider?.id !== 'dsh-switchman-auto')
3683
+ return h('span', null, fallback);
3684
+ if (placement === 'trigger')
3685
+ return h('span', null, t('intentModelLabelTrigger'));
3686
+ if (placement === 'menu' || placement === 'option')
3687
+ return h('span', null, t('intentModelLabelFull'));
3688
+ return h('span', null, fallback);
3689
+ } catch {
3690
+ return h('span', null, fallback);
3691
+ }
3692
+ }
3693
+
2477
3694
  /** Home-sidebar panel icon (sidebar.panellist contract): the shell
2478
3695
  * row renderer passes { size, active } and colors the glyph via the
2479
3696
  * row's currentColor, so only size matters here. 16×16 stroke
@@ -2788,11 +4005,52 @@ window.__ModuleLoader__.load({
2788
4005
  margin: '5px 0',
2789
4006
  accentColor: 'var(--dsw-alias-brand-primary)',
2790
4007
  },
4008
+ /** One intent-candidate editor row: index, provider, model,
4009
+ * effort, note, remove — all free-typed columns shrink to fit. */
4010
+ intentCandidateRow: {
4011
+ display: 'grid',
4012
+ gridTemplateColumns:
4013
+ '20px 20px minmax(0, 1fr) minmax(0, 1fr) minmax(0, 0.7fr) auto',
4014
+ gap: '6px',
4015
+ alignItems: 'center',
4016
+ padding: '2px 0',
4017
+ },
4018
+ intentCandidateCard: {
4019
+ display: 'flex',
4020
+ flexDirection: 'column',
4021
+ gap: '6px',
4022
+ padding: '8px 0',
4023
+ },
4024
+ intentCandidateCaption: {
4025
+ fontSize: '12px',
4026
+ opacity: 0.75,
4027
+ },
4028
+ intentCandidatePreset: {
4029
+ fontSize: '11px',
4030
+ lineHeight: '20px',
4031
+ padding: '0 8px',
4032
+ borderRadius: '999px',
4033
+ border: '1px solid var(--dsw-alias-border-primary, rgba(128, 128, 128, 0.4))',
4034
+ whiteSpace: 'nowrap',
4035
+ textAlign: 'center',
4036
+ },
4037
+ intentCandidateNote: {
4038
+ width: '100%',
4039
+ boxSizing: 'border-box',
4040
+ minHeight: '56px',
4041
+ resize: 'vertical',
4042
+ padding: '6px 8px',
4043
+ font: 'inherit',
4044
+ lineHeight: 1.5,
4045
+ },
2791
4046
  };
2792
4047
 
2793
4048
  /** One settings row: label + help on the left, control + status lines
2794
4049
  * (current value, suggestion, validation) on the right. */
2795
- function SettingsRow({ id, label, help, control, lines }) {
4050
+ function SettingsRow({ id, label, help, control, lines, hidden }) {
4051
+ // Rows irrelevant to the selected classifier channel are omitted
4052
+ // entirely (React drops null children).
4053
+ if (hidden) return null;
2796
4054
  return h(
2797
4055
  'div',
2798
4056
  { style: STYLE.row },
@@ -3391,16 +4649,21 @@ window.__ModuleLoader__.load({
3391
4649
  (values.langConversation !== '' &&
3392
4650
  !isCandidate(values.langConversation));
3393
4651
  /** Per-field numeric validity: raw text (when present) must be a
3394
- * whole number at or above the schema minimum. */
4652
+ * whole number at or above the schema minimum. The intent rows
4653
+ * share the same raw-text overlay and save gate. */
3395
4654
  const numberInvalid = Object.fromEntries(
3396
- WM_NUMBER_FIELDS.map(({ field, min }) => {
3397
- const raw = numText[field];
3398
- return [
3399
- field,
3400
- raw !== undefined &&
3401
- (!/^\d+$/.test(raw.trim()) || Number(raw.trim()) < min),
3402
- ];
3403
- }),
4655
+ [...WM_NUMBER_FIELDS, ...INTENT_NUMBER_FIELDS].map(
4656
+ ({ field, min, max }) => {
4657
+ const raw = numText[field];
4658
+ return [
4659
+ field,
4660
+ raw !== undefined &&
4661
+ (!/^\d+$/.test(raw.trim()) ||
4662
+ Number(raw.trim()) < min ||
4663
+ Number(raw.trim()) > (max ?? Infinity)),
4664
+ ];
4665
+ },
4666
+ ),
3404
4667
  );
3405
4668
  const anyNumberInvalid = Object.values(numberInvalid).some(Boolean);
3406
4669
  /** Ported hard rule (opencode config.ts): strict soft < hard < force. */
@@ -3638,6 +4901,35 @@ window.__ModuleLoader__.load({
3638
4901
  wmSubagentCap: values.wmSubagentCap,
3639
4902
  teamsMode: values.teamsMode,
3640
4903
  syncWhitelist: values.syncWhitelist,
4904
+ // Intent recognition (design: intent-routing.md §2): route
4905
+ // objects materialize {provider, model} like the pool fields;
4906
+ // candidate effort/note are always sent (blank string = unset,
4907
+ // the Host coerces per its schema).
4908
+ intentEnabled: values.intentEnabled,
4909
+ intentClassifierRoute: {
4910
+ provider: values.intentClassifierRoute.provider,
4911
+ model: values.intentClassifierRoute.model,
4912
+ },
4913
+ intentClassifierEffort: values.intentClassifierEffort,
4914
+ intentClassifierKind: values.intentClassifierKind,
4915
+ intentClassifierEndpoint: values.intentClassifierEndpoint,
4916
+ intentClassifierSystemoneModel: values.intentClassifierSystemoneModel,
4917
+ // Full row shape (id/preset/enabled included) through the SAME
4918
+ // sanitizer readCurrent uses — desired must stay structurally
4919
+ // identical to current (toggle/preset persistence + clean
4920
+ // dirty-check). Identified rows without a model survive
4921
+ // (presets awaiting configuration, half-edited custom rows);
4922
+ // only anonymous empty junk is dropped.
4923
+ intentCandidates: values.intentCandidates
4924
+ .map(sanitizeIntentCandidate)
4925
+ .filter((entry) => entry !== null),
4926
+ ...Object.fromEntries(
4927
+ INTENT_NUMBER_FIELDS.map(({ field }) => [field, values[field]]),
4928
+ ),
4929
+ intentFallbackRoute: {
4930
+ provider: values.intentFallbackRoute.provider,
4931
+ model: values.intentFallbackRoute.model,
4932
+ },
3641
4933
  };
3642
4934
  if (sameValues(current, desired)) {
3643
4935
  // Whitespace-only differences: nothing to write, close the draft.
@@ -3852,6 +5144,8 @@ window.__ModuleLoader__.load({
3852
5144
  const wmId = `${headingId}-wm`;
3853
5145
  // ---- teams section ids ----
3854
5146
  const teamsId = `${headingId}-teams`;
5147
+ // ---- intent section id ----
5148
+ const intentId = `${headingId}-intent`;
3855
5149
 
3856
5150
  /** One watermark number input: raw-text overlay over the draft
3857
5151
  * value (0 renders as the placeholder for the follow-force row),
@@ -3898,6 +5192,214 @@ window.__ModuleLoader__.load({
3898
5192
  return lines;
3899
5193
  };
3900
5194
 
5195
+ // ---- intent section controls ----
5196
+ /** One intent number input: the watermark rows' raw-text overlay
5197
+ * pattern under the intent section id (invalid text shows the
5198
+ * shared minimum hint and blocks saving via `invalid`). */
5199
+ const intentNumberControl = (spec) => {
5200
+ const { field } = spec;
5201
+ const shown = numText[field] ?? String(values[field]);
5202
+ return h('input', {
5203
+ type: 'text',
5204
+ inputMode: 'numeric',
5205
+ id: `${intentId}-${field}`,
5206
+ value: shown,
5207
+ onChange: (event) => {
5208
+ editNumber(field, event.target.value);
5209
+ },
5210
+ onBlur: blurNumber(field),
5211
+ disabled: !editable,
5212
+ 'aria-label': t(INTENT_ROW_KEYS[field].label),
5213
+ style: numberInvalid[field]
5214
+ ? {
5215
+ ...STYLE.control,
5216
+ borderColor: 'var(--dsw-alias-state-error-primary)',
5217
+ color: 'var(--dsw-alias-state-error-primary)',
5218
+ }
5219
+ : STYLE.control,
5220
+ });
5221
+ };
5222
+
5223
+ /** Status lines under one intent number row: minimum hint. */
5224
+ const intentNumberLines = (spec) =>
5225
+ numberInvalid[spec.field]
5226
+ ? [{ text: t('wmNumberInvalid', { min: spec.min }), invalid: true }]
5227
+ : [];
5228
+
5229
+ /** Intent option sets fed by the DSH model catalog (the same
5230
+ * source the shipped model-selection page uses). A stored
5231
+ * provider/model that left the catalog stays selectable so a
5232
+ * controlled select never misaligns. */
5233
+ const intentProviders = () => [
5234
+ ...new Set(
5235
+ [...catalogIndex.values()].map((meta) => meta.provider),
5236
+ ),
5237
+ ].sort();
5238
+ const intentProviderName = (provider) => {
5239
+ for (const meta of catalogIndex.values())
5240
+ if (meta.provider === provider)
5241
+ return meta.providerName ?? provider;
5242
+ return provider;
5243
+ };
5244
+ const intentModelsFor = (provider) =>
5245
+ [...catalogIndex.values()].filter(
5246
+ (meta) => meta.provider === provider,
5247
+ );
5248
+ const intentProviderOptions = (current) => {
5249
+ const providers = intentProviders();
5250
+ if (current !== '' && !providers.includes(current))
5251
+ providers.push(current);
5252
+ return [
5253
+ h('option', { value: '' }, t('intentRouteUnset')),
5254
+ ...providers.map((provider) =>
5255
+ h(
5256
+ 'option',
5257
+ { key: provider, value: provider },
5258
+ intentProviderName(provider),
5259
+ ),
5260
+ ),
5261
+ ];
5262
+ };
5263
+ const intentModelOptions = (provider, current) => {
5264
+ const metas = intentModelsFor(provider);
5265
+ return [
5266
+ h('option', { value: '' }, t('intentRouteUnset')),
5267
+ ...metas.map((meta) =>
5268
+ h(
5269
+ 'option',
5270
+ { key: meta.model, value: meta.model },
5271
+ meta.modelName ?? meta.model,
5272
+ ),
5273
+ ),
5274
+ provider !== '' &&
5275
+ current !== '' &&
5276
+ !metas.some((meta) => meta.model === current)
5277
+ ? h('option', { value: current }, current)
5278
+ : null,
5279
+ ];
5280
+ };
5281
+ /** One {provider, model} intent route editor as two linked
5282
+ * selects; switching providers drops a model the new provider
5283
+ * does not offer. An untyped route stays the frozen
5284
+ * empty-route default. */
5285
+ const intentRouteControl = (field, label) => {
5286
+ const route = values[field];
5287
+ return h(
5288
+ 'div',
5289
+ { style: { display: 'flex', gap: '6px', minWidth: 0 } },
5290
+ h(
5291
+ 'select',
5292
+ {
5293
+ ...selectProps(
5294
+ `${intentId}-${field}-provider`,
5295
+ route.provider,
5296
+ (next) => {
5297
+ const kept = intentModelsFor(next).some(
5298
+ (meta) => meta.model === route.model,
5299
+ );
5300
+ edit(field, {
5301
+ provider: next,
5302
+ model: kept ? route.model : '',
5303
+ });
5304
+ },
5305
+ ),
5306
+ 'aria-label': `${label} · provider`,
5307
+ },
5308
+ ...intentProviderOptions(route.provider),
5309
+ ),
5310
+ h(
5311
+ 'select',
5312
+ {
5313
+ ...selectProps(
5314
+ `${intentId}-${field}-model`,
5315
+ route.model,
5316
+ (next) => {
5317
+ edit(field, { ...route, model: next });
5318
+ },
5319
+ ),
5320
+ disabled: !editable || route.provider === '',
5321
+ 'aria-label': `${label} · model`,
5322
+ },
5323
+ ...intentModelOptions(route.provider, route.model),
5324
+ ),
5325
+ );
5326
+ };
5327
+ /** Reasoning-effort select for one intent route: the model's
5328
+ * wire efforts when the catalog reports them, the EFFORTS
5329
+ * fallback otherwise; '' = adapter default. A stored value
5330
+ * missing from the list stays selectable. */
5331
+ const intentEffortSelect = (
5332
+ id,
5333
+ route,
5334
+ value,
5335
+ onCommit,
5336
+ ariaLabel,
5337
+ ) => {
5338
+ const meta = catalogIndex.get(routeKey(route));
5339
+ const wire =
5340
+ meta &&
5341
+ Array.isArray(meta.reasoning?.efforts) &&
5342
+ meta.reasoning.efforts.length > 0
5343
+ ? meta.reasoning.efforts
5344
+ : EFFORTS.map((effort) => ({ id: effort }));
5345
+ const seen = new Set(wire.map((entry) => entry.id));
5346
+ const pending =
5347
+ typeof value === 'string' && value !== '' && !seen.has(value)
5348
+ ? [{ id: value }]
5349
+ : [];
5350
+ return h(
5351
+ 'select',
5352
+ {
5353
+ id,
5354
+ value,
5355
+ onChange: (event) => onCommit(event.target.value),
5356
+ disabled: !editable,
5357
+ style: STYLE.effortSelect,
5358
+ 'aria-label': ariaLabel,
5359
+ },
5360
+ h('option', { value: '' }, t('intentEffortFollow')),
5361
+ ...[...wire, ...pending].map((entry) =>
5362
+ h('option', { value: entry.id }, entry.name ?? entry.id),
5363
+ ),
5364
+ );
5365
+ };
5366
+
5367
+ /** Candidates list editing: the whole array is one draft field;
5368
+ * rows are addressed by position (the classifier reads them as
5369
+ * #1..#n, so order is meaningful). */
5370
+ const editIntentCandidate = (index, patch) => {
5371
+ edit(
5372
+ 'intentCandidates',
5373
+ values.intentCandidates.map((entry, position) =>
5374
+ position === index ? { ...entry, ...patch } : entry,
5375
+ ),
5376
+ );
5377
+ };
5378
+ const addIntentCandidate = () => {
5379
+ // Client-generated id keeps the half-edited row alive across
5380
+ // save/snapshot round-trips (host keeps identified empty rows).
5381
+ edit('intentCandidates', [
5382
+ ...values.intentCandidates,
5383
+ {
5384
+ id: `u${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`,
5385
+ preset: false,
5386
+ enabled: true,
5387
+ provider: '',
5388
+ model: '',
5389
+ effort: '',
5390
+ note: '',
5391
+ },
5392
+ ]);
5393
+ };
5394
+ const removeIntentCandidate = (index) => {
5395
+ edit(
5396
+ 'intentCandidates',
5397
+ values.intentCandidates.filter(
5398
+ (_entry, position) => position !== index,
5399
+ ),
5400
+ );
5401
+ };
5402
+
3901
5403
  /** Catalog status line: loading / error + retry / partial / empty. */
3902
5404
  const catalogNotice =
3903
5405
  catalog.status === 'loading'
@@ -4307,6 +5809,358 @@ window.__ModuleLoader__.load({
4307
5809
  ],
4308
5810
  }),
4309
5811
  ),
5812
+ h(
5813
+ 'section',
5814
+ {
5815
+ 'aria-labelledby': intentId,
5816
+ style: { ...STYLE.section, ...STYLE.sectionDivider },
5817
+ },
5818
+ h(
5819
+ 'h3',
5820
+ { id: intentId, style: STYLE.heading },
5821
+ t('intentSectionTitle'),
5822
+ ),
5823
+ h('p', { style: STYLE.description }, t('intentSectionDescription')),
5824
+ h(
5825
+ 'div',
5826
+ { style: STYLE.rows },
5827
+ h(SettingsRow, {
5828
+ key: 'intentEnabled',
5829
+ id: `${intentId}-enabled`,
5830
+ label: t('intentEnabledLabel'),
5831
+ help: t('intentEnabledHelp'),
5832
+ control: h('input', {
5833
+ type: 'checkbox',
5834
+ id: `${intentId}-enabled`,
5835
+ checked: values.intentEnabled,
5836
+ onChange: () => {
5837
+ edit('intentEnabled', !values.intentEnabled);
5838
+ },
5839
+ disabled: !editable,
5840
+ style: STYLE.checkbox,
5841
+ }),
5842
+ lines: [],
5843
+ }),
5844
+ h(SettingsRow, {
5845
+ key: 'intentClassifierRoute',
5846
+ hidden: values.intentClassifierKind === 'systemone',
5847
+ id: `${intentId}-classifier-route`,
5848
+ label: t('intentClassifierRouteLabel'),
5849
+ help: t('intentClassifierRouteHelp'),
5850
+ control: intentRouteControl(
5851
+ 'intentClassifierRoute',
5852
+ t('intentClassifierRouteLabel'),
5853
+ ),
5854
+ lines:
5855
+ values.intentClassifierRoute.provider === '' ||
5856
+ values.intentClassifierRoute.model === ''
5857
+ ? [
5858
+ {
5859
+ text: t('intentRouteUnset'),
5860
+ // The classifier is load-bearing only
5861
+ // while routing is on.
5862
+ invalid: values.intentEnabled,
5863
+ },
5864
+ ]
5865
+ : [
5866
+ {
5867
+ text: t('currentValue', {
5868
+ value: `${values.intentClassifierRoute.provider}/${values.intentClassifierRoute.model}`,
5869
+ }),
5870
+ invalid: false,
5871
+ },
5872
+ ],
5873
+ }),
5874
+ h(SettingsRow, {
5875
+ key: 'intentClassifierEffort',
5876
+ hidden: values.intentClassifierKind === 'systemone',
5877
+ id: `${intentId}-classifier-effort`,
5878
+ label: t('intentClassifierEffortLabel'),
5879
+ help: t('intentClassifierEffortHelp'),
5880
+ control: intentEffortSelect(
5881
+ `${intentId}-classifier-effort`,
5882
+ values.intentClassifierRoute,
5883
+ values.intentClassifierEffort,
5884
+ (next) => edit('intentClassifierEffort', next),
5885
+ t('intentClassifierEffortLabel'),
5886
+ ),
5887
+ lines: [],
5888
+ }),
5889
+ h(SettingsRow, {
5890
+ key: 'intentClassifierKind',
5891
+ id: `${intentId}-kind`,
5892
+ label: t('intentClassifierKindLabel'),
5893
+ help: t('intentClassifierKindHelp'),
5894
+ control: h(
5895
+ 'select',
5896
+ selectProps(
5897
+ `${intentId}-kind`,
5898
+ values.intentClassifierKind,
5899
+ (kind) => {
5900
+ if (INTENT_KINDS.includes(kind))
5901
+ edit('intentClassifierKind', kind);
5902
+ },
5903
+ ),
5904
+ ...INTENT_KINDS.map((kind) =>
5905
+ h(
5906
+ 'option',
5907
+ { key: kind, value: kind },
5908
+ t(
5909
+ `intentClassifierKind${kind[0].toUpperCase()}${kind.slice(1)}`,
5910
+ ),
5911
+ ),
5912
+ ),
5913
+ ),
5914
+ lines: [],
5915
+ }),
5916
+ h(SettingsRow, {
5917
+ key: 'intentClassifierEndpoint',
5918
+ hidden: values.intentClassifierKind !== 'systemone',
5919
+ id: `${intentId}-classifier-endpoint`,
5920
+ label: t('intentClassifierEndpointLabel'),
5921
+ help: t('intentClassifierEndpointHelp'),
5922
+ control: h('input', {
5923
+ type: 'text',
5924
+ value: values.intentClassifierEndpoint,
5925
+ placeholder: 'http://127.0.0.1:11434/v1/systemone',
5926
+ onChange: (event) =>
5927
+ edit('intentClassifierEndpoint', event.target.value),
5928
+ disabled: !editable,
5929
+ 'aria-label': t('intentClassifierEndpointLabel'),
5930
+ style: STYLE.control,
5931
+ }),
5932
+ lines:
5933
+ values.intentClassifierKind === 'systemone' &&
5934
+ values.intentClassifierEndpoint.trim() === ''
5935
+ ? [
5936
+ {
5937
+ text: t('intentEndpointUnset'),
5938
+ // Load-bearing only for that kind.
5939
+ invalid: values.intentEnabled,
5940
+ },
5941
+ ]
5942
+ : [],
5943
+ }),
5944
+ h(SettingsRow, {
5945
+ key: 'intentClassifierSystemoneModel',
5946
+ hidden: values.intentClassifierKind !== 'systemone',
5947
+ id: `${intentId}-systemone-model`,
5948
+ label: t('intentSystemoneModelLabel'),
5949
+ help: t('intentSystemoneModelHelp'),
5950
+ control: h('input', {
5951
+ type: 'text',
5952
+ value: values.intentClassifierSystemoneModel,
5953
+ placeholder: 'tev1:4b-mxfp8',
5954
+ onChange: (event) =>
5955
+ edit('intentClassifierSystemoneModel', event.target.value),
5956
+ disabled: !editable,
5957
+ 'aria-label': t('intentSystemoneModelLabel'),
5958
+ style: STYLE.control,
5959
+ }),
5960
+ lines:
5961
+ values.intentClassifierKind === 'systemone' &&
5962
+ values.intentClassifierSystemoneModel.trim() === ''
5963
+ ? [
5964
+ {
5965
+ text: t('intentEndpointUnset'),
5966
+ // Load-bearing only for that kind.
5967
+ invalid: values.intentEnabled,
5968
+ },
5969
+ ]
5970
+ : [],
5971
+ }),
5972
+ ),
5973
+ h('h4', { style: STYLE.subheading }, t('intentCandidatesLabel')),
5974
+ h('p', { style: STYLE.description }, t('intentCandidatesHelp')),
5975
+ values.intentCandidates.length === 0
5976
+ ? h('p', { style: STYLE.progress }, t('intentCandidatesEmpty'))
5977
+ : h(
5978
+ 'div',
5979
+ { style: STYLE.rankList },
5980
+ ...values.intentCandidates.map((entry, index) =>
5981
+ h(
5982
+ 'div',
5983
+ { key: entry.id ?? index, style: STYLE.intentCandidateCard },
5984
+ h(
5985
+ 'div',
5986
+ { style: STYLE.intentCandidateRow },
5987
+ h('input', {
5988
+ type: 'checkbox',
5989
+ checked: entry.enabled !== false,
5990
+ onChange: (event) => {
5991
+ editIntentCandidate(index, {
5992
+ enabled: event.target.checked,
5993
+ });
5994
+ },
5995
+ disabled: !editable,
5996
+ 'aria-label': `${t('intentCandidateEnable')} · ${t('intentCandidatesLabel')} ${index + 1}`,
5997
+ }),
5998
+ h(
5999
+ 'span',
6000
+ { style: STYLE.rankIndex },
6001
+ String(index + 1),
6002
+ ),
6003
+ h(
6004
+ 'select',
6005
+ {
6006
+ ...selectProps(
6007
+ `${intentId}-candidate-${index}-provider`,
6008
+ entry.provider,
6009
+ (next) => {
6010
+ const kept = intentModelsFor(
6011
+ next,
6012
+ ).some(
6013
+ (meta) =>
6014
+ meta.model ===
6015
+ entry.model,
6016
+ );
6017
+ editIntentCandidate(index, {
6018
+ provider: next,
6019
+ model: kept
6020
+ ? entry.model
6021
+ : '',
6022
+ });
6023
+ },
6024
+ ),
6025
+ 'aria-label': `${t('intentCandidatesLabel')} ${index + 1} · provider`,
6026
+ },
6027
+ ...intentProviderOptions(entry.provider),
6028
+ ),
6029
+ h(
6030
+ 'select',
6031
+ {
6032
+ ...selectProps(
6033
+ `${intentId}-candidate-${index}-model`,
6034
+ entry.model,
6035
+ (next) => {
6036
+ editIntentCandidate(index, {
6037
+ model: next,
6038
+ });
6039
+ },
6040
+ ),
6041
+ disabled:
6042
+ !editable || entry.provider === '',
6043
+ 'aria-label': `${t('intentCandidatesLabel')} ${index + 1} · model`,
6044
+ },
6045
+ ...intentModelOptions(
6046
+ entry.provider,
6047
+ entry.model,
6048
+ ),
6049
+ ),
6050
+ intentEffortSelect(
6051
+ `${intentId}-candidate-${index}-effort`,
6052
+ entry,
6053
+ entry.effort ?? '',
6054
+ (next) => {
6055
+ editIntentCandidate(index, {
6056
+ effort: next,
6057
+ });
6058
+ },
6059
+ `${t('intentCandidatesLabel')} ${index + 1} · ${t('intentClassifierEffortLabel')}`,
6060
+ ),
6061
+ entry.preset === true
6062
+ ? h(
6063
+ 'span',
6064
+ { style: STYLE.intentCandidatePreset },
6065
+ t('intentCandidatePresetBadge'),
6066
+ )
6067
+ : h(
6068
+ 'button',
6069
+ {
6070
+ type: 'button',
6071
+ onClick: () => {
6072
+ removeIntentCandidate(index);
6073
+ },
6074
+ disabled: !editable,
6075
+ title: t('intentCandidateRemove'),
6076
+ 'aria-label': t('intentCandidateRemove'),
6077
+ style: STYLE.iconButton,
6078
+ },
6079
+ '✕',
6080
+ ),
6081
+ ),
6082
+ entry.preset === true
6083
+ ? h(
6084
+ 'span',
6085
+ { style: STYLE.intentCandidateCaption },
6086
+ entry.label ?? entry.id,
6087
+ )
6088
+ : null,
6089
+ h('textarea', {
6090
+ value: entry.note ?? '',
6091
+ placeholder: t('intentCandidateNotePlaceholder'),
6092
+ readOnly: entry.preset === true,
6093
+ onChange: (event) =>
6094
+ editIntentCandidate(index, {
6095
+ note: event.target.value,
6096
+ }),
6097
+ disabled: !editable,
6098
+ 'aria-label': `${t('intentCandidatesLabel')} ${index + 1} · note`,
6099
+ style: {
6100
+ ...STYLE.intentCandidateNote,
6101
+ ...(entry.preset === true
6102
+ ? { opacity: 0.75, cursor: 'default' }
6103
+ : {}),
6104
+ },
6105
+ }),
6106
+ entry.enabled !== false && entry.model.trim() === ''
6107
+ ? h(
6108
+ 'p',
6109
+ { style: STYLE.progress },
6110
+ t('intentCandidateUnset'),
6111
+ )
6112
+ : null,
6113
+ ),
6114
+ ),
6115
+ ),
6116
+ h(
6117
+ 'button',
6118
+ {
6119
+ type: 'button',
6120
+ onClick: addIntentCandidate,
6121
+ disabled: !editable,
6122
+ style: { ...STYLE.iconButton, marginTop: '6px' },
6123
+ },
6124
+ '+ ',
6125
+ t('intentCandidatesAdd'),
6126
+ ),
6127
+ h(
6128
+ 'div',
6129
+ { style: STYLE.rows },
6130
+ h(SettingsRow, {
6131
+ key: 'intentFallbackRoute',
6132
+ id: `${intentId}-fallback`,
6133
+ label: t('intentFallbackRouteLabel'),
6134
+ help: t('intentFallbackRouteHelp'),
6135
+ control: intentRouteControl(
6136
+ 'intentFallbackRoute',
6137
+ t('intentFallbackRouteLabel'),
6138
+ ),
6139
+ lines:
6140
+ values.intentFallbackRoute.provider === '' ||
6141
+ values.intentFallbackRoute.model === ''
6142
+ ? [{ text: t('intentRouteUnset'), invalid: false }]
6143
+ : [
6144
+ {
6145
+ text: t('currentValue', {
6146
+ value: `${values.intentFallbackRoute.provider}/${values.intentFallbackRoute.model}`,
6147
+ }),
6148
+ invalid: false,
6149
+ },
6150
+ ],
6151
+ }),
6152
+ ...INTENT_NUMBER_FIELDS.map((spec) =>
6153
+ h(SettingsRow, {
6154
+ key: spec.field,
6155
+ id: `${intentId}-${spec.field}`,
6156
+ label: t(INTENT_ROW_KEYS[spec.field].label),
6157
+ help: t(INTENT_ROW_KEYS[spec.field].help),
6158
+ control: intentNumberControl(spec),
6159
+ lines: intentNumberLines(spec),
6160
+ }),
6161
+ ),
6162
+ ),
6163
+ ),
4310
6164
  h(
4311
6165
  'section',
4312
6166
  {
@@ -4712,6 +6566,33 @@ window.__ModuleLoader__.load({
4712
6566
  (kit) => h(VisionDockEntry, { ...kit, t }),
4713
6567
  ),
4714
6568
  );
6569
+ // Composer-adjacent intent chip (same dock list, order 89 —
6570
+ // one slot above the vision hint's 90): shows where the
6571
+ // session's auto-selected model currently routes. Same
6572
+ // standard session-kit forwarding as the vision entry.
6573
+ ctx.slots.inject('conversation.input.dock', () =>
6574
+ ctx.slots.register(
6575
+ {
6576
+ name: 'conversation.input.dock',
6577
+ id: 'dsh-switchman-intent',
6578
+ order: 89,
6579
+ },
6580
+ (kit) => h(IntentDockEntry, { ...kit, t }),
6581
+ ),
6582
+ );
6583
+ // Composer model label (conversation.input.model.label): the
6584
+ // ui-model-selection shell dispatches EVERY model's label
6585
+ // through this single-seat component with owner props
6586
+ // {provider:{id,name}, model, placement, selected} plus the
6587
+ // standard session kit. Single seats render the owner
6588
+ // fallback only while UNREGISTERED, so the component itself
6589
+ // renders the catalog model name for real providers (see
6590
+ // IntentModelLabel).
6591
+ ctx.slots.inject('conversation.input.model.label', () =>
6592
+ ctx.slots.register({ name: 'conversation.input.model.label' }, (kit) =>
6593
+ h(IntentModelLabel, { ...kit, t }),
6594
+ ),
6595
+ );
4715
6596
  // Settings page: one settings.section entry. The restricted
4716
6597
  // dynamic-module runtime has no configForms service, so there
4717
6598
  // is no whileServed gating — the page loads its snapshot from