newmark-agent 0.5.15 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,193 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.readModelResponseHealth = readModelResponseHealth;
37
+ exports.recordModelResponseHealth = recordModelResponseHealth;
38
+ exports.observeModelResponses = observeModelResponses;
39
+ const fs = __importStar(require("fs"));
40
+ const path = __importStar(require("path"));
41
+ const crypto_1 = require("crypto");
42
+ const facets = ['text', 'vision', 'tools'];
43
+ let lastReceiptStamp = 0;
44
+ const cache = new Map();
45
+ function directory(root, identity) {
46
+ const key = (0, crypto_1.createHash)('sha256').update(JSON.stringify(identity)).digest('hex');
47
+ return path.join(root, 'Runtime', 'model-response-health', key);
48
+ }
49
+ function records(dir) {
50
+ try {
51
+ return fs.readdirSync(dir).filter(name => /^\d{16}-[a-f0-9-]+\.json$/.test(name)).sort().reverse().flatMap(name => {
52
+ try {
53
+ return [{ name, health: JSON.parse(fs.readFileSync(path.join(dir, name), 'utf8')) }];
54
+ }
55
+ catch {
56
+ return [];
57
+ }
58
+ });
59
+ }
60
+ catch {
61
+ return [];
62
+ }
63
+ }
64
+ function readModelResponseHealth(root, identity) {
65
+ const dir = directory(root, identity), saved = cache.get(dir);
66
+ let revision = -1;
67
+ try {
68
+ revision = fs.statSync(dir).mtimeMs;
69
+ }
70
+ catch { /* no observations yet */ }
71
+ if (saved && saved.until > Date.now() && saved.revision === revision)
72
+ return saved.health;
73
+ const health = {};
74
+ for (const record of records(dir))
75
+ for (const facet of facets) {
76
+ const entry = record.health[facet];
77
+ if (!health[facet] && entry && typeof entry.ok === 'boolean')
78
+ health[facet] = entry;
79
+ }
80
+ if (cache.size > 512)
81
+ cache.clear();
82
+ cache.set(dir, { until: Date.now() + 150, revision, health });
83
+ return health;
84
+ }
85
+ function recordModelResponseHealth(root, identity, updates) {
86
+ // Immutable, atomic receipts merge independently across GUI/utility/WSL
87
+ // workers. A text result never overwrites a vision/tool result. No secrets,
88
+ // request bodies or server error text enter the observation files.
89
+ const dir = directory(root, identity), now = Date.now(), health = {};
90
+ for (const facet of facets)
91
+ if (typeof updates[facet] === 'boolean')
92
+ health[facet] = { ok: updates[facet], at: new Date(now).toISOString() };
93
+ if (!Object.keys(health).length)
94
+ return;
95
+ try {
96
+ fs.mkdirSync(dir, { recursive: true });
97
+ const latestStamp = Number(records(dir)[0]?.name.split('-')[0] || 0);
98
+ lastReceiptStamp = Math.max(now * 1000, lastReceiptStamp + 1, latestStamp + 1);
99
+ const file = path.join(dir, `${String(lastReceiptStamp).padStart(16, '0')}-${(0, crypto_1.randomUUID)()}.json`);
100
+ fs.writeFileSync(file + '.tmp', JSON.stringify(health));
101
+ fs.renameSync(file + '.tmp', file);
102
+ const counts = {};
103
+ for (const [index, record] of records(dir).entries()) {
104
+ let keep = index < 4;
105
+ for (const facet of facets)
106
+ if (record.health[facet]) {
107
+ counts[facet] = (counts[facet] || 0) + 1;
108
+ if (counts[facet] <= 2)
109
+ keep = true;
110
+ }
111
+ if (!keep) {
112
+ try {
113
+ fs.unlinkSync(path.join(dir, record.name));
114
+ }
115
+ catch { /* another writer may have pruned it */ }
116
+ }
117
+ }
118
+ cache.delete(dir);
119
+ }
120
+ catch { /* Diagnostic persistence must never break input or transport. */ }
121
+ }
122
+ function hasImage(value) {
123
+ if (Array.isArray(value))
124
+ return value.some(hasImage);
125
+ if (!value || typeof value !== 'object')
126
+ return false;
127
+ const entry = value;
128
+ return ['image_url', 'input_image', 'image'].includes(String(entry.type || '')) || hasImage(entry.content);
129
+ }
130
+ function failedFacet(error, images) {
131
+ const text = String(error).toLowerCase();
132
+ if (/image|vision|multimodal/.test(text) && images)
133
+ return 'vision';
134
+ if (/tools?\b|tool[_ -]?(call|choice|use)|function[_ -]?call/.test(text))
135
+ return 'tools';
136
+ return 'text';
137
+ }
138
+ function observeModelResponses(provider, record) {
139
+ const report = (updates) => { try {
140
+ record(updates);
141
+ }
142
+ catch { /* observation only */ } };
143
+ const chat = provider.chat.bind(provider);
144
+ provider.chat = async (...args) => {
145
+ const images = hasImage(args[1]);
146
+ try {
147
+ const response = await chat(...args);
148
+ if (response.trim())
149
+ report({ text: true, ...(images ? { vision: true } : {}) });
150
+ else
151
+ report({ text: false });
152
+ return response;
153
+ }
154
+ catch (error) {
155
+ if (!args[5]?.aborted && error?.name !== 'AbortError')
156
+ report({ [failedFacet(error, images)]: false });
157
+ throw error;
158
+ }
159
+ };
160
+ const stream = provider.chatStreamWithTools.bind(provider);
161
+ provider.chatStreamWithTools = async function* (...args) {
162
+ const images = hasImage(args[1]);
163
+ let meaningful = false, tools = false, failed = false;
164
+ try {
165
+ for await (const token of stream(...args)) {
166
+ if (token.providerError === true) {
167
+ failed = true;
168
+ report({ [failedFacet(token.text, images)]: false });
169
+ }
170
+ if (token.type === 'text' && token.text?.trim())
171
+ meaningful = true;
172
+ if (token.type === 'tool_call') {
173
+ meaningful = true;
174
+ tools = true;
175
+ }
176
+ yield token;
177
+ }
178
+ if (!failed && !args[6]?.aborted) {
179
+ if (meaningful)
180
+ report({ text: true, ...(images ? { vision: true } : {}), ...(tools || args[5].length ? { tools: true } : {}) });
181
+ else
182
+ report({ text: false });
183
+ }
184
+ }
185
+ catch (error) {
186
+ if (!failed && !args[6]?.aborted && error?.name !== 'AbortError')
187
+ report({ [failedFacet(error, images)]: false });
188
+ throw error;
189
+ }
190
+ };
191
+ return provider;
192
+ }
193
+ //# sourceMappingURL=modelResponseHealth.js.map
@@ -46,6 +46,8 @@ export interface ProviderUsageReported {
46
46
  cacheWrite: boolean;
47
47
  }
48
48
  export interface StreamToken {
49
+ /** Transport-originated failure, distinct from model-generated prose. */
50
+ providerError?: boolean;
49
51
  type: 'text' | 'tool_call' | 'status' | 'usage';
50
52
  text: string;
51
53
  toolCall?: {
@@ -1085,7 +1085,7 @@ class LLMProvider {
1085
1085
  yield* this.adapterResponsesBridge(model, messages, systemPrompt, temperature, maxTokens, tools, signal, downgradeTier);
1086
1086
  return;
1087
1087
  }
1088
- yield { type: 'text', text: event.error };
1088
+ yield { type: 'text', text: event.error, providerError: true };
1089
1089
  return;
1090
1090
  }
1091
1091
  }
@@ -1141,7 +1141,7 @@ class LLMProvider {
1141
1141
  continue;
1142
1142
  }
1143
1143
  if (event.type === 'response.failed') {
1144
- yield { type: 'text', text: event.error };
1144
+ yield { type: 'text', text: event.error, providerError: true };
1145
1145
  return;
1146
1146
  }
1147
1147
  }
@@ -1363,7 +1363,7 @@ class LLMProvider {
1363
1363
  yield { type: 'usage', text: '', usage: this.toStreamUsage(event.usage) };
1364
1364
  }
1365
1365
  else if (event.type === 'response.failed') {
1366
- yield { type: 'text', text: event.error };
1366
+ yield { type: 'text', text: event.error, providerError: true };
1367
1367
  return;
1368
1368
  }
1369
1369
  }
@@ -1411,7 +1411,7 @@ class LLMProvider {
1411
1411
  return;
1412
1412
  }
1413
1413
  if (completeCalls.some(block => !block.id || !block.name || !block.input || typeof block.input !== 'object' || Array.isArray(block.input))) {
1414
- yield { type: 'text', text: '[LLM Error] Provider returned incomplete or invalid tool-call arguments.' };
1414
+ yield { type: 'text', text: '[LLM Error] Provider returned incomplete or invalid tool-call arguments.', providerError: true };
1415
1415
  return;
1416
1416
  }
1417
1417
  for (const block of completeCalls) {
@@ -5414,17 +5414,20 @@ html.newmark-memory-overview-viewer body {
5414
5414
  }
5415
5415
 
5416
5416
  .model-chip {
5417
- display: inline-flex;
5418
- align-items: center;
5419
- gap: 4px;
5420
- padding: 3px 8px;
5421
- margin: 2px;
5422
- border-radius: var(--radius-sm);
5423
- background: rgba(91,120,255,0.1);
5424
- border: 1px solid rgba(91,120,255,0.2);
5425
- font-size: 11px;
5426
- color: var(--text-accent);
5427
- }
5417
+ display:flex; align-items:center; gap:12px; box-sizing:border-box;
5418
+ width:100%; min-width:0; margin:6px 0; padding:10px 12px;
5419
+ border-radius:var(--radius-sm); background:rgba(91,120,255,0.1);
5420
+ border:1px solid rgba(91,120,255,0.2); font-size:11px; color:var(--text-accent);
5421
+ }
5422
+ .model-chip-info { flex:1; min-width:0; text-align:left; overflow-wrap:anywhere; }
5423
+ .model-chip-actions { display:flex; flex:0 0 auto; align-items:center; gap:6px; margin-left:auto; }
5424
+ .model-response-warning { color:var(--text-dim); font-size:inherit; margin-left:4px; font-weight:400; }
5425
+ .model-enable-switch { width:38px; height:22px; padding:2px; border-radius:999px; border:1px solid var(--glass-border-1); background:var(--glass-bg-1); cursor:pointer; flex:none; }
5426
+ .model-enable-thumb { display:block; width:16px; height:16px; border-radius:50%; background:rgba(255,255,255,.55); transform:translateX(0); transition:transform 180ms var(--ease-out-expo); }
5427
+ .model-enable-switch[aria-checked="true"] { background:rgba(91,120,255,.35); }
5428
+ .model-enable-switch[aria-checked="true"] .model-enable-thumb { transform:translateX(16px); }
5429
+ .model-enable-switch:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
5430
+ .model-enable-switch:disabled { opacity:.6; }
5428
5431
 
5429
5432
  .model-chip .remove {
5430
5433
  cursor: pointer;
@@ -6273,7 +6276,7 @@ input[type="checkbox"].liquid-switch-dragging::before {
6273
6276
  transition: opacity 105ms var(--ease-out-expo), transform 105ms var(--ease-out-expo);
6274
6277
  }
6275
6278
  .liquid-switch-float.visible { opacity: 1; transform: translate(-50%, -50%) scale(1.22); }
6276
- .liquid-switch-float.landing { opacity: 0; transform: translate(-50%, -50%) scale(.86); }
6279
+ .liquid-switch-float.landing { opacity: 1; transform: translate(-50%, -50%) scale(.8181818); }
6277
6280
 
6278
6281
  /* Center composer and terminal stay solid. Only their left/right edge catches
6279
6282
  light so the glass sidebars remain the visual frame. */
@@ -7613,6 +7616,8 @@ var NEWMARK_I18N = {
7613
7616
  'model.available': 'available',
7614
7617
  'model.degraded': 'available (limited capabilities)',
7615
7618
  'model.unavailable': 'unavailable',
7619
+ 'model.responseAnomaly': '(response abnormal)',
7620
+ 'model.enabled': 'Enable model',
7616
7621
  'model.no': 'no',
7617
7622
  'model.yes': 'yes',
7618
7623
  'model.notChecked': 'not checked',
@@ -8354,6 +8359,8 @@ var NEWMARK_I18N = {
8354
8359
  'model.available': '可用',
8355
8360
  'model.degraded': '可用(部分能力受限)',
8356
8361
  'model.unavailable': '不可用',
8362
+ 'model.responseAnomaly': '(响应异常)',
8363
+ 'model.enabled': '启用模型',
8357
8364
  'model.no': '否',
8358
8365
  'model.yes': '是',
8359
8366
  'model.notChecked': '未检测',
@@ -16601,82 +16608,99 @@ requestAnimationFrame(function() {
16601
16608
  wireDirectPopupOptionPress();
16602
16609
  });
16603
16610
 
16604
- // Checkbox switches use one pointer-captured glass thumb that follows the
16605
- // pointer continuously, then commits to the nearest binary stop on release.
16611
+ // The body-level glass thumb survives settings rerenders and lands only after
16612
+ // reaching the released endpoint. Native keyboard clicks keep checkbox semantics.
16606
16613
  document.addEventListener('pointerdown', function(event) {
16607
16614
  var input = event.target && event.target.closest ? event.target.closest('input[type="checkbox"]') : null;
16608
- if (!input || input.disabled || event.button !== 0) return;
16615
+ if (!input || input.disabled || event.button !== 0 || input._liquidSettling) return;
16609
16616
  event.preventDefault();
16610
- var rect = input.getBoundingClientRect();
16611
- var pointer = event.pointerId;
16612
- var fraction = input.checked ? 1 : 0;
16613
- var startX = event.clientX;
16614
- var startY = event.clientY;
16615
- var dragging = false;
16616
- var canceledToScroll = false;
16617
+ var rect = input.getBoundingClientRect(), pointer = event.pointerId;
16618
+ var initial = input.checked, fraction = initial ? 1 : 0;
16619
+ var startX = event.clientX, startY = event.clientY;
16620
+ var dragging = false, canceledToScroll = false, finished = false;
16617
16621
  var dragThreshold = event.pointerType === 'mouse' ? 4 : 8;
16622
+ var float = document.createElement('div');
16623
+ float.className = 'liquid-switch-float';
16624
+ float.setAttribute('aria-hidden', 'true');
16625
+ document.body.appendChild(float);
16626
+ float.style.width = '22px'; float.style.height = '22px';
16627
+ attachKyantLiquidRenderer(float, liquidBackdropCachedFrame);
16628
+ captureLiquidBackdropFrame().then(function(frame) {
16629
+ if (float.isConnected && frame) attachKyantLiquidRenderer(float, frame);
16630
+ });
16618
16631
  input.dataset.liquidSuppressClick = 'true';
16619
16632
  clearTimeout(input._liquidSuppressClickTimer);
16620
- input._liquidSuppressClickTimer = setTimeout(function() {
16621
- delete input.dataset.liquidSuppressClick;
16622
- delete input._liquidSuppressClickTimer;
16623
- }, 700);
16624
- function update(clientX) {
16625
- fraction = ((clientX - rect.left - 12) / Math.max(1, rect.width - 24));
16626
- fraction = Math.max(0, Math.min(1, fraction));
16627
- input.style.setProperty('--liquid-switch-x', (18 * fraction) + 'px');
16628
- input.style.setProperty('--liquid-switch-progress', (100 * fraction) + '%');
16633
+ function paint(value, lift) {
16634
+ fraction = value;
16635
+ input.style.setProperty('--liquid-switch-x', (18 * value) + 'px');
16636
+ input.style.setProperty('--liquid-switch-progress', (100 * value) + '%');
16637
+ float.style.left = (rect.left + 12 + 18 * value) + 'px';
16638
+ float.style.top = (rect.top + rect.height / 2 - (lift ? 2 : 0)) + 'px';
16639
+ if (float._renderKyantGlass) float._renderKyantGlass({left:rect.left + 1 + 18 * value,top:rect.top + rect.height / 2 - 11 - (lift ? 2 : 0),width:22,height:22},true);
16629
16640
  }
16630
16641
  function finish(commit) {
16642
+ if (finished) return;
16643
+ finished = true;
16631
16644
  input.removeEventListener('pointermove', move);
16632
16645
  input.removeEventListener('pointerup', up);
16633
16646
  input.removeEventListener('pointercancel', cancel);
16634
- input.classList.remove('liquid-switch-dragging');
16635
- input.style.removeProperty('--liquid-switch-x');
16636
- input.style.removeProperty('--liquid-switch-progress');
16647
+ input.removeEventListener('lostpointercapture', cancel);
16637
16648
  try { input.releasePointerCapture(pointer); } catch (_) {}
16638
- if (!commit || canceledToScroll) return;
16639
- var next = dragging ? fraction >= 0.5 : !input.checked;
16640
- var changed = input.checked !== next;
16641
- input.checked = next;
16642
- if (changed) {
16643
- input.dispatchEvent(new Event('input', { bubbles: true }));
16644
- input.dispatchEvent(new Event('change', { bubbles: true }));
16649
+ var next = commit && !canceledToScroll ? (dragging ? fraction >= 0.5 : !initial) : initial;
16650
+ var from = fraction, to = next ? 1 : 0, started;
16651
+ input._liquidSettling = true;
16652
+ // Suppress the synthetic click even after a long hold; never time out mid-drag.
16653
+ input._liquidSuppressClickTimer = setTimeout(function() { delete input.dataset.liquidSuppressClick; }, 700);
16654
+ function frame(now) {
16655
+ if (started === undefined) started = now;
16656
+ var progress = Math.min(1, (now - started) / 180);
16657
+ paint(from + (to - from) * (1 - Math.pow(1 - progress, 3)), true);
16658
+ if (progress < 1) { requestAnimationFrame(frame); return; }
16659
+ // The glass remains present during landing, at the destination anchor.
16660
+ float.classList.add('landing');
16661
+ paint(to, false);
16662
+ setTimeout(function() {
16663
+ input.checked = next;
16664
+ input.classList.remove('liquid-switch-dragging');
16665
+ input.style.removeProperty('--liquid-switch-x');
16666
+ input.style.removeProperty('--liquid-switch-progress');
16667
+ delete input._liquidSettling;
16668
+ if (float._releaseKyantGlass) float._releaseKyantGlass();
16669
+ float.remove();
16670
+ if (next !== initial) {
16671
+ input.dispatchEvent(new Event('input', { bubbles: true }));
16672
+ input.dispatchEvent(new Event('change', { bubbles: true }));
16673
+ }
16674
+ }, 120);
16645
16675
  }
16676
+ requestAnimationFrame(frame);
16646
16677
  }
16647
- function move(moveEvent) {
16648
- if (moveEvent.pointerId !== pointer) return;
16678
+ function move(e) {
16679
+ if (e.pointerId !== pointer || canceledToScroll) return;
16649
16680
  if (!dragging) {
16650
- var deltaX = moveEvent.clientX - startX;
16651
- var deltaY = moveEvent.clientY - startY;
16652
- if (Math.hypot(deltaX, deltaY) < dragThreshold) return;
16653
- if (Math.abs(deltaY) > Math.abs(deltaX)) {
16654
- canceledToScroll = true;
16655
- return;
16656
- }
16681
+ var dx = e.clientX - startX, dy = e.clientY - startY;
16682
+ if (Math.hypot(dx, dy) < dragThreshold) return;
16683
+ if (Math.abs(dy) > Math.abs(dx)) { canceledToScroll = true; finish(false); return; }
16657
16684
  dragging = true;
16658
16685
  }
16659
- moveEvent.preventDefault();
16660
- update(moveEvent.clientX);
16661
- }
16662
- function up(upEvent) {
16663
- if (upEvent.pointerId === pointer) finish(true);
16664
- }
16665
- function cancel(cancelEvent) {
16666
- if (cancelEvent.pointerId === pointer) finish(false);
16686
+ e.preventDefault();
16687
+ paint(Math.max(0, Math.min(1, (initial ? 1 : 0) + (e.clientX - startX) / 18)), true);
16667
16688
  }
16689
+ function up(e) { if (e.pointerId === pointer) finish(true); }
16690
+ function cancel(e) { if (e.pointerId === pointer) finish(false); }
16668
16691
  input.classList.add('liquid-switch-dragging');
16669
- input.setPointerCapture(pointer);
16692
+ paint(fraction, true);
16693
+ float.getBoundingClientRect();
16694
+ float.classList.add('visible');
16695
+ try { input.setPointerCapture(pointer); } catch (_) { finish(false); return; }
16670
16696
  input.addEventListener('pointermove', move);
16671
16697
  input.addEventListener('pointerup', up);
16672
16698
  input.addEventListener('pointercancel', cancel);
16699
+ input.addEventListener('lostpointercapture', cancel);
16673
16700
  }, true);
16674
16701
  document.addEventListener('click', function(event) {
16675
16702
  var input = event.target && event.target.closest ? event.target.closest('input[type="checkbox"]') : null;
16676
- if (!input || input.dataset.liquidSuppressClick !== 'true') return;
16677
- clearTimeout(input._liquidSuppressClickTimer);
16678
- delete input.dataset.liquidSuppressClick;
16679
- delete input._liquidSuppressClickTimer;
16703
+ if (!input || (input.dataset.liquidSuppressClick !== 'true' && !input._liquidSettling)) return;
16680
16704
  event.preventDefault();
16681
16705
  event.stopImmediatePropagation();
16682
16706
  }, true);
@@ -16734,24 +16758,29 @@ document.addEventListener('click', function(event) {
16734
16758
  event.stopImmediatePropagation();
16735
16759
  }, true);
16736
16760
 
16761
+ function modelOptionLabelHtml(option) {
16762
+ return esc(option.textContent || option.value) + (option.dataset.responseAnomaly === 'true'
16763
+ ? '<span class="model-response-warning">' + esc(t('model.responseAnomaly')) + '</span>' : '');
16764
+ }
16765
+
16737
16766
  window.syncModelSelectSurface = function() {
16738
16767
  var sel = els['model-select'] || document.getElementById('model-select');
16739
16768
  var label = document.getElementById('model-select-label');
16740
16769
  var menu = document.getElementById('model-select-menu');
16741
16770
  if (!sel || !label || !menu) return;
16742
16771
  var selected = sel.options[sel.selectedIndex] || sel.options[0];
16743
- label.textContent = selected ? selected.textContent : (state.modelLabel || state.model || t('language.auto'));
16772
+ label.innerHTML = selected ? modelOptionLabelHtml(selected) : esc(state.modelLabel || state.model || t('language.auto'));
16744
16773
  var html = '';
16745
16774
  Array.from(sel.children).forEach(function(child) {
16746
16775
  if (child.tagName === 'OPTGROUP') {
16747
16776
  html += '<div class="model-select-menu-group" role="presentation">' + esc(child.label || '') + '</div>';
16748
16777
  Array.from(child.children).forEach(function(option) {
16749
- html += '<button type="button" class="model-select-menu-option' + (option.value === sel.value ? ' selected' : '') + '" role="option" aria-selected="' + (option.value === sel.value ? 'true' : 'false') + '" data-value="' + escAttr(option.value) + '" onclick="window.selectModelFromMenu(this.getAttribute(\'data-value\'))">' + esc(option.textContent || option.value) + '</button>';
16778
+ html += '<button type="button" class="model-select-menu-option' + (option.value === sel.value ? ' selected' : '') + '" role="option" aria-selected="' + (option.value === sel.value ? 'true' : 'false') + '" data-value="' + escAttr(option.value) + '" onclick="window.selectModelFromMenu(this.getAttribute(\'data-value\'))">' + modelOptionLabelHtml(option) + '</button>';
16750
16779
  });
16751
16780
  return;
16752
16781
  }
16753
16782
  if (child.tagName === 'OPTION') {
16754
- html += '<button type="button" class="model-select-menu-option' + (child.value === sel.value ? ' selected' : '') + '" role="option" aria-selected="' + (child.value === sel.value ? 'true' : 'false') + '" data-value="' + escAttr(child.value) + '" onclick="window.selectModelFromMenu(this.getAttribute(\'data-value\'))">' + esc(child.textContent || child.value) + '</button>';
16783
+ html += '<button type="button" class="model-select-menu-option' + (child.value === sel.value ? ' selected' : '') + '" role="option" aria-selected="' + (child.value === sel.value ? 'true' : 'false') + '" data-value="' + escAttr(child.value) + '" onclick="window.selectModelFromMenu(this.getAttribute(\'data-value\'))">' + modelOptionLabelHtml(child) + '</button>';
16755
16784
  }
16756
16785
  });
16757
16786
  // Rebuilding the option list must not destroy the carrier block. A pending
@@ -17042,13 +17071,13 @@ window.refreshModelSelect = function(options) {
17042
17071
  for (var midx = 0; midx < p.models.length; midx++) {
17043
17072
  var modelEntry = p.models[midx];
17044
17073
  var effectiveStatus = effectiveUiModelStatus(modelEntry);
17045
- if (effectiveStatus !== 'available' && effectiveStatus !== 'unvalidated' && effectiveStatus !== 'verified' && effectiveStatus !== 'degraded') continue;
17074
+ if (modelEntry && modelEntry.enabled === false) continue;
17046
17075
  var modelName = typeof modelEntry === 'string' ? modelEntry : (modelEntry.name || '');
17047
17076
  var modelLabel = typeof modelEntry === 'string' ? modelEntry : (modelEntry.display || modelEntry.name || '');
17048
17077
  var providerId = String(p.id || p.name || '');
17049
17078
  var modelValue = providerId && modelName ? 'deployment:' + encodeURIComponent(providerId) + ':' + encodeURIComponent(modelName) : modelName;
17050
17079
  if (!firstConcreteModel && modelValue) firstConcreteModel = modelValue;
17051
- providerOpts += '<option value="' + esc(modelValue) + '">' + esc(p.name + ' / ' + modelLabel) + '</option>';
17080
+ providerOpts += '<option data-response-anomaly="' + (modelHasResponseAnomaly(modelEntry) ? 'true' : 'false') + '" value="' + esc(modelValue) + '">' + esc(p.name + ' / ' + modelLabel) + '</option>';
17052
17081
  }
17053
17082
  if (providerOpts) modelOpts += '<optgroup label="' + esc(p.name) + '">' + providerOpts + '</optgroup>';
17054
17083
  }
@@ -18208,6 +18237,7 @@ window.sendMessage = async function(modeOverride, queuedText, opts) {
18208
18237
  var completedWorkRequest = captureConversationWorkSnapshotRequest(lockedTarget);
18209
18238
  api.getState(lockedTarget).then(function(s) {
18210
18239
  if (!isCurrentConversationWorkSnapshotRequest(completedWorkRequest)) return;
18240
+ if (s && s.providers) { state.providers = s.providers; window.refreshModelSelect(); }
18211
18241
  if (s && s.runtimeKey) registerRuntimeKey(lockedTarget, s.runtimeKey);
18212
18242
  var stillActiveAfterRefresh = isActiveConversationTarget(lockedTarget) && isViewingRuntimeConversationBranch(lockedTarget);
18213
18243
  if (stillActiveAfterRefresh) applyReturnedGoalState(s);
@@ -19907,8 +19937,18 @@ function formatModelStatus(status) {
19907
19937
  return status || t('model.notChecked');
19908
19938
  }
19909
19939
 
19940
+ function modelHasResponseAnomaly(modelEntry) {
19941
+ if (!modelEntry || typeof modelEntry === 'string') return false;
19942
+ var health = modelEntry.response_health || {};
19943
+ if (Object.keys(health).some(function(key) { return health[key].ok === false; })) return true;
19944
+ if (health.text && health.text.ok) return false;
19945
+ return ['unavailable', 'auth_error', 'invalid_config', 'rate_limited', 'failed', 'error'].indexOf(effectiveUiModelStatus(modelEntry)) >= 0;
19946
+ }
19947
+
19910
19948
  function effectiveUiModelStatus(modelEntry) {
19911
19949
  if (!modelEntry || typeof modelEntry === 'string') return 'unvalidated';
19950
+ var responseHealth = modelEntry.response_health || {};
19951
+ if (responseHealth.text) return responseHealth.text.ok ? 'available' : 'unavailable';
19912
19952
  var rawValidation = String((modelEntry.validation && modelEntry.validation.status) || '').toLowerCase();
19913
19953
  var rawEvaluation = String((modelEntry.evaluation && modelEntry.evaluation.status) || 'unvalidated').toLowerCase();
19914
19954
  var capabilities = modelEntry.validation && modelEntry.validation.capabilities || {};
@@ -20093,18 +20133,22 @@ function renderModelSettings() {
20093
20133
  if (typeof modelEntry !== 'string') {
20094
20134
  var tierMapCount = modelEntry.thinking_tier_map && typeof modelEntry.thinking_tier_map === 'object' ? Object.keys(modelEntry.thinking_tier_map).length : 0;
20095
20135
  details = '<span class="model-eval-line">ctx ' + esc(modelEntry.max_tokens || '') +
20096
- ' | ' + esc(t('model.vision')) + ' ' + (modelEntry.vision ? t('common.on') : t('common.off')) +
20136
+ ' | ' + esc(t('model.vision')) + ' ' + ((modelEntry.response_health && modelEntry.response_health.vision ? modelEntry.response_health.vision.ok : modelEntry.vision) ? t('common.on') : t('common.off')) +
20097
20137
  ' | ' + esc(t('model.thinking')) + ' ' + (modelEntry.thinking ? t('common.on') : t('common.off')) +
20098
20138
  (tierMapCount ? ' | ' + esc(t('model.thinkingTierMap')) + ' ' + tierMapCount : '') +
20099
20139
  ' | ' + esc(t('model.cost')) + ' ' + esc(evaluation ? evaluation.cost_rating : '-') +
20100
20140
  ' | ' + esc(t('model.performanceShort')) + ' ' + esc(modelEntry.capability_rating || '-') +
20101
20141
  ' | ' + esc(t('model.speed')) + ' ' + esc(modelEntry.speed_rating || '-') + '</span>';
20102
20142
  }
20103
- modelsHtml += '<span class="model-chip" title="' + esc(modelName) + '">' + esc(modelLabel) +
20104
- '<span class="' + statusClass + '"> ' + esc(status) + '</span>' +
20105
- details +
20106
- '<span class="remove" onclick="window.editModel(' + i + ',' + j + ')" title="' + escAttr(t('common.edit')) + '">' + iconSvg('square-pen', t('common.edit'), 'tiny') + '</span>' +
20107
- '<span class="remove" onclick="window.removeModel(' + i + ',' + j + ')" title="' + escAttr(t('common.remove')) + '">' + iconSvg('x', t('common.remove'), 'tiny') + '</span></span>';
20143
+ var modelEnabled = typeof modelEntry === 'string' || modelEntry.enabled !== false;
20144
+ var anomaly = modelHasResponseAnomaly(modelEntry);
20145
+ modelsHtml += '<div class="model-chip' + (modelEnabled ? '' : ' is-user-disabled') + '" data-model-name="' + escAttr(modelName) + '">' +
20146
+ '<div class="model-chip-info"><span title="' + escAttr(modelName) + '">' + esc(modelLabel) + '</span>' +
20147
+ (anomaly ? '<span class="model-response-warning">' + esc(t('model.responseAnomaly')) + '</span>' : '<span class="' + statusClass + '"> ' + esc(status) + '</span>') + details + '</div>' +
20148
+ '<div class="model-chip-actions">' +
20149
+ '<input type="checkbox" class="model-enable-switch" role="switch" ' + (modelEnabled ? 'checked ' : '') + 'aria-label="' + escAttr(t('model.enabled') + ': ' + modelLabel) + '" onchange="window.toggleModelEnabled(' + i + ',' + j + ',this)">' +
20150
+ '<button type="button" class="provider-icon-btn" onclick="window.editModel(' + i + ',' + j + ')" aria-label="' + escAttr(t('common.edit')) + '">' + iconSvg('square-pen', t('common.edit'), 'tiny') + '</button>' +
20151
+ '<button type="button" class="provider-icon-btn danger" onclick="window.removeModel(' + i + ',' + j + ')" aria-label="' + escAttr(t('common.delete')) + '">' + iconSvg('trash-2', t('common.delete'), 'tiny') + '</button></div></div>';
20108
20152
  }
20109
20153
  var providerEnabled = p.enabled !== false;
20110
20154
  var providerToggleLabel = providerEnabled ? t('model.disableProvider') : t('model.enableProvider');
@@ -21399,6 +21443,26 @@ window.deleteArchive = function(idx, scope) {
21399
21443
  });
21400
21444
  };
21401
21445
 
21446
+ window.toggleModelEnabled = async function(provIdx, modelIdx, button) {
21447
+ var provider = state.providers[provIdx];
21448
+ if (!provider || !provider.models || !provider.models[modelIdx]) return;
21449
+ var next = JSON.parse(JSON.stringify(state.providers));
21450
+ var previous = provider.models[modelIdx];
21451
+ next[provIdx].models[modelIdx] = typeof previous === 'string' ? { name: previous, enabled: false }
21452
+ : Object.assign({}, previous, { enabled: previous.enabled === false });
21453
+ if (button) button.disabled = true;
21454
+ try {
21455
+ var result = await api.saveConfig({ providers: next });
21456
+ if (result === false || (result && result.error)) throw new Error(result.error || 'Model update failed');
21457
+ state.providers = next;
21458
+ window.refreshModelSelect();
21459
+ window.openSettings('models');
21460
+ } catch (error) {
21461
+ if (button) { button.disabled = false; button.checked = typeof previous === 'string' || previous.enabled !== false; }
21462
+ showUiNotice(String(error.message || error), 'error', 'model-enabled');
21463
+ }
21464
+ };
21465
+
21402
21466
  window.removeModel = function(provIdx, modelIdx) {
21403
21467
  if (state.providers[provIdx] && state.providers[provIdx].models) {
21404
21468
  state.providers[provIdx].models.splice(modelIdx, 1);