@goodandready/dsh-context-lens 0.1.8 → 0.1.10

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
@@ -127,6 +127,14 @@ dsh-context-lens:
127
127
 
128
128
  ## 📝 Version History
129
129
 
130
+ ### v0.1.10
131
+ * **Fix**: Register session header chip in `conversation.session.header.utilities` (`order: 7`).
132
+ * **Fix**: Ensure chip is always visible (`◐ Lens` on initial session, `◐ <N>%` when savings available, `⚠` on low budget).
133
+ * **Feature**: Interactive dropdown Popover on chip click: token savings breakdown, budget progress bar, recent operations, and refresh button.
134
+
135
+ ### v0.1.9
136
+ * **Fix**: Remove obsolete kernel modules from client injects for DSH 0.1.2-rc.1 compatibility.
137
+
130
138
  ### v0.1.8
131
139
  * **Fix**: Support both `text` and `log` parameter names in `context_lens_compress_log`.
132
140
  * **Fix**: Cross-platform path resolution in unit tests on Windows (`fileURLToPath`).
package/README.ru.md CHANGED
@@ -124,6 +124,14 @@ dsh-context-lens:
124
124
 
125
125
  ## 📝 История версий
126
126
 
127
+ ### v0.1.10
128
+ * **Fix**: Регистрация индикатора сессии в актуальном слоте ядра `conversation.session.header.utilities` (`order: 7`).
129
+ * **Fix**: Чип теперь отображается всегда (`◐ Lens` при отсутствии сжатий, `◐ <N>%` при наличии сэкономленных токенов, `⚠` при низком остатке бюджета).
130
+ * **Feature**: Интерактивный выпадающий Popover по клику на чип: детальные метрики токенов, шкала прогресса бюджета, последние 3 операции и кнопка обновления.
131
+
132
+ ### v0.1.9
133
+ * **Fix**: Удаление устаревших модулей ядра из инъекций клиента для совместимости с DSH 0.1.2-rc.1.
134
+
127
135
  ### v0.1.8
128
136
  * **Fix**: Поддержка как `text`, так и `log` в параметрах инструмента `context_lens_compress_log`.
129
137
  * **Fix**: Кроссплатформенное разрешение путей в тестах на Windows (`fileURLToPath`).
package/lib/client.js CHANGED
@@ -81,17 +81,163 @@ window.__ModuleLoader__.load({
81
81
  );
82
82
  }
83
83
 
84
- function HeaderBadge({ ctx: _ctx }) {
84
+ function HeaderChip({ ctx: _ctx }) {
85
85
  const [stats, setStats] = React.useState(null);
86
+ const [history, setHistory] = React.useState([]);
87
+ const [open, setOpen] = React.useState(false);
88
+ const ref = React.useRef(null);
89
+
90
+ const fetchStatus = () => {
91
+ fetch('/dsh-context-lens/status', { headers: { accept: 'application/json' } })
92
+ .then(r => r.ok ? r.json() : null)
93
+ .then(j => {
94
+ if (j && j.stats) setStats(j.stats);
95
+ if (j && j.history) setHistory(j.history);
96
+ })
97
+ .catch(() => {});
98
+ };
99
+
86
100
  React.useEffect(() => {
87
- const tick = () => fetch('/dsh-context-lens/status').then(r => r.ok ? r.json() : null).then(j => { if (j && j.stats) setStats(j.stats); }).catch(() => {});
88
- tick();
89
- const id = setInterval(tick, 5000);
90
- return () => clearInterval(id);
101
+ let alive = true;
102
+ fetchStatus();
103
+ const id = setInterval(() => { if (alive) fetchStatus(); }, 5000);
104
+ return () => { alive = false; clearInterval(id); };
91
105
  }, []);
92
- if (!stats || !stats.savedTokens) return null;
93
- const warn = !!stats.lowBudget;
94
- return React.createElement('span', { style: { fontSize: 11, padding: '2px 6px', borderRadius: 999, background: 'var(--dsw-alias-bg-layer-2)', color: warn ? '#d73a4a' : 'var(--dsw-alias-label-secondary)', border: '1px solid ' + (warn ? '#d73a4a' : 'var(--dsw-alias-border-l2)'), marginLeft: 8 } }, `Lens ${stats.savedPercent}%${warn ? ' ⚠' : ''}`);
106
+
107
+ React.useEffect(() => {
108
+ if (!open) return;
109
+ const onDocClick = (e) => {
110
+ if (ref.current && !ref.current.contains(e.target)) setOpen(false);
111
+ };
112
+ document.addEventListener('click', onDocClick);
113
+ return () => document.removeEventListener('click', onDocClick);
114
+ }, [open]);
115
+
116
+ const warn = !!(stats && stats.lowBudget);
117
+ const hasSavings = !!(stats && stats.savedTokens > 0);
118
+ const label = hasSavings ? `Lens ${stats.savedPercent}%` : 'Lens';
119
+ const color = warn ? '#d73a4a' : 'var(--dsw-alias-label-secondary)';
120
+ const borderColor = warn ? '#d73a4a' : open ? 'var(--dsw-alias-border-l1)' : 'var(--dsw-alias-border-l2)';
121
+
122
+ const popover = open ? React.createElement('div', {
123
+ style: {
124
+ position: 'absolute',
125
+ top: 'calc(100% + 6px)',
126
+ right: 0,
127
+ width: 270,
128
+ background: 'var(--dsw-alias-bg-layer-3)',
129
+ border: '1px solid var(--dsw-alias-border-l2)',
130
+ borderRadius: 10,
131
+ boxShadow: '0 8px 24px rgba(0, 0, 0, 0.28)',
132
+ padding: '12px 14px',
133
+ zIndex: 1000,
134
+ fontSize: 12,
135
+ color: 'var(--dsw-alias-label-primary)',
136
+ cursor: 'default',
137
+ textAlign: 'left'
138
+ },
139
+ onClick: (e) => e.stopPropagation()
140
+ },
141
+ React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 8 } },
142
+ React.createElement('div', { style: { fontWeight: 600, fontSize: 13, display: 'flex', alignItems: 'center', gap: 6 } },
143
+ React.createElement('span', { style: { opacity: 0.8 } }, '◐'),
144
+ 'Context Lens'
145
+ ),
146
+ React.createElement('span', {
147
+ style: {
148
+ fontSize: 11,
149
+ padding: '1px 6px',
150
+ borderRadius: 4,
151
+ background: warn ? 'rgba(215, 58, 74, 0.15)' : 'var(--dsw-alias-bg-layer-2)',
152
+ color: warn ? '#d73a4a' : 'var(--dsw-alias-label-tertiary)',
153
+ border: '1px solid ' + (warn ? 'rgba(215, 58, 74, 0.3)' : 'var(--dsw-alias-border-l2)')
154
+ }
155
+ }, warn ? 'Low Budget ⚠' : (stats ? 'Active' : 'Ready'))
156
+ ),
157
+ hasSavings
158
+ ? React.createElement('div', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)', marginBottom: 8 } },
159
+ `Saved ${stats.savedTokens} tokens (${stats.savedPercent}%) · ${stats.calls} ops`
160
+ )
161
+ : React.createElement('div', { style: { fontSize: 12, color: 'var(--dsw-alias-label-tertiary)', marginBottom: 8 } },
162
+ 'No compression yet · Monitoring active'
163
+ ),
164
+ stats && stats.budgetLimit ? React.createElement('div', { style: { marginTop: 6, marginBottom: 8 } },
165
+ React.createElement('div', {
166
+ style: {
167
+ display: 'flex',
168
+ justifyContent: 'space-between',
169
+ fontSize: 11,
170
+ color: warn ? '#d73a4a' : 'var(--dsw-alias-label-tertiary)',
171
+ marginBottom: 4
172
+ }
173
+ },
174
+ React.createElement('span', null, 'Budget'),
175
+ React.createElement('span', null, `${stats.budgetUsed || 0} / ${stats.budgetLimit} (${stats.budgetPercent || 0}%)`)
176
+ ),
177
+ React.createElement('div', { style: { height: 5, borderRadius: 3, background: 'var(--dsw-alias-bg-layer-1)', overflow: 'hidden' } },
178
+ React.createElement('div', {
179
+ style: {
180
+ height: '100%',
181
+ width: Math.min(100, stats.budgetPercent || 0) + '%',
182
+ background: warn ? '#d73a4a' : 'var(--dsw-alias-label-primary)',
183
+ transition: 'width .2s'
184
+ }
185
+ })
186
+ )
187
+ ) : null,
188
+ history && history.length ? React.createElement('div', { style: { marginTop: 8, paddingTop: 8, borderTop: '1px solid var(--dsw-alias-border-l3)' } },
189
+ React.createElement('div', { style: { fontSize: 11, color: 'var(--dsw-alias-label-tertiary)', marginBottom: 4 } }, 'Recent operations'),
190
+ ...history.slice(0, 3).map((h) => React.createElement('div', {
191
+ key: h.id,
192
+ style: { fontSize: 11, color: 'var(--dsw-alias-label-secondary)', padding: '2px 0', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }
193
+ }, `−${h.savedTokens} tk (${h.savedPercent}%) · ${(h.preview || '').slice(0, 28)}`))
194
+ ) : null,
195
+ React.createElement('div', { style: { marginTop: 10, display: 'flex', justifyContent: 'flex-end' } },
196
+ React.createElement('button', {
197
+ type: 'button',
198
+ onClick: fetchStatus,
199
+ style: {
200
+ appearance: 'none',
201
+ cursor: 'pointer',
202
+ fontSize: 11,
203
+ padding: '3px 8px',
204
+ borderRadius: 6,
205
+ border: '1px solid var(--dsw-alias-border-l2)',
206
+ background: 'var(--dsw-alias-bg-layer-2)',
207
+ color: 'var(--dsw-alias-label-secondary)'
208
+ }
209
+ }, 'Refresh')
210
+ )
211
+ ) : null;
212
+
213
+ return React.createElement('div', { ref, style: { position: 'relative', display: 'inline-flex', alignItems: 'center' } },
214
+ React.createElement('button', {
215
+ type: 'button',
216
+ title: hasSavings ? `Context Lens: Saved ${stats.savedTokens} tk (${stats.savedPercent}%)` : 'Context Lens: Active',
217
+ onClick: () => setOpen((v) => !v),
218
+ style: {
219
+ appearance: 'none',
220
+ font: 'inherit',
221
+ cursor: 'pointer',
222
+ display: 'inline-flex',
223
+ alignItems: 'center',
224
+ gap: 4,
225
+ padding: '2px 8px',
226
+ borderRadius: 999,
227
+ fontSize: 11,
228
+ lineHeight: '16px',
229
+ background: open ? 'var(--dsw-alias-bg-layer-3)' : 'var(--dsw-alias-bg-layer-2)',
230
+ color,
231
+ border: '1px solid ' + borderColor,
232
+ marginLeft: 6
233
+ }
234
+ },
235
+ React.createElement('span', { style: { fontSize: 10, opacity: 0.8 } }, '◐'),
236
+ label,
237
+ warn ? React.createElement('span', { style: { color: '#d73a4a' } }, ' ⚠') : null
238
+ ),
239
+ popover
240
+ );
95
241
  }
96
242
 
97
243
  function PluginCard({ ctx: _ctx, t }) {
@@ -308,24 +454,29 @@ window.__ModuleLoader__.load({
308
454
  console.warn('[dsh-context-lens] betterSidebar registerTab failed', e && e.message || e);
309
455
  }
310
456
  }
311
- // Header badge (#20) — compact savings in conversation header
457
+ // Header chip (#31) — telemetry and token guard in conversation session utilities slot
312
458
  if (ctx.slots) {
313
- const headerBadgeRegister = () => {
459
+ const headerChipRegister = () => {
314
460
  try {
315
461
  return ctx.slots.register({
316
- name: 'conversation.header',
317
- id: 'dsh-context-lens:header-badge',
318
- order: 50,
462
+ name: 'conversation.session.header.utilities',
463
+ id: 'dsh-context-lens-header-chip',
464
+ order: 7,
319
465
  inject: () => ({ ctx })
320
- }, HeaderBadge);
466
+ }, HeaderChip);
321
467
  } catch (e) {
322
- console.warn('[dsh-context-lens] header badge register failed', e && e.message || e);
468
+ console.warn('[dsh-context-lens] header chip register failed', e && e.message || e);
323
469
  }
324
470
  };
325
471
  if (typeof ctx.slots.inject === 'function') {
326
- try { ctx.slots.inject('conversation.header', headerBadgeRegister); } catch (e) { console.warn('[dsh-context-lens] header inject failed', e && e.message || e); try { headerBadgeRegister(); } catch (e2) {} }
472
+ try {
473
+ ctx.slots.inject('conversation.session.header.utilities', headerChipRegister);
474
+ } catch (e) {
475
+ console.warn('[dsh-context-lens] utilities inject failed', e && e.message || e);
476
+ try { headerChipRegister(); } catch (e2) {}
477
+ }
327
478
  } else {
328
- try { headerBadgeRegister(); } catch (e) {}
479
+ try { headerChipRegister(); } catch (e) {}
329
480
  }
330
481
  }
331
482
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-context-lens",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
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",
@@ -44,7 +44,6 @@
44
44
  "platform": "web",
45
45
  "inject": [
46
46
  "@deepseek-ai/dsh-client-locale",
47
- "@deepseek-ai/dsh-client-ui-slots",
48
47
  "@deepseek-ai/dsh-client-ui-settings"
49
48
  ]
50
49
  }