@goodandready/dsh-context-lens 0.1.4 → 0.1.6

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
@@ -48,6 +48,33 @@ window.__ModuleLoader__.load({
48
48
  return lines.filter((_, i) => keep[i]).slice(0, 200).join('\n');
49
49
  }
50
50
 
51
+ function LensTab({ ctx: _ctx, scope }) {
52
+ const [stats, setStats] = React.useState(null);
53
+ React.useEffect(() => {
54
+ fetch('/dsh-context-lens/status').then(r => r.ok ? r.json() : null).then(j => { if (j && j.stats) setStats(j.stats); }).catch(() => {});
55
+ }, []);
56
+ return React.createElement('div', { style: { padding: 12 } },
57
+ React.createElement('div', { style: { fontSize: 13, fontWeight: 600, marginBottom: 8, color: 'var(--dsw-alias-label-primary)' } }, 'Context Lens'),
58
+ stats ? React.createElement('div', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)' } }, `Saved ${stats.savedTokens} tokens (${stats.savedPercent}%) · ${stats.calls} ops`) : React.createElement('div', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)' } }, 'No data yet'),
59
+ React.createElement('div', { style: { marginTop: 12, display: 'flex', gap: 8 } },
60
+ React.createElement('button', { onClick: () => fetch('/dsh-context-lens/status').then(r=>r.json()).then(j=> setStats(j.stats)), style: { fontSize: 12, padding: '4px 8px', borderRadius: 6, border: '1px solid var(--dsw-alias-border-l2)', background: 'var(--dsw-alias-bg-layer-3)', cursor: 'pointer' } }, 'Refresh'),
61
+ React.createElement('button', { onClick: () => { try { _ctx.betterSidebar.openTab({ type: 'dsh-context-lens:tab', title: 'Lens' }) } catch(e){} }, style: { fontSize: 12, padding: '4px 8px', borderRadius: 6, border: '1px solid var(--dsw-alias-border-l2)', background: 'transparent', cursor: 'pointer' } }, 'Settings')
62
+ )
63
+ );
64
+ }
65
+
66
+ function HeaderBadge({ ctx: _ctx }) {
67
+ const [stats, setStats] = React.useState(null);
68
+ React.useEffect(() => {
69
+ const tick = () => fetch('/dsh-context-lens/status').then(r => r.ok ? r.json() : null).then(j => { if (j && j.stats) setStats(j.stats); }).catch(() => {});
70
+ tick();
71
+ const id = setInterval(tick, 5000);
72
+ return () => clearInterval(id);
73
+ }, []);
74
+ if (!stats || !stats.savedTokens) return null;
75
+ return React.createElement('span', { style: { fontSize: 11, padding: '2px 6px', borderRadius: 999, background: 'var(--dsw-alias-bg-layer-2)', color: 'var(--dsw-alias-label-secondary)', border: '1px solid var(--dsw-alias-border-l2)', marginLeft: 8 } }, `Lens ${stats.savedPercent}%`);
76
+ }
77
+
51
78
  function PluginCard({ ctx: _ctx, t }) {
52
79
  const [expanded, setExpanded] = React.useState(false);
53
80
  // hooks must be before any return — React 310
@@ -206,16 +233,75 @@ window.__ModuleLoader__.load({
206
233
  );
207
234
  }
208
235
 
209
- module.exports.inject = ['slots', 'locale'];
236
+ module.exports.inject = ['slots', 'locale', 'betterSidebar'];
210
237
  module.exports.apply = function apply(ctx) {
211
- try { ctx.locale.register(NS, { en, ru }); } catch (e) {}
238
+ try { ctx.locale.register(NS, { en, ru }); } catch (e) { console.warn('[dsh-context-lens] locale register failed', e && e.message || e); }
239
+ if (!ctx.slots) return;
240
+ let registered = false;
241
+ const doRegister = () => {
242
+ if (registered) return;
243
+ try {
244
+ const dispose = ctx.slots.register({
245
+ name: 'settings.plugin.item',
246
+ key: NS,
247
+ locale: NS,
248
+ inject: () => ({ ctx })
249
+ }, PluginCard);
250
+ registered = true;
251
+ return dispose;
252
+ } catch (e) {
253
+ // Do not swallow real registration errors — surface diagnostics
254
+ console.error('[dsh-context-lens] settings.plugin.item register failed', e && e.stack || e);
255
+ throw e;
256
+ }
257
+ };
258
+ // Declaration-aware: wait for host to declare the slot (alpha2 SlotCore throws if not declared)
259
+ if (typeof ctx.slots.inject === 'function') {
260
+ try {
261
+ const ok = ctx.slots.inject('settings.plugin.item', doRegister);
262
+ if (!ok) console.warn('[dsh-context-lens] settings.plugin.item inject returned false — slot not declared yet');
263
+ } catch (e) {
264
+ console.error('[dsh-context-lens] settings.plugin.item inject failed', e && e.stack || e);
265
+ // Fallback for older DSH without inject or if inject itself fails — surface error
266
+ try { doRegister(); } catch (e2) { console.error('[dsh-context-lens] fallback register failed', e2 && e2.stack || e2); throw e2; }
267
+ throw e;
268
+ }
269
+ } else {
270
+ try { doRegister(); } catch (e) { console.error('[dsh-context-lens] direct register failed (no inject)', e && e.stack || e); throw e; }
271
+ }
272
+ // BetterSidebar tab (optional, for dsh-better-sidebar)
273
+ if (ctx.betterSidebar && typeof ctx.betterSidebar.registerTab === 'function') {
274
+ try {
275
+ ctx.effect(() => ctx.betterSidebar.registerTab({
276
+ id: 'dsh-context-lens:tab',
277
+ title: () => 'Lens',
278
+ icon: () => React.createElement('span', null, '◐'),
279
+ order: 50,
280
+ component: ({ scope }) => React.createElement(LensTab, { ctx, scope })
281
+ }));
282
+ } catch (e) {
283
+ console.warn('[dsh-context-lens] betterSidebar registerTab failed', e && e.message || e);
284
+ }
285
+ }
286
+ // Header badge (#20) — compact savings in conversation header
212
287
  if (ctx.slots) {
213
- ctx.slots.register({
214
- name: 'settings.plugin.item',
215
- key: NS,
216
- locale: NS,
217
- inject: () => ({ ctx })
218
- }, PluginCard);
288
+ const headerBadgeRegister = () => {
289
+ try {
290
+ return ctx.slots.register({
291
+ name: 'conversation.header',
292
+ id: 'dsh-context-lens:header-badge',
293
+ order: 50,
294
+ inject: () => ({ ctx })
295
+ }, HeaderBadge);
296
+ } catch (e) {
297
+ console.warn('[dsh-context-lens] header badge register failed', e && e.message || e);
298
+ }
299
+ };
300
+ if (typeof ctx.slots.inject === 'function') {
301
+ 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) {} }
302
+ } else {
303
+ try { headerBadgeRegister(); } catch (e) {}
304
+ }
219
305
  }
220
306
  };
221
307
  return module.exports;
package/lib/index.js CHANGED
@@ -9,7 +9,8 @@ export const inject = ['tools', 'settings', 'webServer'];
9
9
  export const Config = z.object({
10
10
  compressionMode: z.string().default('balanced').description('Log compression aggressiveness (raw/balanced/aggressive)'),
11
11
  astSkeletonMaxDepth: z.number().default(3).description('Max depth for AST skeleton generation'),
12
- tokenSavingsTracking: z.boolean().default(true).description('Track and display token budget savings')
12
+ tokenSavingsTracking: z.boolean().default(true).description('Track and display token budget savings'),
13
+ autoCompressThreshold: z.number().default(4000).description('Auto-compress threshold in chars (0 to disable)')
13
14
  });
14
15
 
15
16
  const NS = '@goodandready/dsh-context-lens';
@@ -20,6 +21,11 @@ let focusState = { paths: [], updatedAt: null };
20
21
  const OUTPUT_SCHEMA = { type: 'object', properties: { success: { type: 'boolean' } }, additionalProperties: true };
21
22
  const renderOutput = (_args, result) => JSON.stringify(result, null, 2);
22
23
 
24
+ function shouldAutoCompress(text, threshold) {
25
+ if (!threshold || threshold <= 0) return false;
26
+ return (text || '').length > threshold || (text || '').split('\n').length > 100;
27
+ }
28
+
23
29
  export function apply(ctx, config) {
24
30
  let getConfig = () => config;
25
31
 
@@ -65,7 +71,8 @@ export function apply(ctx, config) {
65
71
  properties: {
66
72
  text: { type: 'string', description: 'Raw log text to compress' },
67
73
  mode: { type: 'string', enum: ['raw', 'balanced', 'aggressive'], description: 'Compression aggressiveness' },
68
- maxLines: { type: 'number', description: 'Max output lines' }
74
+ maxLines: { type: 'number', description: 'Max output lines' },
75
+ auto: { type: 'boolean', description: 'Auto-compress if large (uses threshold)' }
69
76
  },
70
77
  required: ['text']
71
78
  },
@@ -74,9 +81,13 @@ export function apply(ctx, config) {
74
81
  const cfg = getConfig();
75
82
  const mode = params.mode || cfg.compressionMode || 'balanced';
76
83
  const maxLines = params.maxLines || 400;
77
- const res = compressLog(params.text, { mode, maxLines });
84
+ // Auto-compress check (#15)
85
+ const threshold = cfg.autoCompressThreshold ?? 4000;
86
+ const useAuto = params.auto !== false && shouldAutoCompress(params.text, threshold);
87
+ const effectiveMode = useAuto ? mode : mode;
88
+ const res = compressLog(params.text, { mode: effectiveMode, maxLines });
78
89
  maybeTrack(params.text, res.compressed);
79
- return { success: true, mode, ...res };
90
+ return { success: true, mode: effectiveMode, autoCompressed: useAuto, ...res };
80
91
  }
81
92
  });
82
93
 
@@ -88,7 +99,8 @@ export function apply(ctx, config) {
88
99
  properties: {
89
100
  code: { type: 'string', description: 'Source code to skeletonize' },
90
101
  language: { type: 'string', enum: ['js', 'ts', 'py', 'python', 'go'], description: 'Language hint' },
91
- maxDepth: { type: 'number', description: 'Max depth' }
102
+ maxDepth: { type: 'number', description: 'Max depth' },
103
+ filePath: { type: 'string', description: 'File path to check focus (if in focus, returns full code)' }
92
104
  },
93
105
  required: ['code']
94
106
  },
@@ -96,9 +108,15 @@ export function apply(ctx, config) {
96
108
  execute: async (params) => {
97
109
  const cfg = getConfig();
98
110
  const maxDepth = params.maxDepth ?? cfg.astSkeletonMaxDepth ?? 3;
111
+ // Focus mode (#18): if file is in focus, return full code, else skeleton
112
+ const isFocused = params.filePath && focusState.paths.length > 0 ? focusState.paths.some(p => params.filePath.includes(p) || p.includes(params.filePath)) : false;
113
+ if (isFocused) {
114
+ maybeTrack(params.code, params.code);
115
+ return { success: true, skeleton: params.code, originalTokens: estLog(params.code), skeletonTokens: estLog(params.code), maxDepth, focused: true, hint: 'File is in focus, returned full code' };
116
+ }
99
117
  const skeleton = skeletonize(params.code, { maxDepth, language: params.language });
100
118
  maybeTrack(params.code, skeleton);
101
- return { success: true, skeleton, originalTokens: estLog(params.code), skeletonTokens: estLog(skeleton), maxDepth };
119
+ return { success: true, skeleton, originalTokens: estLog(params.code), skeletonTokens: estLog(skeleton), maxDepth, focused: false };
102
120
  }
103
121
  });
104
122
 
@@ -110,7 +128,7 @@ export function apply(ctx, config) {
110
128
  execute: async () => {
111
129
  const stats = tracker.getStats();
112
130
  const cfg = getConfig();
113
- return { success: true, ...stats, trackingEnabled: cfg.tokenSavingsTracking !== false, focus: focusState };
131
+ return { success: true, ...stats, trackingEnabled: cfg.tokenSavingsTracking !== false, focus: focusState, autoCompressThreshold: cfg.autoCompressThreshold };
114
132
  }
115
133
  });
116
134
  }
@@ -128,4 +146,4 @@ export function apply(ctx, config) {
128
146
  }
129
147
  }
130
148
 
131
- export { compressLog, skeletonize };
149
+ export { compressLog, skeletonize, shouldAutoCompress };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-context-lens",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
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",
@@ -55,6 +55,12 @@
55
55
  "@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6",
56
56
  "@deepseek-ai/dsh-settings": "^0.1.0-rc.6",
57
57
  "@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
58
- "@deepseek-ai/schemastery": "^3.18.1"
58
+ "@deepseek-ai/schemastery": "^3.18.1",
59
+ "dsh-better-sidebar": "^0.18.0-alpha.0"
60
+ },
61
+ "peerDependenciesMeta": {
62
+ "dsh-better-sidebar": {
63
+ "optional": true
64
+ }
59
65
  }
60
66
  }