@goodandready/dsh-context-lens 0.1.14 → 0.1.16

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/README.md CHANGED
@@ -174,6 +174,13 @@ Audit fixes (#33–#47, #18):
174
174
  - **Test**: Added regression test suite simulating strict Cordis Context Proxy behavior.
175
175
 
176
176
 
177
+ ## Changed in v0.1.15
178
+
179
+ - **Feature (#54)**: Support native DSH right sidebar (`ctx.sidebarRightTabs` + `sidebar.right.pane.tab` slot) introduced in DSH 0.1.5-alpha.1.
180
+ - **Compatibility (#54)**: Preserve legacy `dsh-better-sidebar` integration with deterministic, non-conflicting IDs (`@goodandready/dsh-context-lens` vs `dsh-context-lens:tab`).
181
+ - **Resilience (#54)**: Clean boot and graceful fallback across all 4 layouts (Native only, Legacy only, Both active, Neither active).
182
+ - **Tests (#54)**: Added comprehensive test matrix in `test/sidebar-matrix-54.test.mjs`.
183
+
177
184
  ## Changed in v0.1.14
178
185
 
179
186
  - **Fix (#52)**: Multiline AST signature parsing in `skeletonizer.js` for TypeScript, JavaScript, Rust, and Go with complex parameter types and return annotations.
@@ -181,3 +188,11 @@ Audit fixes (#33–#47, #18):
181
188
  - **Quality & UX (#52)**: Dynamic RU/EN locale detection in `HeaderChip`, `LensTab`, and `StatusPanel` via `ctx.locale` for seamless UI integration.
182
189
  - **Perf & Stability (#52)**: Adaptive polling in `HeaderChip` pausing on hidden tabs (`visibilitychange`) and polling actively (4s) only when popover is open.
183
190
  - **Reliability (#52)**: Atomic settings save (`scope.patch` / `scope.setAll`) in `PluginCard` with graceful fallback.
191
+
192
+ ### UI & Styling System (v0.1.16+)
193
+
194
+ Context Lens shares its visual language and component architecture with `dsh-clinebot`:
195
+ - **Native Design System**: 100% theme integration using `--dsw-alias-*` tokens.
196
+ - **Resilient UI Surfaces**: Every component (`PluginCard`, `LensTab`, `StatusPanel`) is protected by an `ErrorBoundary` with instant retry capability.
197
+ - **Reactive Settings**: Live updates via `scope.subscribe()` and zero-delay reads via `scope.getSnapshot()`.
198
+ - **Adaptive Telemetry**: Popover and tab display metric cards for tokens saved, percentage reduced, and live budget utilization.
package/README.ru.md CHANGED
@@ -163,6 +163,13 @@ MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
163
163
  - **Тесты**: Добавлены регрессионные тесты со строгим Cordis Context Proxy.
164
164
 
165
165
 
166
+ ## Changed in v0.1.15
167
+
168
+ - **Функциональность (#54)**: Поддержка встроенной правой боковой панели DSH (`ctx.sidebarRightTabs` + слот `sidebar.right.pane.tab`) из DSH 0.1.5-alpha.1.
169
+ - **Совместимость (#54)**: Сохранена интеграция с легаси `dsh-better-sidebar` с детерминированными неконфликтующими идентификаторами (`@goodandready/dsh-context-lens` и `dsh-context-lens:tab`).
170
+ - **Отказоустойчивость (#54)**: Чистая инициализация во всех 4 конфигурациях (только Native, только Legacy, обе активны, обе отсутствуют).
171
+ - **Тесты (#54)**: Добавлена матрица тестов в `test/sidebar-matrix-54.test.mjs`.
172
+
166
173
  ## Changed in v0.1.14
167
174
 
168
175
  - **Исправление (#52)**: Поддержка многострочных сигнатур функций и методов в `skeletonizer.js` (TypeScript, JavaScript, Rust, Go) со сложными типами параметров и аннотациями возвращаемых значений.
@@ -170,3 +177,11 @@ MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
170
177
  - **Качество и локализация (#52)**: Динамическое переключение языка интерфейса (RU/EN) в чипе шапки (`HeaderChip`), боковой вкладке (`LensTab`) и панели статуса (`StatusPanel`) через `ctx.locale`.
171
178
  - **Производительность (#52)**: Адаптивный поллинг в `HeaderChip` с паузой при неактивной вкладке браузера (`document.visibilityState`) и учащённым опросом (4 сек) только при открытом поповере.
172
179
  - **Стабильность (#52)**: Атомарное сохранение конфигурации в карточке настроек (`scope.patch` / `scope.setAll`) без риска частичной записи.
180
+
181
+ ### Визуальный стиль и отказоустойчивость UI (v0.1.16+)
182
+
183
+ Context Lens приведен к единому стандарту оформления `dsh-clinebot`:
184
+ - **Нативные токены**: 100% совместимость со светлой и тёмной темами DSH через токены `--dsw-alias-*`.
185
+ - **Изоляция сбоев интерфейса**: Компоненты (`PluginCard`, `LensTab`, `StatusPanel`) обёрнуты в `ErrorBoundary` с кнопкой быстрого повтора («Retry»).
186
+ - **Реактивные настройки**: Автоматическое обновление через `scope.subscribe()` и мгновенное чтение через `scope.getSnapshot()`.
187
+ - **Наглядная телеметрия**: Компактные карточки метрик сэкономленных токенов, процента оптимизации и шкалы сессионного бюджета.
package/lib/client.js CHANGED
@@ -58,10 +58,215 @@ window.__ModuleLoader__.load({
58
58
  lowBudgetBadge: 'Мало бюджета ⚠'
59
59
  };
60
60
 
61
- // #45: preview uses POST /dsh-context-lens/compress-preview (server compressor)
61
+ function ensureCss() {
62
+ if (typeof document === 'undefined') return;
63
+ if (document.getElementById('dsh-context-lens-css')) return;
64
+ const style = document.createElement('style');
65
+ style.id = 'dsh-context-lens-css';
66
+ style.dataset.dshPlugin = NS;
67
+ style.textContent = `
68
+ .cl-section-card {
69
+ border: 1px solid var(--dsw-alias-border-l2);
70
+ background: var(--dsw-alias-bg-layer-3);
71
+ border-radius: 12px;
72
+ padding: 16px 18px;
73
+ margin-bottom: 12px;
74
+ display: flex;
75
+ flex-direction: column;
76
+ gap: 12px;
77
+ list-style: none;
78
+ }
79
+ .cl-section-header {
80
+ background: none;
81
+ border: none;
82
+ cursor: pointer;
83
+ display: flex;
84
+ align-items: center;
85
+ width: 100%;
86
+ padding: 0;
87
+ text-align: left;
88
+ }
89
+ .cl-badge {
90
+ font-size: 11px;
91
+ padding: 2px 8px;
92
+ border-radius: 999px;
93
+ border: 1px solid var(--dsw-alias-border-l2);
94
+ display: inline-flex;
95
+ align-items: center;
96
+ gap: 4px;
97
+ font-weight: 500;
98
+ }
99
+ .cl-badge-ok {
100
+ border-color: var(--dsw-alias-state-success-primary, #10b981);
101
+ color: var(--dsw-alias-state-success-primary, #10b981);
102
+ background: rgba(16, 185, 129, 0.08);
103
+ }
104
+ .cl-badge-warn {
105
+ border-color: var(--dsw-alias-state-error-primary, #ef4444);
106
+ color: var(--dsw-alias-state-error-primary, #ef4444);
107
+ background: rgba(239, 68, 68, 0.08);
108
+ }
109
+ .cl-stat-grid {
110
+ display: grid;
111
+ grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
112
+ gap: 10px;
113
+ margin-top: 4px;
114
+ }
115
+ .cl-stat-box {
116
+ padding: 10px 12px;
117
+ border: 1px solid var(--dsw-alias-border-l2);
118
+ border-radius: 8px;
119
+ background: var(--dsw-alias-bg-layer-2);
120
+ display: flex;
121
+ flex-direction: column;
122
+ gap: 3px;
123
+ }
124
+ .cl-stat-val {
125
+ font-size: 16px;
126
+ font-weight: 700;
127
+ color: var(--dsw-alias-label-primary);
128
+ }
129
+ .cl-stat-lbl {
130
+ font-size: 11px;
131
+ color: var(--dsw-alias-label-secondary);
132
+ }
133
+ .cl-bar-box {
134
+ display: flex;
135
+ flex-direction: column;
136
+ gap: 6px;
137
+ padding: 10px 12px;
138
+ border: 1px solid var(--dsw-alias-border-l2);
139
+ border-radius: 8px;
140
+ background: var(--dsw-alias-bg-layer-2);
141
+ }
142
+ .cl-bar-track {
143
+ width: 100%;
144
+ height: 8px;
145
+ border-radius: 999px;
146
+ background: var(--dsw-alias-bg-layer-1);
147
+ overflow: hidden;
148
+ border: 1px solid var(--dsw-alias-border-l2);
149
+ }
150
+ .cl-bar-fill {
151
+ height: 100%;
152
+ border-radius: 999px;
153
+ transition: width 0.3s;
154
+ }
155
+ .cl-input {
156
+ height: 34px;
157
+ border: 1px solid var(--dsw-alias-border-l2);
158
+ background: var(--dsw-alias-bg-layer-2);
159
+ color: var(--dsw-alias-label-primary);
160
+ border-radius: 8px;
161
+ padding: 0 12px;
162
+ font-size: 13px;
163
+ box-sizing: border-box;
164
+ }
165
+ .cl-input:focus {
166
+ outline: none;
167
+ border-color: var(--dsw-alias-state-brand-primary, #3b82f6);
168
+ }
169
+ .cl-btn {
170
+ appearance: none;
171
+ font: inherit;
172
+ cursor: pointer;
173
+ border: 1px solid var(--dsw-alias-border-l2);
174
+ border-radius: 8px;
175
+ padding: 6px 12px;
176
+ font-size: 12px;
177
+ background: var(--dsw-alias-bg-layer-2);
178
+ color: var(--dsw-alias-label-primary);
179
+ font-weight: 500;
180
+ display: inline-flex;
181
+ align-items: center;
182
+ justify-content: center;
183
+ gap: 6px;
184
+ transition: all 0.15s ease;
185
+ }
186
+ .cl-btn:hover:not(:disabled) {
187
+ background: var(--dsw-alias-bg-layer-4, var(--dsw-alias-bg-layer-2));
188
+ border-color: var(--dsw-alias-label-dimmed, var(--dsw-alias-border-l2));
189
+ }
190
+ .cl-btn-primary {
191
+ background: var(--dsw-alias-label-primary);
192
+ color: var(--dsw-alias-bg-layer-3);
193
+ border-color: transparent;
194
+ }
195
+ .cl-btn-primary:hover:not(:disabled) {
196
+ opacity: 0.88;
197
+ }
198
+ .cl-btn-disabled {
199
+ opacity: 0.5;
200
+ cursor: not-allowed;
201
+ }
202
+ .cl-banner-warn {
203
+ padding: 10px 14px;
204
+ border-radius: 8px;
205
+ background: rgba(239, 68, 68, 0.1);
206
+ border: 1px solid var(--dsw-alias-state-error-primary, #ef4444);
207
+ color: var(--dsw-alias-state-error-primary, #ef4444);
208
+ font-size: 12px;
209
+ display: flex;
210
+ align-items: center;
211
+ gap: 8px;
212
+ font-weight: 500;
213
+ }
214
+ .cl-hist-item {
215
+ font-size: 11px;
216
+ color: var(--dsw-alias-label-secondary);
217
+ padding: 4px 0;
218
+ border-bottom: 1px solid var(--dsw-alias-border-l3, var(--dsw-alias-border-l2));
219
+ }
220
+ .cl-hist-item:last-child {
221
+ border-bottom: none;
222
+ }
223
+ .cl-error-box {
224
+ padding: 12px;
225
+ border-radius: 8px;
226
+ background: rgba(239, 68, 68, 0.08);
227
+ border: 1px solid var(--dsw-alias-state-error-primary, #ef4444);
228
+ color: var(--dsw-alias-state-error-primary, #ef4444);
229
+ font-size: 12px;
230
+ }
231
+ `;
232
+ document.head.appendChild(style);
233
+ }
234
+
235
+ function createErrorBoundary() {
236
+ if (!React || typeof React.Component !== 'function') {
237
+ return function NoopBoundary(props) { return (props && props.children) || null; };
238
+ }
239
+ return class ErrorBoundary extends React.Component {
240
+ constructor(props) {
241
+ super(props);
242
+ this.state = { hasError: false, error: null };
243
+ }
244
+ static getDerivedStateFromError(error) {
245
+ return { hasError: true, error };
246
+ }
247
+ componentDidCatch(error, errorInfo) {
248
+ console.error('[dsh-context-lens] UI Error:', error, errorInfo);
249
+ }
250
+ render() {
251
+ if (this.state.hasError) {
252
+ return React.createElement('div', { className: 'cl-error-box' },
253
+ React.createElement('div', { style: { fontWeight: 600, marginBottom: 4 } }, '⚠ Context Lens UI Error'),
254
+ React.createElement('div', { style: { fontSize: 11, wordBreak: 'break-all' } }, String(this.state.error?.message || this.state.error)),
255
+ React.createElement('button', {
256
+ type: 'button',
257
+ className: 'cl-btn',
258
+ style: { marginTop: 8, fontSize: 11, padding: '3px 8px' },
259
+ onClick: () => this.setState({ hasError: false, error: null })
260
+ }, 'Retry')
261
+ );
262
+ }
263
+ return (this.props && this.props.children) || null;
264
+ }
265
+ };
266
+ }
267
+ const ErrorBoundary = createErrorBoundary();
62
268
 
63
- // #43: shared status UI for LensTab + HeaderChip popover
64
- function getActiveLocale(ctx) {
269
+ function getActiveLocale(ctx) {
65
270
  try {
66
271
  if (ctx && ctx.locale && typeof ctx.locale.getSnapshot === 'function') {
67
272
  const snap = ctx.locale.getSnapshot();
@@ -72,13 +277,14 @@ window.__ModuleLoader__.load({
72
277
  }
73
278
 
74
279
  function StatusPanel({ stats, history, labels, onRefresh, title, showBadge, compact }) {
280
+ ensureCss();
75
281
  const L = labels || en;
76
282
  const warn = !!(stats && stats.lowBudget);
77
283
  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;
284
+ const barColor = warn ? 'var(--dsw-alias-state-error-primary, #ef4444)' : 'var(--dsw-alias-state-success-primary, #10b981)';
285
+ const previewLen = compact ? 28 : 44;
81
286
  const children = [];
287
+
82
288
  if (title || showBadge) {
83
289
  children.push(React.createElement('div', {
84
290
  key: 'head',
@@ -92,78 +298,86 @@ window.__ModuleLoader__.load({
92
298
  : React.createElement('span'),
93
299
  showBadge
94
300
  ? 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
- }
301
+ className: 'cl-badge ' + (warn ? 'cl-badge-warn' : 'cl-badge-ok')
101
302
  }, warn ? L.lowBudgetBadge : (stats ? L.active : L.readyState))
102
303
  : null
103
304
  ));
104
305
  }
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
- );
306
+
307
+ if (stats) {
308
+ children.push(React.createElement('div', { key: 'stats', className: 'cl-stat-grid' },
309
+ React.createElement('div', { className: 'cl-stat-box' },
310
+ React.createElement('div', { className: 'cl-stat-val' }, stats.savedTokens ? `${stats.savedTokens}` : '0'),
311
+ React.createElement('div', { className: 'cl-stat-lbl' }, `${L.saved} ${L.tokens}`)
312
+ ),
313
+ React.createElement('div', { className: 'cl-stat-box' },
314
+ React.createElement('div', { className: 'cl-stat-val' }, `${stats.savedPercent || 0}%`),
315
+ React.createElement('div', { className: 'cl-stat-lbl' }, '% ' + L.saved)
316
+ ),
317
+ React.createElement('div', { className: 'cl-stat-box' },
318
+ React.createElement('div', { className: 'cl-stat-val' }, `${stats.calls || 0}`),
319
+ React.createElement('div', { className: 'cl-stat-lbl' }, L.ops)
320
+ )
321
+ ));
322
+ } else {
323
+ children.push(React.createElement('div', {
324
+ key: 'nodata',
325
+ style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)', padding: '6px 0' }
326
+ }, L.noData));
327
+ }
328
+
111
329
  if (stats && stats.lowBudget && !showBadge) {
112
- children.push(React.createElement('div', { key: 'warn', style: { marginTop: 6, fontSize: 11, color: '#d73a4a' } }, '⚠ ' + L.lowBudget));
330
+ children.push(React.createElement('div', {
331
+ key: 'warn',
332
+ className: 'cl-banner-warn',
333
+ style: { marginTop: 8 }
334
+ }, '⚠ ' + L.lowBudget));
113
335
  }
336
+
114
337
  if (stats && stats.budgetLimit) {
115
- children.push(React.createElement('div', { key: 'budget', style: { marginTop: compact ? 6 : 8, marginBottom: compact ? 8 : 0 } },
338
+ children.push(React.createElement('div', { key: 'budget', className: 'cl-bar-box', style: { marginTop: 8 } },
116
339
  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
- }
340
+ style: { display: 'flex', justifyContent: 'space-between', fontSize: 11, color: warn ? 'var(--dsw-alias-state-error-primary, #ef4444)' : 'var(--dsw-alias-label-secondary)' }
124
341
  },
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 ? ' ⚠' : ''}`
342
+ React.createElement('span', { style: { fontWeight: 500 } }, L.budget),
343
+ React.createElement('span', null, `${stats.budgetUsed || 0} / ${stats.budgetLimit} (${stats.budgetPercent || 0}%)`)
131
344
  ),
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' } })
345
+ React.createElement('div', { className: 'cl-bar-track' },
346
+ React.createElement('div', {
347
+ className: 'cl-bar-fill',
348
+ style: { width: Math.min(100, stats.budgetPercent || 0) + '%', background: barColor }
349
+ })
134
350
  )
135
351
  ));
136
352
  }
353
+
137
354
  if (history && history.length) {
138
355
  children.push(React.createElement('div', {
139
356
  key: 'hist',
140
- style: { marginTop: compact ? 8 : 10, paddingTop: compact ? 8 : 0, borderTop: compact ? '1px solid var(--dsw-alias-border-l3)' : undefined }
357
+ style: { marginTop: 10, paddingTop: 8, borderTop: '1px solid var(--dsw-alias-border-l2)' }
141
358
  },
142
- React.createElement('div', { style: { fontSize: 11, color: 'var(--dsw-alias-label-tertiary)', marginBottom: 4 } }, L.history),
359
+ React.createElement('div', { style: { fontSize: 11, fontWeight: 600, color: 'var(--dsw-alias-label-tertiary)', marginBottom: 4 } }, L.history),
143
360
  ...history.slice(0, 3).map((h) => React.createElement('div', {
144
361
  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 }
362
+ className: 'cl-hist-item',
363
+ style: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }
146
364
  }, `−${h.savedTokens} tk (${h.savedPercent}%) · ${(h.preview || '').slice(0, previewLen)}`))
147
365
  ));
148
366
  }
367
+
149
368
  if (onRefresh) {
150
369
  children.push(React.createElement('div', {
151
370
  key: 'act',
152
- style: { marginTop: compact ? 10 : 12, display: 'flex', gap: 8, justifyContent: compact ? 'flex-end' : undefined }
371
+ style: { marginTop: 10, display: 'flex', justifyContent: 'flex-end' }
153
372
  },
154
373
  React.createElement('button', {
155
374
  type: 'button',
156
375
  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
- }
376
+ className: 'cl-btn'
164
377
  }, L.refresh)
165
378
  ));
166
379
  }
380
+
167
381
  return React.createElement(React.Fragment, null, ...children);
168
382
  }
169
383
 
@@ -172,7 +386,8 @@ window.__ModuleLoader__.load({
172
386
  .then((r) => r.ok ? r.json() : null);
173
387
  }
174
388
 
175
- function LensTab({ ctx: _ctx, scope }) {
389
+ function LensTabInner({ ctx: _ctx, scope }) {
390
+ ensureCss();
176
391
  const [stats, setStats] = React.useState(null);
177
392
  const [history, setHistory] = React.useState([]);
178
393
  const L = React.useMemo(() => getActiveLocale(_ctx), [_ctx]);
@@ -189,7 +404,14 @@ window.__ModuleLoader__.load({
189
404
  );
190
405
  }
191
406
 
407
+ function LensTab(props) {
408
+ return React.createElement(ErrorBoundary, null,
409
+ React.createElement(LensTabInner, props)
410
+ );
411
+ }
412
+
192
413
  function HeaderChip({ ctx: _ctx }) {
414
+ ensureCss();
193
415
  const [stats, setStats] = React.useState(null);
194
416
  const [history, setHistory] = React.useState([]);
195
417
  const [open, setOpen] = React.useState(false);
@@ -204,7 +426,6 @@ window.__ModuleLoader__.load({
204
426
  React.useEffect(() => {
205
427
  let alive = true;
206
428
  fetchStatus();
207
- // Adaptive polling: poll every 4s when popover is open, 15s when closed, pause when tab hidden
208
429
  const pollInterval = open ? 4000 : 15000;
209
430
  const id = setInterval(() => {
210
431
  if (!alive) return;
@@ -241,12 +462,12 @@ window.__ModuleLoader__.load({
241
462
  const warn = !!(stats && stats.lowBudget);
242
463
  const hasSavings = !!(stats && stats.savedTokens > 0);
243
464
  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)';
465
+ const color = warn ? 'var(--dsw-alias-state-error-primary, #ef4444)' : 'var(--dsw-alias-label-secondary)';
466
+ const borderColor = warn ? 'var(--dsw-alias-state-error-primary, #ef4444)' : open ? 'var(--dsw-alias-border-l1)' : 'var(--dsw-alias-border-l2)';
246
467
 
247
468
  const popover = open ? React.createElement('div', {
248
469
  style: {
249
- position: 'absolute', top: 'calc(100% + 6px)', right: 0, width: 270,
470
+ position: 'absolute', top: 'calc(100% + 6px)', right: 0, width: 280,
250
471
  background: 'var(--dsw-alias-bg-layer-3)', border: '1px solid var(--dsw-alias-border-l2)',
251
472
  borderRadius: 10, boxShadow: '0 8px 24px rgba(0, 0, 0, 0.28)', padding: '12px 14px',
252
473
  zIndex: 1000, fontSize: 12, color: 'var(--dsw-alias-label-primary)', cursor: 'default', textAlign: 'left'
@@ -272,16 +493,23 @@ window.__ModuleLoader__.load({
272
493
  },
273
494
  React.createElement('span', { style: { fontSize: 10, opacity: 0.8 } }, '◐'),
274
495
  label,
275
- warn ? React.createElement('span', { style: { color: '#d73a4a' } }, ' ⚠') : null
496
+ warn ? React.createElement('span', { style: { color: 'var(--dsw-alias-state-error-primary, #ef4444)' } }, ' ⚠') : null
276
497
  ),
277
498
  popover
278
499
  );
279
500
  }
280
501
 
281
- function PluginCard({ ctx: _ctx, t }) {
502
+ function PluginCardInner({ ctx: _ctx, t }) {
503
+ ensureCss();
282
504
  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 });
505
+ const [draft, setDraft] = React.useState({
506
+ compressionMode: 'balanced',
507
+ astSkeletonMaxDepth: 3,
508
+ tokenSavingsTracking: true,
509
+ budgetLimit: 100000,
510
+ autoCollapse: true,
511
+ autoCompressThreshold: 4000
512
+ });
285
513
  const [status, setStatus] = React.useState('loading');
286
514
  const [saving, setSaving] = React.useState(false);
287
515
  const [saveErr, setSaveErr] = React.useState('');
@@ -293,8 +521,9 @@ window.__ModuleLoader__.load({
293
521
  const scopeRef = React.useRef(null);
294
522
  if (!scopeRef.current && _ctx) {
295
523
  try {
296
- if (_ctx.settingsScope) {
297
- scopeRef.current = _ctx.settingsScope.bind({ namespace: NS });
524
+ const s = (_ctx.get && _ctx.get('lanSettings')) || _ctx.settingsScope;
525
+ if (s && typeof s.bind === 'function') {
526
+ scopeRef.current = s.bind({ namespace: NS });
298
527
  }
299
528
  } catch (e) { scopeRef.current = null; }
300
529
  }
@@ -303,16 +532,17 @@ window.__ModuleLoader__.load({
303
532
  React.useEffect(() => {
304
533
  if (!scope) { setStatus('unavailable'); return; }
305
534
  let cancelled = false;
306
- (async () => {
535
+
536
+ const loadSnap = async () => {
307
537
  try {
308
- const snap = await scope.get();
538
+ const snap = (typeof scope.getSnapshot === 'function')
539
+ ? scope.getSnapshot()
540
+ : (typeof scope.get === 'function' ? await scope.get() : null);
309
541
  if (cancelled) return;
310
- // check snapshot status if available
311
542
  if (snap && typeof snap === 'object' && 'status' in snap) {
312
543
  if (snap.status === 'loading') { setStatus('loading'); return; }
313
544
  if (snap.status === 'unavailable') { setStatus('unavailable'); return; }
314
545
  }
315
- // snap is either values or wrapper with values
316
546
  const vals = snap && snap.values ? snap.values : snap;
317
547
  if (vals && typeof vals === 'object') {
318
548
  setDraft((d) => ({ ...d, ...vals }));
@@ -321,8 +551,23 @@ window.__ModuleLoader__.load({
321
551
  } catch (e) {
322
552
  if (!cancelled) setStatus('unavailable');
323
553
  }
324
- })();
325
- return () => { cancelled = true; };
554
+ };
555
+
556
+ loadSnap();
557
+
558
+ let unsub = null;
559
+ if (typeof scope.subscribe === 'function') {
560
+ try {
561
+ unsub = scope.subscribe(() => {
562
+ if (!cancelled) loadSnap();
563
+ });
564
+ } catch (_) {}
565
+ }
566
+
567
+ return () => {
568
+ cancelled = true;
569
+ if (typeof unsub === 'function') unsub();
570
+ };
326
571
  }, [scope]);
327
572
 
328
573
  React.useEffect(() => {
@@ -330,7 +575,6 @@ window.__ModuleLoader__.load({
330
575
  fetch('/dsh-context-lens/status').then((r) => r.ok ? r.json() : null).then((j) => {
331
576
  if (j && j.stats) {
332
577
  setStats(j.stats);
333
- // #41: warn instead of force-closing the card
334
578
  setBudgetWarn(!!(j.stats.lowBudget && draft.autoCollapse !== false));
335
579
  }
336
580
  }).catch(() => {});
@@ -387,106 +631,103 @@ window.__ModuleLoader__.load({
387
631
  }
388
632
  }
389
633
 
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' } },
634
+ return React.createElement('li', { className: 'cl-section-card' },
392
635
  React.createElement('button', {
393
- className: 'cl-head',
636
+ type: 'button',
637
+ className: 'cl-section-header',
394
638
  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' }
639
+ 'aria-expanded': expanded
397
640
  },
398
- React.createElement('span', { style: { display: 'flex', flexDirection: 'column' } },
641
+ React.createElement('span', { style: { display: 'flex', flexDirection: 'column', gap: 2 } },
399
642
  React.createElement('span', { className: 'cl-title', style: { color: 'var(--dsw-alias-label-primary)', fontSize: 15, fontWeight: 600, lineHeight: 1.4 } }, tt('title')),
400
643
  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
644
  ),
402
645
  React.createElement(Chevron, { open: expanded })
403
646
  ),
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,
647
+ expanded ? React.createElement('div', { className: 'cl-body', style: { borderTop: '1px solid var(--dsw-alias-border-l2)', paddingTop: 12 } },
648
+ budgetWarn ? React.createElement('div', { className: 'cl-banner-warn', style: { marginBottom: 12 } }, '⚠ ' + tt('lowBudget')) : null,
406
649
  status === 'loading' ? React.createElement('div', { style: { padding: 12, color: 'var(--dsw-alias-label-secondary)' } }, 'Loading…') :
407
650
  status === 'unavailable' ? React.createElement('div', { style: { padding: 12, color: 'var(--dsw-alias-label-secondary)' } }, 'Settings unavailable — plugin not registered on host yet') :
408
651
  React.createElement(React.Fragment, null,
409
- React.createElement('div', { className: 'cl-field', style: { display: 'flex', flexDirection: 'column', gap: 6, padding: '12px 0' } },
652
+ React.createElement('div', { className: 'cl-field', style: { display: 'flex', flexDirection: 'column', gap: 6, padding: '8px 0' } },
410
653
  React.createElement('label', { style: { fontSize: 13, color: 'var(--dsw-alias-label-secondary)' } }, tt('mode')),
411
654
  React.createElement('select', {
412
655
  className: 'cl-input',
413
656
  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 }
657
+ onChange: (e) => setDraft((d) => ({ ...d, compressionMode: e.target.value }))
416
658
  },
417
659
  React.createElement('option', { value: 'raw' }, 'raw'),
418
660
  React.createElement('option', { value: 'balanced' }, 'balanced'),
419
661
  React.createElement('option', { value: 'aggressive' }, 'aggressive')
420
662
  )
421
663
  ),
422
- React.createElement('div', { className: 'cl-field', style: { display: 'flex', flexDirection: 'column', gap: 6, padding: '12px 0' } },
664
+ React.createElement('div', { className: 'cl-field', style: { display: 'flex', flexDirection: 'column', gap: 6, padding: '8px 0' } },
423
665
  React.createElement('label', { style: { fontSize: 13, color: 'var(--dsw-alias-label-secondary)' } }, tt('depth')),
424
666
  React.createElement('input', {
425
667
  className: 'cl-input',
426
668
  type: 'number', min: 1, max: 10,
427
669
  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 }
670
+ onChange: (e) => setDraft((d) => ({ ...d, astSkeletonMaxDepth: parseInt(e.target.value, 10) || 3 }))
430
671
  })
431
672
  ),
432
- React.createElement('div', { className: 'cl-field', style: { display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 8, padding: '12px 0' } },
673
+ React.createElement('div', { className: 'cl-field', style: { display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 8, padding: '8px 0' } },
433
674
  React.createElement('input', {
434
675
  type: 'checkbox',
435
676
  checked: !!draft.tokenSavingsTracking,
436
677
  onChange: (e) => setDraft((d) => ({ ...d, tokenSavingsTracking: e.target.checked })),
437
678
  id: 'cl-tracking'
438
679
  }),
439
- React.createElement('label', { htmlFor: 'cl-tracking', style: { fontSize: 13 } }, tt('tracking'))
680
+ React.createElement('label', { htmlFor: 'cl-tracking', style: { fontSize: 13, color: 'var(--dsw-alias-label-primary)' } }, tt('tracking'))
440
681
  ),
441
- React.createElement('div', { className: 'cl-field', style: { display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 8, padding: '12px 0' } },
682
+ React.createElement('div', { className: 'cl-field', style: { display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 8, padding: '8px 0' } },
442
683
  React.createElement('input', {
443
684
  type: 'checkbox',
444
685
  checked: !!draft.autoCollapse,
445
686
  onChange: (e) => setDraft((d) => ({ ...d, autoCollapse: e.target.checked })),
446
687
  id: 'cl-autocollapse'
447
688
  }),
448
- React.createElement('label', { htmlFor: 'cl-autocollapse', style: { fontSize: 13 } }, tt('autoCollapse'))
689
+ React.createElement('label', { htmlFor: 'cl-autocollapse', style: { fontSize: 13, color: 'var(--dsw-alias-label-primary)' } }, tt('autoCollapse'))
449
690
  ),
450
- React.createElement('div', { className: 'cl-field', style: { display: 'flex', flexDirection: 'column', gap: 6, padding: '12px 0' } },
691
+ React.createElement('div', { className: 'cl-field', style: { display: 'flex', flexDirection: 'column', gap: 6, padding: '8px 0' } },
451
692
  React.createElement('label', { style: { fontSize: 13, color: 'var(--dsw-alias-label-secondary)' } }, tt('budget') + ' (tokens)'),
452
693
  React.createElement('input', {
453
694
  className: 'cl-input', type: 'number', min: 1000, step: 1000,
454
695
  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 }
696
+ onChange: (e) => setDraft((d) => ({ ...d, budgetLimit: parseInt(e.target.value, 10) || 100000 }))
457
697
  })
458
698
  ),
459
- React.createElement('div', { className: 'cl-field', style: { display: 'flex', flexDirection: 'column', gap: 6, padding: '12px 0' } },
699
+ React.createElement('div', { className: 'cl-field', style: { display: 'flex', flexDirection: 'column', gap: 6, padding: '8px 0' } },
460
700
  React.createElement('label', { style: { fontSize: 13, color: 'var(--dsw-alias-label-secondary)' } }, tt('threshold')),
461
701
  React.createElement('input', {
462
702
  className: 'cl-input', type: 'number', min: 0, step: 500,
463
703
  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 }
704
+ onChange: (e) => setDraft((d) => ({ ...d, autoCompressThreshold: parseInt(e.target.value, 10) || 0 }))
466
705
  })
467
706
  ),
468
- React.createElement('div', { className: 'cl-field', style: { display: 'flex', flexDirection: 'column', gap: 6, padding: '12px 0' } },
707
+ React.createElement('div', { className: 'cl-field', style: { display: 'flex', flexDirection: 'column', gap: 6, padding: '8px 0' } },
469
708
  React.createElement('label', { style: { fontSize: 13, color: 'var(--dsw-alias-label-secondary)' } }, tt('preview') + ' (' + tt('previewApprox') + ')'),
470
709
  React.createElement('textarea', {
471
710
  value: previewIn,
472
711
  onChange: (e) => setPreviewIn(e.target.value),
473
712
  placeholder: tt('placeholder'),
474
713
  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' }
714
+ 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
715
  }),
477
716
  React.createElement('button', {
717
+ type: 'button',
718
+ className: 'cl-btn cl-btn-primary',
478
719
  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' }
720
+ style: { alignSelf: 'flex-start', marginTop: 4 }
480
721
  }, 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
722
+ 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
723
  ),
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' } },
724
+ saveErr ? React.createElement('div', { style: { color: 'var(--dsw-alias-state-error-primary, #ef4444)', fontSize: 12, padding: '4px 0' } }, saveErr) : null,
725
+ 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
726
  React.createElement('button', {
486
- className: 'cl-save',
727
+ type: 'button',
728
+ className: 'cl-btn cl-btn-primary' + (saving ? ' cl-btn-disabled' : ''),
487
729
  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 }
730
+ disabled: saving
490
731
  }, saving ? tt('saving') : tt('ready'))
491
732
  )
492
733
  )
@@ -494,6 +735,12 @@ window.__ModuleLoader__.load({
494
735
  );
495
736
  }
496
737
 
738
+ function PluginCard(props) {
739
+ return React.createElement(ErrorBoundary, null,
740
+ React.createElement(PluginCardInner, props)
741
+ );
742
+ }
743
+
497
744
  module.exports.inject = ['slots', 'locale'];
498
745
  module.exports.apply = function apply(ctx) {
499
746
  try { ctx.locale.register(NS, { en, ru }); } catch (e) { console.warn('[dsh-context-lens] locale register failed', e && e.message || e); }
@@ -511,26 +758,77 @@ window.__ModuleLoader__.load({
511
758
  registered = true;
512
759
  return dispose;
513
760
  } catch (e) {
514
- // Do not swallow real registration errors — surface diagnostics
515
761
  console.error('[dsh-context-lens] settings.plugin.item register failed', e && e.stack || e);
516
762
  throw e;
517
763
  }
518
764
  };
519
- // Declaration-aware: wait for host to declare the slot (alpha2 SlotCore throws if not declared)
520
765
  if (typeof ctx.slots.inject === 'function') {
521
766
  try {
522
767
  const ok = ctx.slots.inject('settings.plugin.item', doRegister);
523
768
  if (!ok) console.warn('[dsh-context-lens] settings.plugin.item inject returned false — slot not declared yet');
524
769
  } catch (e) {
525
770
  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
771
  try { doRegister(); } catch (e2) { console.error('[dsh-context-lens] fallback register failed', e2 && e2.stack || e2); throw e2; }
528
772
  throw e;
529
773
  }
530
774
  } else {
531
775
  try { doRegister(); } catch (e) { console.error('[dsh-context-lens] direct register failed (no inject)', e && e.stack || e); throw e; }
532
776
  }
533
- // BetterSidebar tab (optional, for dsh-better-sidebar via ctx.inject)
777
+ if (typeof ctx.inject === "function") {
778
+ try {
779
+ ctx.inject(["sidebarRightTabs"], (sctx) => {
780
+ const tabs = sctx && sctx.sidebarRightTabs;
781
+ if (!tabs || typeof tabs.register !== "function") return;
782
+ try {
783
+ const def = {
784
+ id: "@goodandready/dsh-context-lens",
785
+ kind: "context-lens",
786
+ priority: "extension",
787
+ title: () => "Lens",
788
+ guide: [{
789
+ order: 50,
790
+ title: () => "Context Lens",
791
+ description: () => "AST compression, log filtering, token budget",
792
+ icon: () => React.createElement("span", null, "◐")
793
+ }]
794
+ };
795
+ if (typeof sctx.effect === "function") {
796
+ sctx.effect(() => tabs.register(def));
797
+ } else {
798
+ tabs.register(def);
799
+ }
800
+
801
+ if (ctx.slots) {
802
+ const registerPaneTab = () => {
803
+ try {
804
+ return ctx.slots.register({
805
+ name: "sidebar.right.pane.tab",
806
+ key: "@goodandready/dsh-context-lens",
807
+ locale: NS,
808
+ inject: () => ({ ctx })
809
+ }, LensTab);
810
+ } catch (e) {
811
+ console.warn("[dsh-context-lens] native sidebar pane tab register failed", e && e.message || e);
812
+ }
813
+ };
814
+ if (typeof ctx.slots.inject === "function") {
815
+ try {
816
+ const ok = ctx.slots.inject("sidebar.right.pane.tab", registerPaneTab);
817
+ if (!ok) console.warn("[dsh-context-lens] sidebar.right.pane.tab inject returned false");
818
+ } catch (e) {
819
+ try { registerPaneTab(); } catch (e2) {}
820
+ }
821
+ } else {
822
+ try { registerPaneTab(); } catch (e) {}
823
+ }
824
+ }
825
+ } catch (e) {
826
+ console.warn("[dsh-context-lens] native sidebar registration failed", e && e.message || e);
827
+ }
828
+ });
829
+ } catch (e) {}
830
+ }
831
+
534
832
  if (typeof ctx.inject === 'function') {
535
833
  try {
536
834
  ctx.inject(['betterSidebar'], (sctx) => {
@@ -553,11 +851,9 @@ window.__ModuleLoader__.load({
553
851
  console.warn('[dsh-context-lens] betterSidebar registerTab failed', e && e.message || e);
554
852
  }
555
853
  });
556
- } catch (e) {
557
- // host without betterSidebar declared or inject failure
558
- }
854
+ } catch (e) {}
559
855
  }
560
- // Header chip (#31) — telemetry and token guard in conversation session utilities slot
856
+
561
857
  if (ctx.slots) {
562
858
  const headerChipRegister = () => {
563
859
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-context-lens",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
4
4
  "description": "DSH plugin for AST context compression, test log filtering, and token budget guard",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",