@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 +94 -8
- package/lib/index.js +26 -8
- package/package.json +8 -2
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
}
|