tensorgrid-ui 1.10.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -180,10 +180,7 @@ window.__ModuleLoader__.load({
180
180
  'review.effortName': 'effort',
181
181
  'review.other': 'another…',
182
182
  'review.modelName': 'model name',
183
- 'review.custom': 'Your own set of reviewers — no mode matches it.',
184
- 'review.details': 'Detailed settings',
185
183
  'review.fromList': 'back to the list',
186
- 'review.ours': 'ours',
187
184
  'review.objectFalse': 'I disagree — this is real',
188
185
  'review.accuracy': 'How often each reviewer turned out to be right',
189
186
  'review.notChecked': 'not verified yet',
@@ -193,9 +190,19 @@ window.__ModuleLoader__.load({
193
190
  'review.objectReal': 'I disagree — this is wrong',
194
191
  'review.of': 'of',
195
192
  'review.byHuman': 'your call',
193
+ 'review.needsProduct': 'Works only if that product is installed separately',
194
+ 'review.add': 'Add a reviewer:',
195
+ 'review.participates': 'Take part in reviews',
196
+ 'review.noneActive': 'Every reviewer is switched off — a review will find nothing.',
197
+ 'review.remove': 'Remove this reviewer',
198
+ 'review.bySubscription': 'subscription',
199
+ 'review.limit': 'That is as many reviewers as one review can hold.',
200
+ 'review.byApiKey': 'paid per token',
201
+ 'review.groupSubscription': 'Included in a subscription',
202
+ 'review.groupPaid': 'Paid separately \u2014 per token',
196
203
  'review.nav': 'Reviewers',
197
204
  'review.title': 'Second opinion',
198
- 'review.hint': 'Independent reviewers examine finished work and report what is wrong. They run as separate products and do not see this conversation.',
205
+ 'review.hint': 'Independent reviewers examine finished work and report what is wrong. Choose how many and which ones: different models find different things.',
199
206
  'review.model': 'Model',
200
207
  'review.effort': 'Effort',
201
208
  'review.asConfigured': 'as configured',
@@ -251,10 +258,7 @@ window.__ModuleLoader__.load({
251
258
  'review.effortName': '强度',
252
259
  'review.other': '其他…',
253
260
  'review.modelName': '模型名称',
254
- 'review.custom': '自定义审阅者组合——没有匹配的模式。',
255
- 'review.details': '详细设置',
256
261
  'review.fromList': '返回列表',
257
- 'review.ours': '我们的',
258
262
  'review.objectFalse': '我不同意——这是真的',
259
263
  'review.accuracy': '各审阅者的正确率',
260
264
  'review.notChecked': '尚未核实',
@@ -264,9 +268,19 @@ window.__ModuleLoader__.load({
264
268
  'review.objectReal': '我不同意——这是错的',
265
269
  'review.of': '共',
266
270
  'review.byHuman': '你的判断',
271
+ 'review.needsProduct': '仅在单独安装该产品后可用',
272
+ 'review.add': '添加审阅者:',
273
+ 'review.participates': '参与审阅',
274
+ 'review.noneActive': '所有审阅者都已关闭——审阅将一无所获。',
275
+ 'review.remove': '移除该审阅者',
276
+ 'review.bySubscription': '订阅',
277
+ 'review.limit': '一次审阅最多只能有这么多审阅者。',
278
+ 'review.byApiKey': '按量计费',
279
+ 'review.groupSubscription': '包含在订阅中',
280
+ 'review.groupPaid': '单独计费 \u2014 按量',
267
281
  'review.nav': '审阅者',
268
282
  'review.title': '第二意见',
269
- 'review.hint': '独立审阅者检查已完成的工作并报告问题。它们作为独立产品运行,看不到本次对话。',
283
+ 'review.hint': '独立审阅者检查已完成的工作并报告问题。自行决定数量与人选:不同模型发现的问题不同。',
270
284
  'review.model': '模型',
271
285
  'review.effort': '推理强度',
272
286
  'review.asConfigured': '按产品设置',
@@ -1619,10 +1633,7 @@ window.__ModuleLoader__.load({
1619
1633
  'review.effortName': 'усилие',
1620
1634
  'review.other': 'другая…',
1621
1635
  'review.modelName': 'имя модели',
1622
- 'review.custom': 'Свой набор ревизоров — ни один режим ему не соответствует.',
1623
- 'review.details': 'Подробные настройки',
1624
1636
  'review.fromList': 'вернуться к списку',
1625
- 'review.ours': 'наш',
1626
1637
  'review.objectFalse': 'Не согласен — это настоящая',
1627
1638
  'review.accuracy': 'Как часто каждый ревизор оказывался прав',
1628
1639
  'review.notChecked': 'ещё не проверено',
@@ -1632,9 +1643,19 @@ window.__ModuleLoader__.load({
1632
1643
  'review.objectReal': 'Не согласен — это ложная',
1633
1644
  'review.of': 'из',
1634
1645
  'review.byHuman': 'ваше решение',
1646
+ 'review.needsProduct': 'Работает, только если этот продукт установлен отдельно',
1647
+ 'review.add': 'Добавить ревизора:',
1648
+ 'review.participates': 'Участвует в обзорах',
1649
+ 'review.noneActive': 'Все ревизоры выключены — обзор ничего не найдёт.',
1650
+ 'review.remove': 'Убрать этого ревизора',
1651
+ 'review.bySubscription': 'по подписке',
1652
+ 'review.limit': 'Больше ревизоров в один обзор не поместится.',
1653
+ 'review.byApiKey': 'по токенам',
1654
+ 'review.groupSubscription': 'Входят в подписку',
1655
+ 'review.groupPaid': 'Оплата отдельно \u2014 по токенам',
1635
1656
  'review.nav': 'Ревизоры',
1636
1657
  'review.title': 'Второе мнение',
1637
- 'review.hint': 'Независимые ревизоры смотрят готовую работу и сообщают, что в ней не так. Работают как отдельные продукты и нашей переписки не видят.',
1658
+ 'review.hint': 'Независимые ревизоры смотрят готовую работу и сообщают, что в ней не так. Сколько их и какие выбираете вы: разные модели находят разное.',
1638
1659
  'review.model': 'Модель',
1639
1660
  'review.effort': 'Усилие',
1640
1661
  'review.asConfigured': 'как настроено',
@@ -1788,7 +1809,7 @@ window.__ModuleLoader__.load({
1788
1809
  // поэтому строка остаётся согласованной при любой палитре.
1789
1810
  '.dsx-setting{display:flex;align-items:center;justify-content:space-between;gap:16px;width:100%;}',
1790
1811
  '.dsx-setting__title{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:500;}',
1791
- '.dsx-rev__panel{margin-top:14px;padding-top:12px;border-top:1px solid var(--dsw-alias-border-l1)}.dsx-rev__panel-head{display:flex;align-items:center;gap:8px;margin-bottom:8px}.dsx-rev__count{padding:1px 7px;border-radius:99px;background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-tertiary);font-size:11px}.dsx-rev__findings{display:flex;flex-direction:column;gap:8px}.dsx-rev__finding{padding:9px 11px;border-radius:8px;border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-2)}.dsx-rev__finding--false{opacity:.55}.dsx-rev__finding-head{display:flex;align-items:baseline;gap:7px;flex-wrap:wrap}.dsx-rev__finding-title{font-weight:600;color:var(--dsw-alias-label-primary);font-size:13px;flex:1}.dsx-rev__sev{padding:1px 6px;border-radius:4px;font-size:10px;text-transform:uppercase;letter-spacing:.03em}.dsx-rev__sev--crit{background:#5c1f22;color:#ffb4b4}.dsx-rev__sev--high{background:#5a3f16;color:#f0cd8a}.dsx-rev__sev--unknown{background:#3a3a44;color:#c9c9d4}.dsx-rev__sev--low{background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-tertiary)}.dsx-rev__who{font-size:11px;color:var(--dsw-alias-label-tertiary)}.dsx-rev__where{margin-top:4px;font-family:ui-monospace,monospace;font-size:11px;color:var(--dsw-alias-label-tertiary)}.dsx-rev__what{margin-top:4px;font-size:12px;color:var(--dsw-alias-label-secondary);line-height:1.45}.dsx-rev__verdict{display:flex;align-items:flex-start;gap:6px;margin-top:7px;padding-top:7px;border-top:1px dashed var(--dsw-alias-border-l1);font-size:11px;color:var(--dsw-alias-label-secondary)}.dsx-rev__verdict-mark{font-weight:700}.dsx-rev__verdict-text{flex:1;line-height:1.4}.dsx-rev__by{color:var(--dsw-alias-label-tertiary);white-space:nowrap}.dsx-rev__object{margin-top:5px}.dsx-rev__object-btn{border:0;background:none;padding:0;color:var(--dsw-alias-label-tertiary);font-size:11px;cursor:pointer;text-decoration:underline dotted}.dsx-rev__object-btn:hover{color:var(--dsw-alias-label-primary)}.dsx-rev__stats{margin-top:12px;padding-top:10px;border-top:1px solid var(--dsw-alias-border-l1)}.dsx-rev__stat{display:flex;justify-content:space-between;gap:10px;margin-top:4px;font-size:12px}.dsx-rev__stat-name{color:var(--dsw-alias-label-secondary)}.dsx-rev__stat-rate{color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums}.dsx-rev__modes{display:flex;gap:8px;margin:10px 0 4px}.dsx-rev__mode{flex:1;display:flex;flex-direction:column;gap:3px;padding:9px 11px;border-radius:8px;border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);cursor:pointer;text-align:left;transition:border-color .15s ease,background .15s ease}.dsx-rev__mode:hover{border-color:var(--dsw-alias-border-l2)}.dsx-rev__mode--on{border-color:var(--dsw-alias-brand-default);background:var(--dsw-alias-bg-layer-1)}.dsx-rev__mode-name{font-weight:600;color:var(--dsw-alias-label-primary);font-size:13px}.dsx-rev__mode-hint{font-size:11px;line-height:1.35}.dsx-rev__toggle{align-self:flex-start;margin-top:6px;padding:2px 0;border:0;background:none;color:var(--dsw-alias-label-secondary);font-size:12px;cursor:pointer}.dsx-rev__toggle:hover{color:var(--dsw-alias-label-primary)}.dsx-rev__tag{margin-left:6px;padding:1px 6px;border-radius:99px;background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-tertiary);font-size:10px}.dsx-rev__custom{display:inline-flex;align-items:center;gap:4px}.dsx-rev__input{padding:3px 6px;border-radius:6px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font-size:12px;width:170px}.dsx-rev__back{border:0;background:none;color:var(--dsw-alias-label-tertiary);cursor:pointer;font-size:14px;line-height:1;padding:0 2px}.dsx-rev__list{display:flex;flex-direction:column;gap:8px}.dsx-rev__card{padding:10px 12px;border-radius:8px;border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-2);opacity:.6;transition:opacity .15s ease,border-color .15s ease}.dsx-rev__card--on{opacity:1;border-color:var(--dsw-alias-border-l2)}.dsx-rev__head{display:flex;align-items:center;gap:8px;cursor:pointer}.dsx-rev__name{font-weight:600;color:var(--dsw-alias-label-primary)}.dsx-rev__fields{display:flex;flex-wrap:wrap;gap:12px;margin-top:8px;padding-left:22px}.dsx-rev__field{display:flex;align-items:center;gap:6px;color:var(--dsw-alias-label-secondary);font-size:12px}.dsx-rev__select{padding:3px 6px;border-radius:6px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font-size:12px}.dsx-subs{display:flex;flex-direction:column;gap:14px;padding:4px 0}.dsx-subs__head{display:flex;flex-direction:column;gap:4px}.dsx-subs__title{font-weight:600;font-size:15px;color:var(--dsw-alias-label-primary)}.dsx-subs__note{padding-top:6px;border-top:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.5}.dsx-setting--stack{flex-direction:column;align-items:stretch;gap:10px}.dsx-auth__list{display:flex;flex-direction:column;gap:6px}.dsx-auth__row{display:flex;align-items:center;justify-content:space-between;gap:12px}.dsx-auth__name{color:var(--dsw-alias-label-primary)}.dsx-auth__badge{margin-left:8px;padding:1px 6px;border-radius:999px;font-size:11px;color:var(--dsw-alias-state-success-primary);border:1px solid var(--dsw-alias-border-l2)}.dsx-auth__panel{display:flex;flex-direction:column;gap:8px;padding:10px;border-radius:8px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1)}.dsx-auth__notice,.dsx-auth__prompt{display:flex;flex-direction:column;gap:6px;color:var(--dsw-alias-label-secondary)}.dsx-auth__link{color:var(--dsw-alias-brand-primary);word-break:break-all}.dsx-auth__code{font-family:ui-monospace,monospace;letter-spacing:.08em;color:var(--dsw-alias-label-primary)}.dsx-auth__input{flex:1;min-width:0;padding:4px 8px;border-radius:6px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary)}.dsx-setting__hint--strong{color:var(--dsw-alias-label-primary)}.dsx-setting__hint--warn{color:var(--dsw-alias-state-warn-primary)}.dsx-setting__hint{color:var(--dsw-alias-label-secondary);font-size:12px;margin-top:2px;}',
1812
+ '.dsx-rev__panel{margin-top:14px;padding-top:12px;border-top:1px solid var(--dsw-alias-border-l1)}.dsx-rev__panel-head{display:flex;align-items:center;gap:8px;margin-bottom:8px}.dsx-rev__count{padding:1px 7px;border-radius:99px;background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-tertiary);font-size:11px}.dsx-rev__findings{display:flex;flex-direction:column;gap:8px}.dsx-rev__finding{padding:9px 11px;border-radius:8px;border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-2)}.dsx-rev__finding--false{opacity:.55}.dsx-rev__finding-head{display:flex;align-items:baseline;gap:7px;flex-wrap:wrap}.dsx-rev__finding-title{font-weight:600;color:var(--dsw-alias-label-primary);font-size:13px;flex:1}.dsx-rev__sev{padding:1px 6px;border-radius:4px;font-size:10px;text-transform:uppercase;letter-spacing:.03em}.dsx-rev__sev--crit{background:#5c1f22;color:#ffb4b4}.dsx-rev__sev--high{background:#5a3f16;color:#f0cd8a}.dsx-rev__sev--unknown{background:#3a3a44;color:#c9c9d4}.dsx-rev__sev--low{background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-tertiary)}.dsx-rev__who{font-size:11px;color:var(--dsw-alias-label-tertiary)}.dsx-rev__where{margin-top:4px;font-family:ui-monospace,monospace;font-size:11px;color:var(--dsw-alias-label-tertiary)}.dsx-rev__what{margin-top:4px;font-size:12px;color:var(--dsw-alias-label-secondary);line-height:1.45}.dsx-rev__verdict{display:flex;align-items:flex-start;gap:6px;margin-top:7px;padding-top:7px;border-top:1px dashed var(--dsw-alias-border-l1);font-size:11px;color:var(--dsw-alias-label-secondary)}.dsx-rev__verdict-mark{font-weight:700}.dsx-rev__verdict-text{flex:1;line-height:1.4}.dsx-rev__by{color:var(--dsw-alias-label-tertiary);white-space:nowrap}.dsx-rev__object{margin-top:5px}.dsx-rev__object-btn{border:0;background:none;padding:0;color:var(--dsw-alias-label-tertiary);font-size:11px;cursor:pointer;text-decoration:underline dotted}.dsx-rev__object-btn:hover{color:var(--dsw-alias-label-primary)}.dsx-rev__stats{margin-top:12px;padding-top:10px;border-top:1px solid var(--dsw-alias-border-l1)}.dsx-rev__stat{display:flex;justify-content:space-between;gap:10px;margin-top:4px;font-size:12px}.dsx-rev__stat-name{color:var(--dsw-alias-label-secondary)}.dsx-rev__stat-rate{color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums}.dsx-rev__tag{margin-left:6px;padding:1px 6px;border-radius:99px;background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-tertiary);font-size:10px}.dsx-rev__custom{display:inline-flex;align-items:center;gap:4px}.dsx-rev__input{padding:3px 6px;border-radius:6px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font-size:12px;width:170px}.dsx-rev__back{border:0;background:none;color:var(--dsw-alias-label-tertiary);cursor:pointer;font-size:14px;line-height:1;padding:0 2px}.dsx-rev__remove{margin-left:auto;border:0;background:none;color:var(--dsw-alias-label-tertiary);font-size:16px;line-height:1;padding:0 4px;cursor:pointer}.dsx-rev__remove:hover{color:#ffb4b4}.dsx-rev__add{display:flex;flex-wrap:wrap;align-items:center;gap:6px;margin-top:12px}.dsx-rev__add-label{color:var(--dsw-alias-label-secondary);font-size:12px;margin-right:2px}.dsx-rev__add-btn{padding:4px 11px;border:1px dashed var(--dsw-alias-border-l2);border-radius:99px;background:transparent;color:var(--dsw-alias-label-secondary);font-size:12px;cursor:pointer;font-family:inherit}.dsx-rev__add-btn:hover:not(:disabled){border-style:solid;color:var(--dsw-alias-label-primary)}.dsx-rev__add-btn:disabled{opacity:.4;cursor:default}.dsx-rev__list{display:flex;flex-direction:column;gap:8px}.dsx-rev__card{padding:10px 12px;border-radius:8px;border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-2);opacity:.6;transition:opacity .15s ease,border-color .15s ease}.dsx-rev__card--on{opacity:1;border-color:var(--dsw-alias-border-l2)}.dsx-rev__head{display:flex;align-items:center;gap:8px;cursor:pointer}.dsx-rev__name{font-weight:600;color:var(--dsw-alias-label-primary)}.dsx-rev__fields{display:flex;flex-wrap:wrap;gap:12px;margin-top:8px;padding-left:22px}.dsx-rev__field{display:flex;align-items:center;gap:6px;color:var(--dsw-alias-label-secondary);font-size:12px}.dsx-rev__select{padding:3px 6px;border-radius:6px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font-size:12px}.dsx-subs{display:flex;flex-direction:column;gap:14px;padding:4px 0}.dsx-subs__head{display:flex;flex-direction:column;gap:4px}.dsx-subs__title{font-weight:600;font-size:15px;color:var(--dsw-alias-label-primary)}.dsx-subs__note{padding-top:6px;border-top:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.5}.dsx-setting--stack{flex-direction:column;align-items:stretch;gap:10px}.dsx-auth__list{display:flex;flex-direction:column;gap:6px}.dsx-auth__row{display:flex;align-items:center;justify-content:space-between;gap:12px}.dsx-auth__name{color:var(--dsw-alias-label-primary)}.dsx-auth__badge{margin-left:8px;padding:1px 6px;border-radius:999px;font-size:11px;color:var(--dsw-alias-state-success-primary);border:1px solid var(--dsw-alias-border-l2)}.dsx-auth__panel{display:flex;flex-direction:column;gap:8px;padding:10px;border-radius:8px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1)}.dsx-auth__notice,.dsx-auth__prompt{display:flex;flex-direction:column;gap:6px;color:var(--dsw-alias-label-secondary)}.dsx-auth__link{color:var(--dsw-alias-brand-primary);word-break:break-all}.dsx-auth__code{font-family:ui-monospace,monospace;letter-spacing:.08em;color:var(--dsw-alias-label-primary)}.dsx-auth__input{flex:1;min-width:0;padding:4px 8px;border-radius:6px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary)}.dsx-setting__hint--strong{color:var(--dsw-alias-label-primary)}.dsx-setting__hint--warn{color:var(--dsw-alias-state-warn-primary)}.dsx-setting__hint{color:var(--dsw-alias-label-secondary);font-size:12px;margin-top:2px;}',
1792
1813
  '.dsx-setting__control{display:inline-flex;padding:2px;gap:2px;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;background:var(--dsw-alias-bg-layer-2);flex:none;}',
1793
1814
  '.dsx-seg{appearance:none;border:0;cursor:pointer;padding:4px 10px;border-radius:6px;font-size:12px;line-height:18px;background:transparent;color:var(--dsw-alias-label-secondary);transition:background .16s ease,color .16s ease;}',
1794
1815
  '.dsx-seg:hover{color:var(--dsw-alias-label-primary);}',
@@ -2318,13 +2339,12 @@ window.__ModuleLoader__.load({
2318
2339
  /**
2319
2340
  * Выбор значения: известные варианты плюс «своё».
2320
2341
  *
2321
- * Список моделей подсказка, а не ограничение. У наших ревизоров он
2322
- * приходит живым из маршрута, у внешних продуктов зашит коротким: спросить
2323
- * у Claude Code или Codex их собственный перечень нечем. Поэтому рядом со
2324
- * списком всегда есть «другая» — иначе любой новый выпуск модели делал бы
2325
- * интерфейс бесполезным до следующего обновления пакета.
2342
+ * `groups` разносит варианты по подписям. Для моделей это принципиально:
2343
+ * один и тот же `claude-opus-5` доступен и по подписке, и через
2344
+ * OpenRouter по токенам, и вперемешку они неразличимы а разница в том,
2345
+ * придёт ли за них счёт.
2326
2346
  */
2327
- function ValuePicker({ t, value, options, disabled, onPick, placeholder }) {
2347
+ function ValuePicker({ t, value, options, disabled, onPick, placeholder, groups }) {
2328
2348
  const known = options.some((option) => option.value === value)
2329
2349
  const [custom, setCustom] = React.useState(value !== null && !known)
2330
2350
 
@@ -2346,12 +2366,23 @@ window.__ModuleLoader__.load({
2346
2366
  disabled: disabled,
2347
2367
  title: t('review.fromList'),
2348
2368
  onClick: () => { setCustom(false); onPick(null) },
2349
- children: '×',
2369
+ children: '\u00d7',
2350
2370
  }),
2351
2371
  ],
2352
2372
  })
2353
2373
  }
2354
2374
 
2375
+ // Группы — это `optgroup`, а не подписи-разделители внутри списка:
2376
+ // браузер сам не даст выбрать заголовок и показывает его иначе.
2377
+ const body = groups === undefined
2378
+ ? options.map((option) => jsx('option', { value: option.value, children: option.label }, option.value))
2379
+ : groups
2380
+ .filter((group) => group.options.length > 0)
2381
+ .map((group) => jsx('optgroup', {
2382
+ label: group.label,
2383
+ children: group.options.map((option) => jsx('option', { value: option.value, children: option.label }, option.value)),
2384
+ }, group.label))
2385
+
2355
2386
  return jsxs('select', {
2356
2387
  className: 'dsx-rev__select',
2357
2388
  value: value === null ? '' : value,
@@ -2362,12 +2393,11 @@ window.__ModuleLoader__.load({
2362
2393
  },
2363
2394
  children: [
2364
2395
  jsx('option', { value: '', children: t('review.asConfigured') }, ''),
2365
- ...options.map((option) => jsx('option', { value: option.value, children: option.label }, option.value)),
2396
+ ...body,
2366
2397
  jsx('option', { value: '\u0000custom', children: t('review.other') }, 'custom'),
2367
2398
  ],
2368
2399
  })
2369
2400
  }
2370
-
2371
2401
  /** Класс важности: имена уровней приходят с Host по-русски. */
2372
2402
  function severityClass(severity) {
2373
2403
  if (severity === 'критично') return 'crit'
@@ -2385,16 +2415,19 @@ window.__ModuleLoader__.load({
2385
2415
  }
2386
2416
  // ── Ревизоры ─────────────────────────────────────────────────────────
2387
2417
  //
2388
- // Раздел настроек: чем и как проверять работу вторым мнением.
2418
+ // Свободный список, а не готовые режимы.
2419
+ //
2420
+ // Режимы «быстрый, обычный, глубокий» отвечали за человека на вопрос, на
2421
+ // который он вполне способен ответить сам, и при этом не давали позвать,
2422
+ // скажем, три разные модели одного семейства. Теперь: сколько угодно
2423
+ // записей, у каждой свой вид, модель и усилие.
2389
2424
  //
2390
- // Сверху режимы один выбор для тех, кто не хочет вникать. Ниже
2391
- // поштучно: кто участвует, какая модель, какое усилие. Поштучное НЕ
2392
- // спрятано за «для опытных»: человек, знающий, какая модель ему нужна,
2393
- // не должен её выпрашивать.
2425
+ // Рядом с каждой моделью показано, чем она оплачена. Это не украшение:
2426
+ // подписка безлимитна, а ключ API считается по токенам, и человек,
2427
+ // собирающий состав, должен видеть разницу ДО того, как получит счёт.
2394
2428
  function ReviewRow({ t }) {
2395
2429
  const [state, setState] = React.useState(null)
2396
2430
  const [busy, setBusy] = React.useState(false)
2397
- const [expanded, setExpanded] = React.useState(false)
2398
2431
 
2399
2432
  const translate = React.useRef(t)
2400
2433
  translate.current = t
@@ -2443,41 +2476,63 @@ window.__ModuleLoader__.load({
2443
2476
  React.useEffect(() => { refresh() }, [refresh])
2444
2477
 
2445
2478
  const catalogue = state !== null && Array.isArray(state.catalogue) ? state.catalogue : []
2446
- const modes = state !== null && Array.isArray(state.modes) ? state.modes : []
2447
- const settings = state !== null && state.settings ? state.settings : null
2448
2479
  const journal = state !== null && state.journal ? state.journal : null
2480
+ const effortMap = state !== null && state.efforts ? state.efforts : {}
2449
2481
 
2450
- // Изменение сохраняется сразу. Отдельная кнопка «применить» создаёт
2451
- // состояние «изменено, но не сохранено», в котором легко забыть нажать.
2452
- const save = React.useCallback((next) => { refresh(next) }, [refresh])
2453
-
2454
- const toggle = (kind) => {
2455
- if (settings === null) return
2456
- const on = settings.enabled.includes(kind)
2457
- const enabled = on ? settings.enabled.filter((k) => k !== kind) : [...settings.enabled, kind]
2458
- save({ ...settings, enabled })
2482
+ /**
2483
+ * Уровни усилия для записи.
2484
+ *
2485
+ * У наших ревизоров приходят с Host по выбранной модели; у внешних
2486
+ * продуктов остаются зашитыми спросить у них нечем.
2487
+ */
2488
+ const effortsFor = (entry) => {
2489
+ const spec = catalogue.find((item) => item.kind === entry.kind)
2490
+ if (spec && !spec.internal) return spec.efforts || []
2491
+ if (entry.provider === null || entry.model === null) return []
2492
+ const info = effortMap[entry.provider + '/' + entry.model]
2493
+ return info && Array.isArray(info.efforts) ? info.efforts : []
2459
2494
  }
2460
-
2461
- const pick = (kind, field, value) => {
2462
- if (settings === null) return
2463
- const previous = settings.choices[kind] || { provider: null, model: null, effort: null }
2464
- const choices = { ...settings.choices, [kind]: { ...previous, [field]: value } }
2465
- save({ ...settings, choices })
2495
+ const reviewers = state !== null && state.settings && Array.isArray(state.settings.reviewers)
2496
+ ? state.settings.reviewers
2497
+ : []
2498
+
2499
+ // Список уходит ЦЕЛИКОМ: удаление записи иначе было бы невыразимо
2500
+ // слияние на стороне Host всегда возвращало бы удалённое обратно.
2501
+ const save = React.useCallback((next) => { refresh({ reviewers: next }) }, [refresh])
2502
+
2503
+ const patch = (id, changes) =>
2504
+ save(reviewers.map((entry) => (entry.id === id ? { ...entry, ...changes } : entry)))
2505
+
2506
+ const remove = (id) => save(reviewers.filter((entry) => entry.id !== id))
2507
+
2508
+ const add = (kind) => {
2509
+ const spec = catalogue.find((entry) => entry.kind === kind)
2510
+ save([...reviewers, {
2511
+ kind,
2512
+ // Умолчание берётся из справочника: пустая модель означала бы
2513
+ // умолчание маршрута, то есть копию уже имеющегося ревизора.
2514
+ provider: spec && spec.internal ? spec.defaultProvider : null,
2515
+ model: spec ? spec.defaultModel : null,
2516
+ effort: null,
2517
+ enabled: true,
2518
+ }])
2466
2519
  }
2467
2520
 
2468
- // Модель нашего ревизора приходит парой «провайдер и имя»: у OpenRouter
2469
- // имя само содержит косую черту, и делить по ней нельзя провайдер там
2470
- // называется иначе, чем первая часть имени.
2471
- const pickModel = (kind, entry) => {
2472
- if (settings === null) return
2473
- const previous = settings.choices[kind] || { provider: null, model: null, effort: null }
2474
- const choices = {
2475
- ...settings.choices,
2476
- [kind]: { ...previous, provider: entry === null ? null : entry.provider, model: entry === null ? null : entry.id },
2477
- }
2478
- save({ ...settings, choices })
2521
+ /**
2522
+ * Отметка на карточке: по подписке или за отдельные деньги.
2523
+ *
2524
+ * Ровно два состояния, как и в списке моделей. Разница между
2525
+ * сохранённым ключом и ключом из окружения человека не касается:
2526
+ * и то и другое означает счёт по токенам.
2527
+ */
2528
+ const billingLabel = (kind) => {
2529
+ if (kind === undefined || kind === null) return null
2530
+ return kind === 'subscription' ? t('review.bySubscription') : t('review.byApiKey')
2479
2531
  }
2480
2532
 
2533
+ const atLimit = reviewers.length >= 8
2534
+ const activeCount = reviewers.filter((entry) => entry.enabled).length
2535
+
2481
2536
  return jsxs('div', {
2482
2537
  className: 'dsx-subs',
2483
2538
  children: [
@@ -2492,53 +2547,37 @@ window.__ModuleLoader__.load({
2492
2547
  ? jsx('div', { className: 'dsx-setting__hint dsx-setting__hint--warn', children: state.problem })
2493
2548
  : null,
2494
2549
 
2495
- // Режимы
2496
- modes.length === 0 ? null : jsx('div', {
2497
- className: 'dsx-rev__modes',
2498
- children: modes.map((mode) => jsxs('button', {
2499
- type: 'button',
2500
- className: 'dsx-rev__mode' + (settings !== null && settings.mode === mode.id ? ' dsx-rev__mode--on' : ''),
2501
- disabled: busy,
2502
- onClick: () => save({ mode: mode.id, applyMode: true }),
2503
- children: [
2504
- jsx('span', { className: 'dsx-rev__mode-name', children: mode.label }),
2505
- jsx('span', { className: 'dsx-rev__mode-hint', children: mode.hint }),
2506
- ],
2507
- }, mode.id)),
2508
- }),
2509
- settings !== null && settings.mode === null
2510
- ? jsx('div', { className: 'dsx-setting__hint', children: t('review.custom') })
2511
- : null,
2512
-
2513
- // Подробные настройки
2514
- jsx('button', {
2515
- type: 'button',
2516
- className: 'dsx-rev__toggle',
2517
- onClick: () => setExpanded(!expanded),
2518
- children: (expanded ? '▾ ' : '▸ ') + t('review.details'),
2519
- }),
2520
-
2521
- !expanded ? null : jsx('div', {
2550
+ jsx('div', {
2522
2551
  className: 'dsx-rev__list',
2523
- children: catalogue.map((entry) => {
2524
- const on = settings !== null && settings.enabled.includes(entry.kind)
2525
- // Запись может отсутствовать, если списки Host и браузера
2526
- // разойдутся; раньше обращение к ней роняло весь раздел.
2527
- const stored = settings === null ? null : settings.choices[entry.kind]
2528
- const choice = stored || { provider: null, model: null, effort: null }
2529
- const models = Array.isArray(entry.models) ? entry.models : []
2552
+ children: reviewers.map((entry) => {
2553
+ const spec = catalogue.find((item) => item.kind === entry.kind)
2554
+ const models = spec && Array.isArray(spec.models) ? spec.models : []
2555
+ const chosen = models.find((m) => m.id === entry.model)
2556
+ const billing = billingLabel(chosen ? chosen.billing : null)
2530
2557
 
2531
2558
  return jsxs('div', {
2532
- className: 'dsx-rev__card' + (on ? ' dsx-rev__card--on' : ''),
2559
+ className: 'dsx-rev__card' + (entry.enabled ? ' dsx-rev__card--on' : ''),
2533
2560
  children: [
2534
- jsxs('label', {
2561
+ jsxs('div', {
2535
2562
  className: 'dsx-rev__head',
2536
2563
  children: [
2537
- jsx('input', { type: 'checkbox', checked: on, disabled: busy, onChange: () => toggle(entry.kind) }),
2538
- jsx('span', { className: 'dsx-rev__name', children: entry.label }),
2539
- entry.internal
2540
- ? jsx('span', { className: 'dsx-rev__tag', children: t('review.ours') })
2541
- : null,
2564
+ jsx('input', {
2565
+ type: 'checkbox',
2566
+ checked: entry.enabled,
2567
+ disabled: busy,
2568
+ title: t('review.participates'),
2569
+ onChange: () => patch(entry.id, { enabled: !entry.enabled }),
2570
+ }),
2571
+ jsx('span', { className: 'dsx-rev__name', children: spec ? spec.label : entry.kind }),
2572
+ billing === null ? null : jsx('span', { className: 'dsx-rev__tag', children: billing }),
2573
+ jsx('button', {
2574
+ type: 'button',
2575
+ className: 'dsx-rev__remove',
2576
+ disabled: busy,
2577
+ title: t('review.remove'),
2578
+ onClick: () => remove(entry.id),
2579
+ children: '\u00d7',
2580
+ }),
2542
2581
  ],
2543
2582
  }),
2544
2583
  jsxs('div', {
@@ -2550,16 +2589,42 @@ window.__ModuleLoader__.load({
2550
2589
  jsx('span', { children: t('review.model') }),
2551
2590
  jsx(ValuePicker, {
2552
2591
  t: t,
2553
- value: choice.model,
2592
+ value: entry.model,
2554
2593
  disabled: busy,
2555
2594
  placeholder: t('review.modelName'),
2556
- options: models.map((m) => ({
2557
- value: m.id,
2558
- label: m.provider === null ? m.id : m.provider + ' · ' + m.id,
2559
- })),
2595
+ options: models.map((m) => ({ value: m.id, label: m.id })),
2596
+ // РАЗДЕЛЕНИЕ ПО ОПЛАТЕ.
2597
+ //
2598
+ // Один и тот же claude-opus-5 доступен и по
2599
+ // подписке, и через OpenRouter по токенам. В общем
2600
+ // списке они неразличимы, а разница в том, придёт
2601
+ // ли за них счёт. Провайдер стоит в названии: у
2602
+ // OpenRouter сотни моделей, и без него не понять,
2603
+ // чья она.
2604
+ // Групп ДВЕ, а не три. Различие, которое важно
2605
+ // человеку, одно: придёт ли за эту модель
2606
+ // отдельный счёт. Ключ, сохранённый в dsh, и
2607
+ // ключ из окружения — с этой стороны одно и то
2608
+ // же, а третья группа оказывалась почти пустой и
2609
+ // только мешала выбирать.
2610
+ groups: [
2611
+ {
2612
+ label: t('review.groupSubscription'),
2613
+ options: models.filter((m) => m.billing === 'subscription')
2614
+ .map((m) => ({ value: m.id, label: m.provider + ' \u00b7 ' + m.id })),
2615
+ },
2616
+ {
2617
+ label: t('review.groupPaid'),
2618
+ options: models.filter((m) => m.billing !== 'subscription')
2619
+ .map((m) => ({ value: m.id, label: (m.provider === null ? '' : m.provider + ' \u00b7 ') + m.id })),
2620
+ },
2621
+ ],
2560
2622
  onPick: (value) => {
2561
2623
  const found = models.find((m) => m.id === value)
2562
- pickModel(entry.kind, value === null ? null : (found || { provider: choice.provider, id: value }))
2624
+ patch(entry.id, {
2625
+ provider: value === null ? null : (found ? found.provider : entry.provider),
2626
+ model: value,
2627
+ })
2563
2628
  },
2564
2629
  }),
2565
2630
  ],
@@ -2570,21 +2635,52 @@ window.__ModuleLoader__.load({
2570
2635
  jsx('span', { children: t('review.effort') }),
2571
2636
  jsx(ValuePicker, {
2572
2637
  t: t,
2573
- value: choice.effort,
2638
+ value: entry.effort,
2574
2639
  disabled: busy,
2575
2640
  placeholder: t('review.effortName'),
2576
- options: (entry.efforts || []).map((e) => ({ value: e, label: e })),
2577
- onPick: (value) => pick(entry.kind, 'effort', value),
2641
+ // Уровни у КАЖДОЙ МОДЕЛИ свои, и зашитый список
2642
+ // врал почти везде. Проверено: у claude-opus-5 их
2643
+ // шесть, у deepseek-v4-pro — три, а одна и та же
2644
+ // модель через подписку и через OpenRouter даёт
2645
+ // разные наборы.
2646
+ //
2647
+ // Пока модель не выбрана, спрашивать нечего:
2648
+ // остаётся ввод своего значения.
2649
+ options: effortsFor(entry).map((e) => ({ value: e, label: e })),
2650
+ onPick: (value) => patch(entry.id, { effort: value }),
2578
2651
  }),
2579
2652
  ],
2580
2653
  }),
2581
2654
  ],
2582
2655
  }),
2583
2656
  ],
2584
- }, entry.kind)
2657
+ }, entry.id)
2585
2658
  }),
2586
2659
  }),
2587
2660
 
2661
+ // Добавление: по кнопке на каждый вид ревизора. Внешние продукты
2662
+ // помечены — они работают, только если установлены отдельно.
2663
+ jsxs('div', {
2664
+ className: 'dsx-rev__add',
2665
+ children: [
2666
+ jsx('span', { className: 'dsx-rev__add-label', children: t('review.add') }),
2667
+ ...catalogue.map((spec) => jsx('button', {
2668
+ type: 'button',
2669
+ className: 'dsx-rev__add-btn',
2670
+ disabled: busy || atLimit,
2671
+ title: spec.optional ? t('review.needsProduct') : '',
2672
+ onClick: () => add(spec.kind),
2673
+ children: spec.label + (spec.optional ? ' \u2217' : ''),
2674
+ }, spec.kind)),
2675
+ ],
2676
+ }),
2677
+ atLimit ? jsx('div', { className: 'dsx-setting__hint', children: t('review.limit') }) : null,
2678
+
2679
+ reviewers.length === 0
2680
+ ? jsx('div', { className: 'dsx-setting__hint dsx-setting__hint--warn', children: t('review.none') })
2681
+ : activeCount === 0
2682
+ ? jsx('div', { className: 'dsx-setting__hint dsx-setting__hint--warn', children: t('review.noneActive') })
2683
+ : null,
2588
2684
  // ── Панель находок ──────────────────────────────────────────
2589
2685
  //
2590
2686
  // Показывает последний обзор: что нашли ревизоры и что из этого
@@ -2680,9 +2776,6 @@ window.__ModuleLoader__.load({
2680
2776
  }),
2681
2777
  ],
2682
2778
  }),
2683
- settings !== null && settings.enabled.length === 0
2684
- ? jsx('div', { className: 'dsx-setting__hint dsx-setting__hint--warn', children: t('review.none') })
2685
- : null,
2686
2779
  jsx('div', { className: 'dsx-subs__note', children: t('review.note') }),
2687
2780
  ],
2688
2781
  })