@goodandready/dsh-context-lens 0.1.15 → 0.1.17

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
@@ -5,7 +5,7 @@ window.__ModuleLoader__.load({
5
5
  const React = require('react');
6
6
  const NS = '@goodandready/dsh-context-lens';
7
7
 
8
- const en = {
8
+ const en = {
9
9
  title: 'Context Lens & Token Guard',
10
10
  sub: 'AST compression, log filtering, token budget',
11
11
  mode: 'Compression mode',
@@ -23,62 +23,289 @@ window.__ModuleLoader__.load({
23
23
  lowBudget: 'Budget nearly exhausted',
24
24
  threshold: 'Auto-compress threshold (chars)',
25
25
  autoCollapse: 'Warn when budget is nearly exhausted',
26
+ budgetAlertPercent: 'Budget alert threshold (%)',
26
27
  previewApprox: 'Server preview',
27
28
  noData: 'No data yet',
28
29
  refresh: 'Refresh',
29
30
  ops: 'ops',
30
31
  active: 'Active',
31
32
  readyState: 'Ready',
32
- lowBudgetBadge: 'Low Budget ⚠'
33
+ lowBudgetBadge: 'Low Budget ⚠',
34
+ focusPaths: 'Active focus',
35
+ noFocus: 'No focus paths set',
36
+ clearFocus: 'Clear focus'
33
37
  };
34
- const ru = {
35
- title: 'Context Lens & Token Guard',
36
- sub: 'Сжатие AST, фильтрация логов, контроль токенов',
37
- mode: 'Режим сжатия',
38
- depth: 'Глубина AST',
39
- tracking: 'Считать экономию',
40
- preview: 'Предпросмотр лога',
41
- compress: 'Сжать',
42
- saved: 'Сэкономлено',
43
- tokens: 'токенов',
44
- placeholder: 'Вставьте лог Jest/Pytest…',
45
- saving: 'Сохранение…',
46
- ready: 'Готово',
47
- budget: 'Бюджет',
48
- history: 'Последние операции',
49
- lowBudget: 'Бюджет почти исчерпан',
50
- threshold: 'Порог авто-сжатия (символы)',
51
- autoCollapse: 'Предупреждать при почти исчерпанном бюджете',
52
- previewApprox: 'Серверный предпросмотр',
53
- noData: 'Пока нет данных',
54
- refresh: 'Обновить',
55
- ops: 'оп.',
56
- active: 'Активен',
57
- readyState: 'Готов',
58
- lowBudgetBadge: 'Мало бюджета ⚠'
38
+
39
+ const zh = {
40
+ title: 'Context Lens 上下文镜头与 Token 守卫',
41
+ sub: 'AST 代码精简、日志过滤与 Token 预算监控',
42
+ mode: '压缩模式',
43
+ depth: 'AST 最大深度',
44
+ tracking: '追踪 Token 节省',
45
+ preview: '日志预览',
46
+ compress: '压缩',
47
+ saved: '已节省',
48
+ tokens: 'Token',
49
+ placeholder: '在此粘贴 Jest/Pytest 等测试日志…',
50
+ saving: '保存中…',
51
+ ready: '已就绪',
52
+ budget: '预算',
53
+ history: '近期操作',
54
+ lowBudget: 'Token 预算即将耗尽',
55
+ threshold: '自动压缩阈值 (字符数)',
56
+ autoCollapse: '预算偏低时在界面提示警告',
57
+ budgetAlertPercent: '预算警告阈值 (%)',
58
+ previewApprox: '服务端压缩预览',
59
+ noData: '暂无数据',
60
+ refresh: '刷新',
61
+ ops: '次操作',
62
+ active: '运行中',
63
+ readyState: '已就绪',
64
+ lowBudgetBadge: '预算紧张 ⚠',
65
+ focusPaths: '当前聚焦路径',
66
+ noFocus: '未设置聚焦路径',
67
+ clearFocus: '清除聚焦'
59
68
  };
60
69
 
61
- // #45: preview uses POST /dsh-context-lens/compress-preview (server compressor)
70
+ function ensureCss() {
71
+ if (typeof document === 'undefined') return;
72
+ if (document.getElementById('dsh-context-lens-css')) return;
73
+ const style = document.createElement('style');
74
+ style.id = 'dsh-context-lens-css';
75
+ style.dataset.dshPlugin = NS;
76
+ style.textContent = `
77
+ .cl-section-card {
78
+ border: 1px solid var(--dsw-alias-border-l2);
79
+ background: var(--dsw-alias-bg-layer-3);
80
+ border-radius: 12px;
81
+ padding: 16px 18px;
82
+ margin-bottom: 12px;
83
+ display: flex;
84
+ flex-direction: column;
85
+ gap: 12px;
86
+ list-style: none;
87
+ }
88
+ .cl-section-header {
89
+ background: none;
90
+ border: none;
91
+ cursor: pointer;
92
+ display: flex;
93
+ align-items: center;
94
+ width: 100%;
95
+ padding: 0;
96
+ text-align: left;
97
+ }
98
+ .cl-badge {
99
+ font-size: 11px;
100
+ padding: 2px 8px;
101
+ border-radius: 999px;
102
+ border: 1px solid var(--dsw-alias-border-l2);
103
+ display: inline-flex;
104
+ align-items: center;
105
+ gap: 4px;
106
+ font-weight: 500;
107
+ }
108
+ .cl-badge-ok {
109
+ border-color: var(--dsw-alias-state-success-primary, #10b981);
110
+ color: var(--dsw-alias-state-success-primary, #10b981);
111
+ background: rgba(16, 185, 129, 0.08);
112
+ }
113
+ .cl-badge-warn {
114
+ border-color: var(--dsw-alias-state-error-primary, #ef4444);
115
+ color: var(--dsw-alias-state-error-primary, #ef4444);
116
+ background: rgba(239, 68, 68, 0.08);
117
+ }
118
+ .cl-stat-grid {
119
+ display: grid;
120
+ grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
121
+ gap: 10px;
122
+ margin-top: 4px;
123
+ }
124
+ .cl-stat-box {
125
+ padding: 10px 12px;
126
+ border: 1px solid var(--dsw-alias-border-l2);
127
+ border-radius: 8px;
128
+ background: var(--dsw-alias-bg-layer-2);
129
+ display: flex;
130
+ flex-direction: column;
131
+ gap: 3px;
132
+ }
133
+ .cl-stat-val {
134
+ font-size: 16px;
135
+ font-weight: 700;
136
+ color: var(--dsw-alias-label-primary);
137
+ }
138
+ .cl-stat-lbl {
139
+ font-size: 11px;
140
+ color: var(--dsw-alias-label-secondary);
141
+ }
142
+ .cl-bar-box {
143
+ display: flex;
144
+ flex-direction: column;
145
+ gap: 6px;
146
+ padding: 10px 12px;
147
+ border: 1px solid var(--dsw-alias-border-l2);
148
+ border-radius: 8px;
149
+ background: var(--dsw-alias-bg-layer-2);
150
+ }
151
+ .cl-bar-track {
152
+ width: 100%;
153
+ height: 8px;
154
+ border-radius: 999px;
155
+ background: var(--dsw-alias-bg-layer-1);
156
+ overflow: hidden;
157
+ border: 1px solid var(--dsw-alias-border-l2);
158
+ }
159
+ .cl-bar-fill {
160
+ height: 100%;
161
+ border-radius: 999px;
162
+ transition: width 0.3s;
163
+ }
164
+ .cl-input {
165
+ height: 34px;
166
+ border: 1px solid var(--dsw-alias-border-l2);
167
+ background: var(--dsw-alias-bg-layer-2);
168
+ color: var(--dsw-alias-label-primary);
169
+ border-radius: 8px;
170
+ padding: 0 12px;
171
+ font-size: 13px;
172
+ box-sizing: border-box;
173
+ }
174
+ .cl-input:focus {
175
+ outline: none;
176
+ border-color: var(--dsw-alias-state-brand-primary, #3b82f6);
177
+ }
178
+ .cl-btn {
179
+ appearance: none;
180
+ font: inherit;
181
+ cursor: pointer;
182
+ border: 1px solid var(--dsw-alias-border-l2);
183
+ border-radius: 8px;
184
+ padding: 6px 12px;
185
+ font-size: 12px;
186
+ background: var(--dsw-alias-bg-layer-2);
187
+ color: var(--dsw-alias-label-primary);
188
+ font-weight: 500;
189
+ display: inline-flex;
190
+ align-items: center;
191
+ justify-content: center;
192
+ gap: 6px;
193
+ transition: all 0.15s ease;
194
+ }
195
+ .cl-btn:hover:not(:disabled) {
196
+ background: var(--dsw-alias-bg-layer-4, var(--dsw-alias-bg-layer-2));
197
+ border-color: var(--dsw-alias-label-dimmed, var(--dsw-alias-border-l2));
198
+ }
199
+ .cl-btn-primary {
200
+ background: var(--dsw-alias-label-primary);
201
+ color: var(--dsw-alias-bg-layer-3);
202
+ border-color: transparent;
203
+ }
204
+ .cl-btn-primary:hover:not(:disabled) {
205
+ opacity: 0.88;
206
+ }
207
+ .cl-btn-disabled {
208
+ opacity: 0.5;
209
+ cursor: not-allowed;
210
+ }
211
+ .cl-banner-warn {
212
+ padding: 10px 14px;
213
+ border-radius: 8px;
214
+ background: rgba(239, 68, 68, 0.1);
215
+ border: 1px solid var(--dsw-alias-state-error-primary, #ef4444);
216
+ color: var(--dsw-alias-state-error-primary, #ef4444);
217
+ font-size: 12px;
218
+ display: flex;
219
+ align-items: center;
220
+ gap: 8px;
221
+ font-weight: 500;
222
+ }
223
+ .cl-hist-item {
224
+ font-size: 11px;
225
+ color: var(--dsw-alias-label-secondary);
226
+ padding: 4px 0;
227
+ border-bottom: 1px solid var(--dsw-alias-border-l3, var(--dsw-alias-border-l2));
228
+ }
229
+ .cl-hist-item:last-child {
230
+ border-bottom: none;
231
+ }
232
+ .cl-tag {
233
+ display: inline-flex;
234
+ align-items: center;
235
+ gap: 4px;
236
+ padding: 2px 6px;
237
+ background: var(--dsw-alias-bg-layer-1);
238
+ border: 1px solid var(--dsw-alias-border-l2);
239
+ border-radius: 4px;
240
+ font-size: 11px;
241
+ font-family: monospace;
242
+ color: var(--dsw-alias-label-secondary);
243
+ }
244
+ .cl-error-box {
245
+ padding: 12px;
246
+ border-radius: 8px;
247
+ background: rgba(239, 68, 68, 0.08);
248
+ border: 1px solid var(--dsw-alias-state-error-primary, #ef4444);
249
+ color: var(--dsw-alias-state-error-primary, #ef4444);
250
+ font-size: 12px;
251
+ }
252
+ `;
253
+ document.head.appendChild(style);
254
+ }
255
+
256
+ function createErrorBoundary() {
257
+ if (!React || typeof React.Component !== 'function') {
258
+ return function NoopBoundary(props) { return (props && props.children) || null; };
259
+ }
260
+ return class ErrorBoundary extends React.Component {
261
+ constructor(props) {
262
+ super(props);
263
+ this.state = { hasError: false, error: null };
264
+ }
265
+ static getDerivedStateFromError(error) {
266
+ return { hasError: true, error };
267
+ }
268
+ componentDidCatch(error, errorInfo) {
269
+ console.error('[dsh-context-lens] UI Error:', error, errorInfo);
270
+ }
271
+ render() {
272
+ if (this.state.hasError) {
273
+ return React.createElement('div', { className: 'cl-error-box' },
274
+ React.createElement('div', { style: { fontWeight: 600, marginBottom: 4 } }, '⚠ Context Lens UI Error'),
275
+ React.createElement('div', { style: { fontSize: 11, wordBreak: 'break-all' } }, String(this.state.error?.message || this.state.error)),
276
+ React.createElement('button', {
277
+ type: 'button',
278
+ className: 'cl-btn',
279
+ style: { marginTop: 8, fontSize: 11, padding: '3px 8px' },
280
+ onClick: () => this.setState({ hasError: false, error: null })
281
+ }, 'Retry')
282
+ );
283
+ }
284
+ return (this.props && this.props.children) || null;
285
+ }
286
+ };
287
+ }
288
+ const ErrorBoundary = createErrorBoundary();
62
289
 
63
- // #43: shared status UI for LensTab + HeaderChip popover
64
- function getActiveLocale(ctx) {
290
+ function getActiveLocale(ctx) {
65
291
  try {
66
292
  if (ctx && ctx.locale && typeof ctx.locale.getSnapshot === 'function') {
67
293
  const snap = ctx.locale.getSnapshot();
68
- if (snap && snap.active && snap.active.startsWith('ru')) return ru;
294
+ if (snap && snap.active && snap.active.startsWith('zh')) return zh;
69
295
  }
70
296
  } catch {}
71
297
  return en;
72
298
  }
73
299
 
74
- function StatusPanel({ stats, history, labels, onRefresh, title, showBadge, compact }) {
300
+ function StatusPanel({ stats, history, focus, sessionId, labels, onRefresh, onClearFocus, title, showBadge, compact }) {
301
+ ensureCss();
75
302
  const L = labels || en;
76
303
  const warn = !!(stats && stats.lowBudget);
77
304
  const hasSavings = !!(stats && stats.savedTokens > 0);
78
- const barColor = warn ? '#d73a4a' : 'var(--dsw-alias-label-primary)';
79
- const previewLen = compact ? 28 : 40;
80
- const barH = compact ? 5 : 6;
305
+ const barColor = warn ? 'var(--dsw-alias-state-error-primary, #ef4444)' : 'var(--dsw-alias-state-success-primary, #10b981)';
306
+ const previewLen = compact ? 28 : 44;
81
307
  const children = [];
308
+
82
309
  if (title || showBadge) {
83
310
  children.push(React.createElement('div', {
84
311
  key: 'head',
@@ -92,78 +319,113 @@ window.__ModuleLoader__.load({
92
319
  : React.createElement('span'),
93
320
  showBadge
94
321
  ? React.createElement('span', {
95
- style: {
96
- fontSize: 11, padding: '1px 6px', borderRadius: 4,
97
- background: warn ? 'rgba(215, 58, 74, 0.15)' : 'var(--dsw-alias-bg-layer-2)',
98
- color: warn ? '#d73a4a' : 'var(--dsw-alias-label-tertiary)',
99
- border: '1px solid ' + (warn ? 'rgba(215, 58, 74, 0.3)' : 'var(--dsw-alias-border-l2)')
100
- }
322
+ className: 'cl-badge ' + (warn ? 'cl-badge-warn' : 'cl-badge-ok')
101
323
  }, warn ? L.lowBudgetBadge : (stats ? L.active : L.readyState))
102
324
  : null
103
325
  ));
104
326
  }
105
- children.push(
106
- hasSavings
107
- ? React.createElement('div', { key: 'sum', style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)', marginBottom: compact ? 8 : 0 } },
108
- `${L.saved} ${stats.savedTokens} ${L.tokens} (${stats.savedPercent}%) · ${stats.calls} ${L.ops}`)
109
- : React.createElement('div', { key: 'sum', style: { fontSize: 12, color: compact ? 'var(--dsw-alias-label-tertiary)' : 'var(--dsw-alias-label-secondary)', marginBottom: compact ? 8 : 0 } }, L.noData)
110
- );
327
+
328
+ if (stats) {
329
+ children.push(React.createElement('div', { key: 'stats', className: 'cl-stat-grid' },
330
+ React.createElement('div', { className: 'cl-stat-box' },
331
+ React.createElement('div', { className: 'cl-stat-val' }, stats.savedTokens ? `${stats.savedTokens}` : '0'),
332
+ React.createElement('div', { className: 'cl-stat-lbl' }, `${L.saved} ${L.tokens}`)
333
+ ),
334
+ React.createElement('div', { className: 'cl-stat-box' },
335
+ React.createElement('div', { className: 'cl-stat-val' }, `${stats.savedPercent || 0}%`),
336
+ React.createElement('div', { className: 'cl-stat-lbl' }, '% ' + L.saved)
337
+ ),
338
+ React.createElement('div', { className: 'cl-stat-box' },
339
+ React.createElement('div', { className: 'cl-stat-val' }, `${stats.calls || 0}`),
340
+ React.createElement('div', { className: 'cl-stat-lbl' }, L.ops)
341
+ )
342
+ ));
343
+ } else {
344
+ children.push(React.createElement('div', {
345
+ key: 'nodata',
346
+ style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)', padding: '6px 0' }
347
+ }, L.noData));
348
+ }
349
+
111
350
  if (stats && stats.lowBudget && !showBadge) {
112
- children.push(React.createElement('div', { key: 'warn', style: { marginTop: 6, fontSize: 11, color: '#d73a4a' } }, '⚠ ' + L.lowBudget));
351
+ children.push(React.createElement('div', {
352
+ key: 'warn',
353
+ className: 'cl-banner-warn',
354
+ style: { marginTop: 8 }
355
+ }, '⚠ ' + L.lowBudget));
113
356
  }
357
+
114
358
  if (stats && stats.budgetLimit) {
115
- children.push(React.createElement('div', { key: 'budget', style: { marginTop: compact ? 6 : 8, marginBottom: compact ? 8 : 0 } },
359
+ children.push(React.createElement('div', { key: 'budget', className: 'cl-bar-box', style: { marginTop: 8 } },
116
360
  React.createElement('div', {
117
- style: {
118
- display: compact ? 'flex' : 'block',
119
- justifyContent: 'space-between',
120
- fontSize: 11,
121
- color: warn ? '#d73a4a' : 'var(--dsw-alias-label-secondary)',
122
- marginBottom: 4
123
- }
361
+ style: { display: 'flex', justifyContent: 'space-between', fontSize: 11, color: warn ? 'var(--dsw-alias-state-error-primary, #ef4444)' : 'var(--dsw-alias-label-secondary)' }
124
362
  },
125
- compact
126
- ? React.createElement(React.Fragment, null,
127
- React.createElement('span', null, L.budget),
128
- React.createElement('span', null, `${stats.budgetUsed || 0} / ${stats.budgetLimit} (${stats.budgetPercent || 0}%)`)
129
- )
130
- : `${L.budget} ${stats.budgetUsed}/${stats.budgetLimit} ${L.tokens} (${stats.budgetPercent}%)${warn ? ' ⚠' : ''}`
363
+ React.createElement('span', { style: { fontWeight: 500 } }, L.budget),
364
+ React.createElement('span', null, `${stats.budgetUsed || 0} / ${stats.budgetLimit} (${stats.budgetPercent || 0}%)`)
131
365
  ),
132
- React.createElement('div', { style: { height: barH, borderRadius: 3, background: compact ? 'var(--dsw-alias-bg-layer-1)' : 'var(--dsw-alias-bg-layer-2, var(--dsw-alias-bg-layer-3))', overflow: 'hidden' } },
133
- React.createElement('div', { style: { height: '100%', width: Math.min(100, stats.budgetPercent || 0) + '%', background: barColor, transition: 'width .2s' } })
366
+ React.createElement('div', { className: 'cl-bar-track' },
367
+ React.createElement('div', {
368
+ className: 'cl-bar-fill',
369
+ style: { width: Math.min(100, stats.budgetPercent || 0) + '%', background: barColor }
370
+ })
134
371
  )
135
372
  ));
136
373
  }
374
+
375
+ // Active focus paths section
376
+ const focusPaths = (focus && focus.paths) || [];
377
+ if (focusPaths.length > 0 || onClearFocus) {
378
+ children.push(React.createElement('div', {
379
+ key: 'focus',
380
+ style: { marginTop: 10, paddingTop: 8, borderTop: '1px solid var(--dsw-alias-border-l2)' }
381
+ },
382
+ React.createElement('div', {
383
+ style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 4 }
384
+ },
385
+ React.createElement('span', { style: { fontSize: 11, fontWeight: 600, color: 'var(--dsw-alias-label-secondary)' } }, L.focusPaths),
386
+ focusPaths.length > 0 && onClearFocus
387
+ ? React.createElement('button', {
388
+ type: 'button',
389
+ onClick: onClearFocus,
390
+ style: { background: 'none', border: 'none', padding: 0, fontSize: 10, color: 'var(--dsw-alias-label-tertiary)', cursor: 'pointer', textDecoration: 'underline' }
391
+ }, L.clearFocus)
392
+ : null
393
+ ),
394
+ focusPaths.length > 0
395
+ ? React.createElement('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 4, marginTop: 4 } },
396
+ ...focusPaths.slice(0, 4).map((p, i) => React.createElement('span', { key: i, className: 'cl-tag' }, p))
397
+ )
398
+ : React.createElement('div', { style: { fontSize: 11, color: 'var(--dsw-alias-label-tertiary)', fontStyle: 'italic' } }, L.noFocus)
399
+ ));
400
+ }
401
+
137
402
  if (history && history.length) {
138
403
  children.push(React.createElement('div', {
139
404
  key: 'hist',
140
- style: { marginTop: compact ? 8 : 10, paddingTop: compact ? 8 : 0, borderTop: compact ? '1px solid var(--dsw-alias-border-l3)' : undefined }
405
+ style: { marginTop: 10, paddingTop: 8, borderTop: '1px solid var(--dsw-alias-border-l2)' }
141
406
  },
142
- React.createElement('div', { style: { fontSize: 11, color: 'var(--dsw-alias-label-tertiary)', marginBottom: 4 } }, L.history),
407
+ React.createElement('div', { style: { fontSize: 11, fontWeight: 600, color: 'var(--dsw-alias-label-tertiary)', marginBottom: 4 } }, L.history),
143
408
  ...history.slice(0, 3).map((h) => React.createElement('div', {
144
409
  key: h.id,
145
- style: { fontSize: 11, color: 'var(--dsw-alias-label-secondary)', padding: '2px 0', overflow: compact ? 'hidden' : undefined, textOverflow: compact ? 'ellipsis' : undefined, whiteSpace: compact ? 'nowrap' : undefined }
410
+ className: 'cl-hist-item',
411
+ style: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }
146
412
  }, `−${h.savedTokens} tk (${h.savedPercent}%) · ${(h.preview || '').slice(0, previewLen)}`))
147
413
  ));
148
414
  }
415
+
149
416
  if (onRefresh) {
150
417
  children.push(React.createElement('div', {
151
418
  key: 'act',
152
- style: { marginTop: compact ? 10 : 12, display: 'flex', gap: 8, justifyContent: compact ? 'flex-end' : undefined }
419
+ style: { marginTop: 10, display: 'flex', justifyContent: 'flex-end' }
153
420
  },
154
421
  React.createElement('button', {
155
422
  type: 'button',
156
423
  onClick: onRefresh,
157
- style: {
158
- appearance: 'none', cursor: 'pointer', fontSize: compact ? 11 : 12,
159
- padding: compact ? '3px 8px' : '4px 10px', borderRadius: 6,
160
- border: '1px solid var(--dsw-alias-border-l2)',
161
- background: compact ? 'var(--dsw-alias-bg-layer-2)' : 'var(--dsw-alias-bg-layer-3)',
162
- color: compact ? 'var(--dsw-alias-label-secondary)' : 'var(--dsw-alias-label-primary)'
163
- }
424
+ className: 'cl-btn'
164
425
  }, L.refresh)
165
426
  ));
166
427
  }
428
+
167
429
  return React.createElement(React.Fragment, null, ...children);
168
430
  }
169
431
 
@@ -172,26 +434,47 @@ window.__ModuleLoader__.load({
172
434
  .then((r) => r.ok ? r.json() : null);
173
435
  }
174
436
 
175
- function LensTab({ ctx: _ctx, scope }) {
437
+ function clearLensFocus(sessionId) {
438
+ const q = sessionId ? '?sessionId=' + encodeURIComponent(sessionId) : '';
439
+ return fetch('/dsh-context-lens/clear-focus' + q, { method: 'POST', headers: { accept: 'application/json' } })
440
+ .then((r) => r.ok ? r.json() : null);
441
+ }
442
+
443
+ function LensTabInner({ ctx: _ctx, scope }) {
444
+ ensureCss();
176
445
  const [stats, setStats] = React.useState(null);
177
446
  const [history, setHistory] = React.useState([]);
447
+ const [focus, setFocus] = React.useState(null);
448
+ const [sessionId, setSessionId] = React.useState(null);
178
449
  const L = React.useMemo(() => getActiveLocale(_ctx), [_ctx]);
179
450
  const apply = (j) => {
180
451
  if (j && j.stats) setStats(j.stats);
181
452
  if (j && j.history) setHistory(j.history || []);
453
+ if (j && j.focus) setFocus(j.focus);
454
+ if (j && j.sessionId) setSessionId(j.sessionId);
182
455
  };
183
456
  const refresh = () => fetchLensStatus().then(apply).catch(() => {});
457
+ const handleClearFocus = () => clearLensFocus(sessionId).then(() => refresh()).catch(() => {});
184
458
  React.useEffect(() => { refresh(); }, []);
185
459
  return React.createElement('div', { style: { padding: 12 } },
186
460
  React.createElement(StatusPanel, {
187
- stats, history, labels: L, onRefresh: refresh, title: 'Context Lens', showBadge: false, compact: false
461
+ stats, history, focus, sessionId, labels: L, onRefresh: refresh, onClearFocus: handleClearFocus, title: 'Context Lens', showBadge: false, compact: false
188
462
  })
189
463
  );
190
464
  }
191
465
 
466
+ function LensTab(props) {
467
+ return React.createElement(ErrorBoundary, null,
468
+ React.createElement(LensTabInner, props)
469
+ );
470
+ }
471
+
192
472
  function HeaderChip({ ctx: _ctx }) {
473
+ ensureCss();
193
474
  const [stats, setStats] = React.useState(null);
194
475
  const [history, setHistory] = React.useState([]);
476
+ const [focus, setFocus] = React.useState(null);
477
+ const [sessionId, setSessionId] = React.useState(null);
195
478
  const [open, setOpen] = React.useState(false);
196
479
  const ref = React.useRef(null);
197
480
  const L = React.useMemo(() => getActiveLocale(_ctx), [_ctx]);
@@ -199,12 +482,15 @@ window.__ModuleLoader__.load({
199
482
  const fetchStatus = () => fetchLensStatus().then((j) => {
200
483
  if (j && j.stats) setStats(j.stats);
201
484
  if (j && j.history) setHistory(j.history || []);
485
+ if (j && j.focus) setFocus(j.focus);
486
+ if (j && j.sessionId) setSessionId(j.sessionId);
202
487
  }).catch(() => {});
203
488
 
489
+ const handleClearFocus = () => clearLensFocus(sessionId).then(() => fetchStatus()).catch(() => {});
490
+
204
491
  React.useEffect(() => {
205
492
  let alive = true;
206
493
  fetchStatus();
207
- // Adaptive polling: poll every 4s when popover is open, 15s when closed, pause when tab hidden
208
494
  const pollInterval = open ? 4000 : 15000;
209
495
  const id = setInterval(() => {
210
496
  if (!alive) return;
@@ -241,12 +527,12 @@ window.__ModuleLoader__.load({
241
527
  const warn = !!(stats && stats.lowBudget);
242
528
  const hasSavings = !!(stats && stats.savedTokens > 0);
243
529
  const label = hasSavings ? `Lens ${stats.savedPercent}%` : 'Lens';
244
- const color = warn ? '#d73a4a' : 'var(--dsw-alias-label-secondary)';
245
- const borderColor = warn ? '#d73a4a' : open ? 'var(--dsw-alias-border-l1)' : 'var(--dsw-alias-border-l2)';
530
+ const color = warn ? 'var(--dsw-alias-state-error-primary, #ef4444)' : 'var(--dsw-alias-label-secondary)';
531
+ const borderColor = warn ? 'var(--dsw-alias-state-error-primary, #ef4444)' : open ? 'var(--dsw-alias-border-l1)' : 'var(--dsw-alias-border-l2)';
246
532
 
247
533
  const popover = open ? React.createElement('div', {
248
534
  style: {
249
- position: 'absolute', top: 'calc(100% + 6px)', right: 0, width: 270,
535
+ position: 'absolute', top: 'calc(100% + 6px)', right: 0, width: 280,
250
536
  background: 'var(--dsw-alias-bg-layer-3)', border: '1px solid var(--dsw-alias-border-l2)',
251
537
  borderRadius: 10, boxShadow: '0 8px 24px rgba(0, 0, 0, 0.28)', padding: '12px 14px',
252
538
  zIndex: 1000, fontSize: 12, color: 'var(--dsw-alias-label-primary)', cursor: 'default', textAlign: 'left'
@@ -254,7 +540,7 @@ window.__ModuleLoader__.load({
254
540
  onClick: (e) => e.stopPropagation()
255
541
  },
256
542
  React.createElement(StatusPanel, {
257
- stats, history, labels: L, onRefresh: fetchStatus, title: 'Context Lens', showBadge: true, compact: true
543
+ stats, history, focus, sessionId, labels: L, onRefresh: fetchStatus, onClearFocus: handleClearFocus, title: 'Context Lens', showBadge: true, compact: true
258
544
  })
259
545
  ) : null;
260
546
 
@@ -272,16 +558,24 @@ window.__ModuleLoader__.load({
272
558
  },
273
559
  React.createElement('span', { style: { fontSize: 10, opacity: 0.8 } }, '◐'),
274
560
  label,
275
- warn ? React.createElement('span', { style: { color: '#d73a4a' } }, ' ⚠') : null
561
+ warn ? React.createElement('span', { style: { color: 'var(--dsw-alias-state-error-primary, #ef4444)' } }, ' ⚠') : null
276
562
  ),
277
563
  popover
278
564
  );
279
565
  }
280
566
 
281
- function PluginCard({ ctx: _ctx, t }) {
567
+ function PluginCardInner({ ctx: _ctx, t }) {
568
+ ensureCss();
282
569
  const [expanded, setExpanded] = React.useState(false);
283
- // hooks must be before any return — React 310
284
- const [draft, setDraft] = React.useState({ compressionMode: 'balanced', astSkeletonMaxDepth: 3, tokenSavingsTracking: true, budgetLimit: 100000, autoCollapse: true, autoCompressThreshold: 4000 });
570
+ const [draft, setDraft] = React.useState({
571
+ compressionMode: 'balanced',
572
+ astSkeletonMaxDepth: 3,
573
+ tokenSavingsTracking: true,
574
+ budgetLimit: 100000,
575
+ budgetAlertPercent: 90,
576
+ autoCollapse: true,
577
+ autoCompressThreshold: 4000
578
+ });
285
579
  const [status, setStatus] = React.useState('loading');
286
580
  const [saving, setSaving] = React.useState(false);
287
581
  const [saveErr, setSaveErr] = React.useState('');
@@ -293,8 +587,9 @@ window.__ModuleLoader__.load({
293
587
  const scopeRef = React.useRef(null);
294
588
  if (!scopeRef.current && _ctx) {
295
589
  try {
296
- if (_ctx.settingsScope) {
297
- scopeRef.current = _ctx.settingsScope.bind({ namespace: NS });
590
+ const s = (_ctx.get && _ctx.get('lanSettings')) || _ctx.settingsScope;
591
+ if (s && typeof s.bind === 'function') {
592
+ scopeRef.current = s.bind({ namespace: NS });
298
593
  }
299
594
  } catch (e) { scopeRef.current = null; }
300
595
  }
@@ -303,16 +598,17 @@ window.__ModuleLoader__.load({
303
598
  React.useEffect(() => {
304
599
  if (!scope) { setStatus('unavailable'); return; }
305
600
  let cancelled = false;
306
- (async () => {
601
+
602
+ const loadSnap = async () => {
307
603
  try {
308
- const snap = await scope.get();
604
+ const snap = (typeof scope.getSnapshot === 'function')
605
+ ? scope.getSnapshot()
606
+ : (typeof scope.get === 'function' ? await scope.get() : null);
309
607
  if (cancelled) return;
310
- // check snapshot status if available
311
608
  if (snap && typeof snap === 'object' && 'status' in snap) {
312
609
  if (snap.status === 'loading') { setStatus('loading'); return; }
313
610
  if (snap.status === 'unavailable') { setStatus('unavailable'); return; }
314
611
  }
315
- // snap is either values or wrapper with values
316
612
  const vals = snap && snap.values ? snap.values : snap;
317
613
  if (vals && typeof vals === 'object') {
318
614
  setDraft((d) => ({ ...d, ...vals }));
@@ -321,8 +617,23 @@ window.__ModuleLoader__.load({
321
617
  } catch (e) {
322
618
  if (!cancelled) setStatus('unavailable');
323
619
  }
324
- })();
325
- return () => { cancelled = true; };
620
+ };
621
+
622
+ loadSnap();
623
+
624
+ let unsub = null;
625
+ if (typeof scope.subscribe === 'function') {
626
+ try {
627
+ unsub = scope.subscribe(() => {
628
+ if (!cancelled) loadSnap();
629
+ });
630
+ } catch (_) {}
631
+ }
632
+
633
+ return () => {
634
+ cancelled = true;
635
+ if (typeof unsub === 'function') unsub();
636
+ };
326
637
  }, [scope]);
327
638
 
328
639
  React.useEffect(() => {
@@ -330,7 +641,6 @@ window.__ModuleLoader__.load({
330
641
  fetch('/dsh-context-lens/status').then((r) => r.ok ? r.json() : null).then((j) => {
331
642
  if (j && j.stats) {
332
643
  setStats(j.stats);
333
- // #41: warn instead of force-closing the card
334
644
  setBudgetWarn(!!(j.stats.lowBudget && draft.autoCollapse !== false));
335
645
  }
336
646
  }).catch(() => {});
@@ -387,106 +697,111 @@ window.__ModuleLoader__.load({
387
697
  }
388
698
  }
389
699
 
390
- // styles: ponytail minimal, theme vars only
391
- return React.createElement('li', { className: 'cl-card', style: { border: '1px solid var(--dsw-alias-border-l2)', background: 'var(--dsw-alias-bg-layer-3)', borderRadius: 12, listStyle: 'none' } },
700
+ return React.createElement('li', { className: 'cl-section-card' },
392
701
  React.createElement('button', {
393
- className: 'cl-head',
702
+ type: 'button',
703
+ className: 'cl-section-header',
394
704
  onClick: () => setExpanded(!expanded),
395
- 'aria-expanded': expanded,
396
- style: { appearance: 'none', width: '100%', font: 'inherit', color: 'inherit', textAlign: 'left', cursor: 'pointer', background: '0 0', border: 0, borderRadius: 12, display: 'flex', alignItems: 'center', gap: 12, padding: '14px 16px' }
705
+ 'aria-expanded': expanded
397
706
  },
398
- React.createElement('span', { style: { display: 'flex', flexDirection: 'column' } },
707
+ React.createElement('span', { style: { display: 'flex', flexDirection: 'column', gap: 2 } },
399
708
  React.createElement('span', { className: 'cl-title', style: { color: 'var(--dsw-alias-label-primary)', fontSize: 15, fontWeight: 600, lineHeight: 1.4 } }, tt('title')),
400
709
  React.createElement('span', { className: 'cl-sub', style: { color: 'var(--dsw-alias-label-secondary)', fontSize: 13 } }, tt('sub') + (stats ? ' · ' + tt('saved') + ' ' + (stats.savedTokens || 0) + ' ' + tt('tokens') + ' (' + (stats.savedPercent || 0) + '%)' : ''))
401
710
  ),
402
711
  React.createElement(Chevron, { open: expanded })
403
712
  ),
404
- expanded ? React.createElement('div', { className: 'cl-body', style: { borderTop: '1px solid var(--dsw-alias-border-l2)', margin: '0 16px', paddingBottom: 8 } },
405
- budgetWarn ? React.createElement('div', { style: { marginTop: 10, padding: '8px 10px', borderRadius: 8, background: 'rgba(215,58,74,0.12)', color: '#d73a4a', fontSize: 12 } }, '⚠ ' + tt('lowBudget')) : null,
713
+ expanded ? React.createElement('div', { className: 'cl-body', style: { borderTop: '1px solid var(--dsw-alias-border-l2)', paddingTop: 12 } },
714
+ budgetWarn ? React.createElement('div', { className: 'cl-banner-warn', style: { marginBottom: 12 } }, '⚠ ' + tt('lowBudget')) : null,
406
715
  status === 'loading' ? React.createElement('div', { style: { padding: 12, color: 'var(--dsw-alias-label-secondary)' } }, 'Loading…') :
407
716
  status === 'unavailable' ? React.createElement('div', { style: { padding: 12, color: 'var(--dsw-alias-label-secondary)' } }, 'Settings unavailable — plugin not registered on host yet') :
408
717
  React.createElement(React.Fragment, null,
409
- React.createElement('div', { className: 'cl-field', style: { display: 'flex', flexDirection: 'column', gap: 6, padding: '12px 0' } },
718
+ React.createElement('div', { className: 'cl-field', style: { display: 'flex', flexDirection: 'column', gap: 6, padding: '8px 0' } },
410
719
  React.createElement('label', { style: { fontSize: 13, color: 'var(--dsw-alias-label-secondary)' } }, tt('mode')),
411
720
  React.createElement('select', {
412
721
  className: 'cl-input',
413
722
  value: draft.compressionMode,
414
- onChange: (e) => setDraft((d) => ({ ...d, compressionMode: e.target.value })),
415
- style: { height: 34, border: '1px solid var(--dsw-alias-border-l2)', background: 'var(--dsw-alias-bg-layer-3)', color: 'var(--dsw-alias-label-primary)', borderRadius: 8, padding: '0 12px', fontSize: 13 }
723
+ onChange: (e) => setDraft((d) => ({ ...d, compressionMode: e.target.value }))
416
724
  },
417
725
  React.createElement('option', { value: 'raw' }, 'raw'),
418
726
  React.createElement('option', { value: 'balanced' }, 'balanced'),
419
727
  React.createElement('option', { value: 'aggressive' }, 'aggressive')
420
728
  )
421
729
  ),
422
- React.createElement('div', { className: 'cl-field', style: { display: 'flex', flexDirection: 'column', gap: 6, padding: '12px 0' } },
730
+ React.createElement('div', { className: 'cl-field', style: { display: 'flex', flexDirection: 'column', gap: 6, padding: '8px 0' } },
423
731
  React.createElement('label', { style: { fontSize: 13, color: 'var(--dsw-alias-label-secondary)' } }, tt('depth')),
424
732
  React.createElement('input', {
425
733
  className: 'cl-input',
426
734
  type: 'number', min: 1, max: 10,
427
735
  value: draft.astSkeletonMaxDepth,
428
- onChange: (e) => setDraft((d) => ({ ...d, astSkeletonMaxDepth: parseInt(e.target.value, 10) || 3 })),
429
- style: { height: 34, border: '1px solid var(--dsw-alias-border-l2)', background: 'var(--dsw-alias-bg-layer-3)', color: 'var(--dsw-alias-label-primary)', borderRadius: 8, padding: '0 12px', fontSize: 13 }
736
+ onChange: (e) => setDraft((d) => ({ ...d, astSkeletonMaxDepth: parseInt(e.target.value, 10) || 3 }))
430
737
  })
431
738
  ),
432
- React.createElement('div', { className: 'cl-field', style: { display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 8, padding: '12px 0' } },
739
+ React.createElement('div', { className: 'cl-field', style: { display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 8, padding: '8px 0' } },
433
740
  React.createElement('input', {
434
741
  type: 'checkbox',
435
742
  checked: !!draft.tokenSavingsTracking,
436
743
  onChange: (e) => setDraft((d) => ({ ...d, tokenSavingsTracking: e.target.checked })),
437
744
  id: 'cl-tracking'
438
745
  }),
439
- React.createElement('label', { htmlFor: 'cl-tracking', style: { fontSize: 13 } }, tt('tracking'))
746
+ React.createElement('label', { htmlFor: 'cl-tracking', style: { fontSize: 13, color: 'var(--dsw-alias-label-primary)' } }, tt('tracking'))
440
747
  ),
441
- React.createElement('div', { className: 'cl-field', style: { display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 8, padding: '12px 0' } },
748
+ React.createElement('div', { className: 'cl-field', style: { display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 8, padding: '8px 0' } },
442
749
  React.createElement('input', {
443
750
  type: 'checkbox',
444
751
  checked: !!draft.autoCollapse,
445
752
  onChange: (e) => setDraft((d) => ({ ...d, autoCollapse: e.target.checked })),
446
753
  id: 'cl-autocollapse'
447
754
  }),
448
- React.createElement('label', { htmlFor: 'cl-autocollapse', style: { fontSize: 13 } }, tt('autoCollapse'))
755
+ React.createElement('label', { htmlFor: 'cl-autocollapse', style: { fontSize: 13, color: 'var(--dsw-alias-label-primary)' } }, tt('autoCollapse'))
449
756
  ),
450
- React.createElement('div', { className: 'cl-field', style: { display: 'flex', flexDirection: 'column', gap: 6, padding: '12px 0' } },
757
+ React.createElement('div', { className: 'cl-field', style: { display: 'flex', flexDirection: 'column', gap: 6, padding: '8px 0' } },
451
758
  React.createElement('label', { style: { fontSize: 13, color: 'var(--dsw-alias-label-secondary)' } }, tt('budget') + ' (tokens)'),
452
759
  React.createElement('input', {
453
760
  className: 'cl-input', type: 'number', min: 1000, step: 1000,
454
761
  value: draft.budgetLimit,
455
- onChange: (e) => setDraft((d) => ({ ...d, budgetLimit: parseInt(e.target.value, 10) || 100000 })),
456
- style: { height: 34, border: '1px solid var(--dsw-alias-border-l2)', background: 'var(--dsw-alias-bg-layer-3)', color: 'var(--dsw-alias-label-primary)', borderRadius: 8, padding: '0 12px', fontSize: 13 }
762
+ onChange: (e) => setDraft((d) => ({ ...d, budgetLimit: parseInt(e.target.value, 10) || 100000 }))
763
+ })
764
+ ),
765
+ React.createElement('div', { className: 'cl-field', style: { display: 'flex', flexDirection: 'column', gap: 6, padding: '8px 0' } },
766
+ React.createElement('label', { style: { fontSize: 13, color: 'var(--dsw-alias-label-secondary)' } }, tt('budgetAlertPercent')),
767
+ React.createElement('input', {
768
+ className: 'cl-input', type: 'number', min: 50, max: 99, step: 1,
769
+ value: draft.budgetAlertPercent,
770
+ onChange: (e) => setDraft((d) => ({ ...d, budgetAlertPercent: parseInt(e.target.value, 10) || 90 }))
457
771
  })
458
772
  ),
459
- React.createElement('div', { className: 'cl-field', style: { display: 'flex', flexDirection: 'column', gap: 6, padding: '12px 0' } },
773
+ React.createElement('div', { className: 'cl-field', style: { display: 'flex', flexDirection: 'column', gap: 6, padding: '8px 0' } },
460
774
  React.createElement('label', { style: { fontSize: 13, color: 'var(--dsw-alias-label-secondary)' } }, tt('threshold')),
461
775
  React.createElement('input', {
462
776
  className: 'cl-input', type: 'number', min: 0, step: 500,
463
777
  value: draft.autoCompressThreshold,
464
- onChange: (e) => setDraft((d) => ({ ...d, autoCompressThreshold: parseInt(e.target.value, 10) || 0 })),
465
- style: { height: 34, border: '1px solid var(--dsw-alias-border-l2)', background: 'var(--dsw-alias-bg-layer-3)', color: 'var(--dsw-alias-label-primary)', borderRadius: 8, padding: '0 12px', fontSize: 13 }
778
+ onChange: (e) => setDraft((d) => ({ ...d, autoCompressThreshold: parseInt(e.target.value, 10) || 0 }))
466
779
  })
467
780
  ),
468
- React.createElement('div', { className: 'cl-field', style: { display: 'flex', flexDirection: 'column', gap: 6, padding: '12px 0' } },
781
+ React.createElement('div', { className: 'cl-field', style: { display: 'flex', flexDirection: 'column', gap: 6, padding: '8px 0' } },
469
782
  React.createElement('label', { style: { fontSize: 13, color: 'var(--dsw-alias-label-secondary)' } }, tt('preview') + ' (' + tt('previewApprox') + ')'),
470
783
  React.createElement('textarea', {
471
784
  value: previewIn,
472
785
  onChange: (e) => setPreviewIn(e.target.value),
473
786
  placeholder: tt('placeholder'),
474
787
  rows: 5,
475
- style: { border: '1px solid var(--dsw-alias-border-l2)', background: 'var(--dsw-alias-bg-layer-3)', color: 'var(--dsw-alias-label-primary)', borderRadius: 8, padding: 12, fontSize: 12, fontFamily: 'monospace' }
788
+ style: { border: '1px solid var(--dsw-alias-border-l2)', background: 'var(--dsw-alias-bg-layer-2)', color: 'var(--dsw-alias-label-primary)', borderRadius: 8, padding: 12, fontSize: 12, fontFamily: 'monospace' }
476
789
  }),
477
790
  React.createElement('button', {
791
+ type: 'button',
792
+ className: 'cl-btn cl-btn-primary',
478
793
  onClick: onPreview,
479
- style: { appearance: 'none', font: 'inherit', cursor: 'pointer', border: '1px solid transparent', borderRadius: 8, padding: '5px 14px', fontSize: 13, background: 'var(--dsw-alias-label-primary)', color: 'var(--dsw-alias-bg-layer-3)', alignSelf: 'flex-start' }
794
+ style: { alignSelf: 'flex-start', marginTop: 4 }
480
795
  }, tt('compress')),
481
- previewOut ? React.createElement('pre', { style: { whiteSpace: 'pre-wrap', fontSize: 12, background: 'var(--dsw-alias-bg-layer-2, var(--dsw-alias-bg-layer-3))', padding: 12, borderRadius: 8, maxHeight: 200, overflow: 'auto' } }, previewOut) : null
796
+ previewOut ? React.createElement('pre', { style: { whiteSpace: 'pre-wrap', fontSize: 12, background: 'var(--dsw-alias-bg-layer-2)', padding: 12, borderRadius: 8, maxHeight: 200, overflow: 'auto', border: '1px solid var(--dsw-alias-border-l2)', marginTop: 8 } }, previewOut) : null
482
797
  ),
483
- saveErr ? React.createElement('div', { style: { color: '#d73a4a', fontSize: 12, padding: '4px 0' } }, saveErr) : null,
484
- React.createElement('div', { className: 'cl-foot', style: { borderTop: '1px solid var(--dsw-alias-border-l2)', display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 8, padding: '12px 0 4px' } },
798
+ saveErr ? React.createElement('div', { style: { color: 'var(--dsw-alias-state-error-primary, #ef4444)', fontSize: 12, padding: '4px 0' } }, saveErr) : null,
799
+ React.createElement('div', { className: 'cl-foot', style: { borderTop: '1px solid var(--dsw-alias-border-l2)', display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 8, paddingTop: 12 } },
485
800
  React.createElement('button', {
486
- className: 'cl-save',
801
+ type: 'button',
802
+ className: 'cl-btn cl-btn-primary' + (saving ? ' cl-btn-disabled' : ''),
487
803
  onClick: onSave,
488
- disabled: saving,
489
- style: { appearance: 'none', font: 'inherit', cursor: 'pointer', border: '1px solid transparent', borderRadius: 8, padding: '5px 14px', fontSize: 13, background: 'var(--dsw-alias-label-primary)', color: 'var(--dsw-alias-bg-layer-3)', opacity: saving ? 0.6 : 1 }
804
+ disabled: saving
490
805
  }, saving ? tt('saving') : tt('ready'))
491
806
  )
492
807
  )
@@ -494,9 +809,15 @@ window.__ModuleLoader__.load({
494
809
  );
495
810
  }
496
811
 
812
+ function PluginCard(props) {
813
+ return React.createElement(ErrorBoundary, null,
814
+ React.createElement(PluginCardInner, props)
815
+ );
816
+ }
817
+
497
818
  module.exports.inject = ['slots', 'locale'];
498
819
  module.exports.apply = function apply(ctx) {
499
- try { ctx.locale.register(NS, { en, ru }); } catch (e) { console.warn('[dsh-context-lens] locale register failed', e && e.message || e); }
820
+ try { ctx.locale.register(NS, { en, zh }); } catch (e) { console.warn('[dsh-context-lens] locale register failed', e && e.message || e); }
500
821
  if (!ctx.slots) return;
501
822
  let registered = false;
502
823
  const doRegister = () => {
@@ -511,26 +832,22 @@ window.__ModuleLoader__.load({
511
832
  registered = true;
512
833
  return dispose;
513
834
  } catch (e) {
514
- // Do not swallow real registration errors — surface diagnostics
515
835
  console.error('[dsh-context-lens] settings.plugin.item register failed', e && e.stack || e);
516
836
  throw e;
517
837
  }
518
838
  };
519
- // Declaration-aware: wait for host to declare the slot (alpha2 SlotCore throws if not declared)
520
839
  if (typeof ctx.slots.inject === 'function') {
521
840
  try {
522
841
  const ok = ctx.slots.inject('settings.plugin.item', doRegister);
523
842
  if (!ok) console.warn('[dsh-context-lens] settings.plugin.item inject returned false — slot not declared yet');
524
843
  } catch (e) {
525
844
  console.error('[dsh-context-lens] settings.plugin.item inject failed', e && e.stack || e);
526
- // Fallback for older DSH without inject or if inject itself fails — surface error
527
845
  try { doRegister(); } catch (e2) { console.error('[dsh-context-lens] fallback register failed', e2 && e2.stack || e2); throw e2; }
528
846
  throw e;
529
847
  }
530
848
  } else {
531
849
  try { doRegister(); } catch (e) { console.error('[dsh-context-lens] direct register failed (no inject)', e && e.stack || e); throw e; }
532
850
  }
533
- // Native DSH Right Sidebar (#54, DSH 0.1.5-alpha.1+)
534
851
  if (typeof ctx.inject === "function") {
535
852
  try {
536
853
  ctx.inject(["sidebarRightTabs"], (sctx) => {
@@ -555,7 +872,6 @@ window.__ModuleLoader__.load({
555
872
  tabs.register(def);
556
873
  }
557
874
 
558
- // Register body in sidebar.right.pane.tab slot
559
875
  if (ctx.slots) {
560
876
  const registerPaneTab = () => {
561
877
  try {
@@ -584,12 +900,9 @@ window.__ModuleLoader__.load({
584
900
  console.warn("[dsh-context-lens] native sidebar registration failed", e && e.message || e);
585
901
  }
586
902
  });
587
- } catch (e) {
588
- // host without sidebarRightTabs declared or inject failure
589
- }
903
+ } catch (e) {}
590
904
  }
591
905
 
592
- // BetterSidebar tab (optional, for dsh-better-sidebar via ctx.inject)
593
906
  if (typeof ctx.inject === 'function') {
594
907
  try {
595
908
  ctx.inject(['betterSidebar'], (sctx) => {
@@ -612,11 +925,9 @@ window.__ModuleLoader__.load({
612
925
  console.warn('[dsh-context-lens] betterSidebar registerTab failed', e && e.message || e);
613
926
  }
614
927
  });
615
- } catch (e) {
616
- // host without betterSidebar declared or inject failure
617
- }
928
+ } catch (e) {}
618
929
  }
619
- // Header chip (#31) — telemetry and token guard in conversation session utilities slot
930
+
620
931
  if (ctx.slots) {
621
932
  const headerChipRegister = () => {
622
933
  try {