@goodandready/dsh-context-lens 0.1.13 → 0.1.14
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 +9 -0
- package/README.ru.md +9 -0
- package/lib/ast/skeletonizer.js +49 -18
- package/lib/client.js +60 -18
- package/lib/index.js +31 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -172,3 +172,12 @@ Audit fixes (#33–#47, #18):
|
|
|
172
172
|
- **Fix (#50)**: Guard optional `betterSidebar` tab registration via `ctx.inject(['betterSidebar'], ...)` instead of direct property access on Cordis context proxy, resolving `cannot get property "betterSidebar" without inject` on client boot.
|
|
173
173
|
- **Fix (#50)**: Safely guard `_ctx.settingsScope` access in `PluginCard` with try/catch to prevent proxy property errors.
|
|
174
174
|
- **Test**: Added regression test suite simulating strict Cordis Context Proxy behavior.
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
## Changed in v0.1.14
|
|
178
|
+
|
|
179
|
+
- **Fix (#52)**: Multiline AST signature parsing in `skeletonizer.js` for TypeScript, JavaScript, Rust, and Go with complex parameter types and return annotations.
|
|
180
|
+
- **Feature (#52)**: Registered `context_lens_reset` tool to allow resetting tracker statistics and history at the start of new tasks.
|
|
181
|
+
- **Quality & UX (#52)**: Dynamic RU/EN locale detection in `HeaderChip`, `LensTab`, and `StatusPanel` via `ctx.locale` for seamless UI integration.
|
|
182
|
+
- **Perf & Stability (#52)**: Adaptive polling in `HeaderChip` pausing on hidden tabs (`visibilitychange`) and polling actively (4s) only when popover is open.
|
|
183
|
+
- **Reliability (#52)**: Atomic settings save (`scope.patch` / `scope.setAll`) in `PluginCard` with graceful fallback.
|
package/README.ru.md
CHANGED
|
@@ -161,3 +161,12 @@ MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
|
|
|
161
161
|
- **Fix (#50)**: Безопасная регистрация вкладки BetterSidebar через `ctx.inject(['betterSidebar'], ...)` вместо прямого чтения свойства из Cordis Context Proxy. Устранена ошибка `cannot get property "betterSidebar" without inject` при загрузке клиентской половины в DSH.
|
|
162
162
|
- **Fix (#50)**: Безопасное обращение к `_ctx.settingsScope` в `PluginCard` через try/catch.
|
|
163
163
|
- **Тесты**: Добавлены регрессионные тесты со строгим Cordis Context Proxy.
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
## Changed in v0.1.14
|
|
167
|
+
|
|
168
|
+
- **Исправление (#52)**: Поддержка многострочных сигнатур функций и методов в `skeletonizer.js` (TypeScript, JavaScript, Rust, Go) со сложными типами параметров и аннотациями возвращаемых значений.
|
|
169
|
+
- **Функция (#52)**: Добавлен инструмент `context_lens_reset` для сброса собранной статистики токенов и истории сжатий перед запуском новой задачи.
|
|
170
|
+
- **Качество и локализация (#52)**: Динамическое переключение языка интерфейса (RU/EN) в чипе шапки (`HeaderChip`), боковой вкладке (`LensTab`) и панели статуса (`StatusPanel`) через `ctx.locale`.
|
|
171
|
+
- **Производительность (#52)**: Адаптивный поллинг в `HeaderChip` с паузой при неактивной вкладке браузера (`document.visibilityState`) и учащённым опросом (4 сек) только при открытом поповере.
|
|
172
|
+
- **Стабильность (#52)**: Атомарное сохранение конфигурации в карточке настроек (`scope.patch` / `scope.setAll`) без риска частичной записи.
|
package/lib/ast/skeletonizer.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// ponytail: regex skeletons, no tree-sitter — handles JS/TS/Python/Go/Rust/Java signatures + imports/comments
|
|
1
|
+
// ponytail: regex skeletons, no tree-sitter — handles JS/TS/Python/Go/Rust/Java signatures + imports/comments + multiline signatures
|
|
2
2
|
const JS_FUNC_RE = /^\s*(export\s+)?(async\s+)?(function\s+(\w+)|const\s+(\w+)\s*=\s*(async\s+)?\([^)]*\)\s*=>|(\w+)\s*:\s*\([^)]*\)\s*=>|class\s+(\w+)|interface\s+(\w+)|type\s+(\w+)\s*=)/;
|
|
3
3
|
const PY_RE = /^\s*(def\s+(\w+)\s*\([^)]*\)|class\s+(\w+).*?:|async def\s+(\w+)\s*\([^)]*\))/;
|
|
4
4
|
const GO_RE = /^\s*(func\s+(\([^)]+\)\s+)?(\w+)\s*\([^)]*\)|type\s+(\w+)\s+(struct|interface))/;
|
|
@@ -6,6 +6,9 @@ const RUST_RE = /^\s*(pub(\([^)]+\))?\s+)?(async\s+)?(fn|struct|enum|impl|trait|
|
|
|
6
6
|
const IMPORT_RE = /^\s*import\s+.*from\s+['"].*['"]|^\s*import\s+['"].*['"]|^\s*export\s+.*from\s+['"]|^\s*use\s+[a-zA-Z0-9_:]+|^\s*(from\s+\S+\s+)?import\s+\S+|^\s*from\s+\S+\s+import\s+/;
|
|
7
7
|
const COMMENT_RE = /^\s*(\/\/.*|\/\*.*\*\/|\/\*.*|\*.*|#.*)/;
|
|
8
8
|
|
|
9
|
+
// Multiline signature starters
|
|
10
|
+
const MULTILINE_START_RE = /^\s*(export\s+)?(async\s+)?(function\b|const\s+\w+\s*=|\w+\s*:\s*(async\s+)?\(|def\b|async\s+def\b|func\b|(pub(\([^)]+\))?\s+)?(async\s+)?fn\b|(?:(?:public|protected|private|static|final|abstract|synchronized|native|default)\s+)+[\w.<>,\[\]?]+\s+\w+\s*\()/;
|
|
11
|
+
|
|
9
12
|
function indentDepth(line) {
|
|
10
13
|
const m = line.match(/^(\s*)/);
|
|
11
14
|
return m ? Math.floor(m[1].replace(/\t/g, ' ').length / 2) : 0;
|
|
@@ -17,12 +20,14 @@ export function skeletonize(text, { maxDepth = 3, language, includeImports = tru
|
|
|
17
20
|
const out = [];
|
|
18
21
|
let seen = new Set();
|
|
19
22
|
let pendingComment = null;
|
|
23
|
+
|
|
20
24
|
for (let idx = 0; idx < lines.length; idx++) {
|
|
21
25
|
const raw = lines[idx];
|
|
22
26
|
const line = raw.trimEnd();
|
|
23
27
|
if (!line.trim()) { pendingComment = null; continue; }
|
|
24
28
|
const d = indentDepth(raw);
|
|
25
29
|
if (d > maxDepth) { pendingComment = null; continue; }
|
|
30
|
+
|
|
26
31
|
// Handle imports
|
|
27
32
|
if (includeImports && IMPORT_RE.test(line)) {
|
|
28
33
|
const sig = line.trim().slice(0, 120);
|
|
@@ -33,14 +38,14 @@ export function skeletonize(text, { maxDepth = 3, language, includeImports = tru
|
|
|
33
38
|
pendingComment = null;
|
|
34
39
|
continue;
|
|
35
40
|
}
|
|
41
|
+
|
|
36
42
|
// Handle comments (keep JSDoc or line comments directly above a definition)
|
|
37
43
|
if (includeComments && COMMENT_RE.test(line)) {
|
|
38
|
-
// Keep comment if next non-empty line is a definition
|
|
39
44
|
let nextIdx = idx + 1;
|
|
40
45
|
while (nextIdx < lines.length && !lines[nextIdx].trim()) nextIdx++;
|
|
41
46
|
if (nextIdx < lines.length) {
|
|
42
47
|
const nextLine = lines[nextIdx];
|
|
43
|
-
if (JS_FUNC_RE.test(nextLine) || PY_RE.test(nextLine) || GO_RE.test(nextLine) || RUST_RE.test(nextLine) || /^\s*(class|function|interface|type|def |func |pub |fn )/.test(nextLine)) {
|
|
48
|
+
if (JS_FUNC_RE.test(nextLine) || PY_RE.test(nextLine) || GO_RE.test(nextLine) || RUST_RE.test(nextLine) || MULTILINE_START_RE.test(nextLine) || /^\s*(class|function|interface|type|def |func |pub |fn )/.test(nextLine)) {
|
|
44
49
|
pendingComment = line.trim().slice(0, 120);
|
|
45
50
|
continue;
|
|
46
51
|
}
|
|
@@ -56,36 +61,61 @@ export function skeletonize(text, { maxDepth = 3, language, includeImports = tru
|
|
|
56
61
|
pendingComment = null;
|
|
57
62
|
continue;
|
|
58
63
|
}
|
|
64
|
+
|
|
65
|
+
// Multiline signature detection: if line starts a signature but doesn't finish with '{', '=>', or ':'
|
|
66
|
+
let candidateLine = line;
|
|
67
|
+
let accumulatedIdx = idx;
|
|
68
|
+
const isComplete = (s) => (s.includes('{') && s.includes(')')) || (s.includes('=>') && s.includes(')')) || (s.endsWith(':') && s.includes(')'));
|
|
69
|
+
if (MULTILINE_START_RE.test(candidateLine) && !isComplete(candidateLine)) {
|
|
70
|
+
let buffer = candidateLine.trim();
|
|
71
|
+
let scanIdx = idx + 1;
|
|
72
|
+
while (scanIdx < lines.length && scanIdx <= idx + 12) {
|
|
73
|
+
const nextRaw = lines[scanIdx].trim();
|
|
74
|
+
if (!nextRaw) { scanIdx++; continue; }
|
|
75
|
+
buffer += ' ' + nextRaw;
|
|
76
|
+
if ((nextRaw.includes('{') || nextRaw.includes('=>') || nextRaw.endsWith(':')) && buffer.includes(')')) {
|
|
77
|
+
candidateLine = buffer;
|
|
78
|
+
accumulatedIdx = scanIdx;
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
scanIdx++;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
59
85
|
let sig = null;
|
|
60
86
|
// try JS/TS
|
|
61
87
|
if (!language || language === 'js' || language === 'ts') {
|
|
62
|
-
const m =
|
|
63
|
-
if (m) sig =
|
|
64
|
-
if (!sig && /^\s*(export\s+)?(async\s+)?function\s+\w+/.test(
|
|
65
|
-
if (!sig && /^\s*class\s+\w+/.test(
|
|
66
|
-
if (!sig && /^\s*import\s+/.test(
|
|
88
|
+
const m = candidateLine.match(/^\s*(export\s+)?(async\s+)?(function\s+\w+[^\n]*|const\s+\w+\s*=.*=>.*|class\s+\w+.*|interface\s+\w+.*|type\s+\w+\s*=.*|(?:public|private|protected)?\s*(async\s+)?\w+\s*\([^)]*\)\s*[:{]|import\s+.*|export\s+.*)/);
|
|
89
|
+
if (m) sig = candidateLine.trim().replace(/\s*\{\s*$/, '').replace(/\s+$/, '');
|
|
90
|
+
if (!sig && /^\s*(export\s+)?(async\s+)?function\s+\w+/.test(candidateLine)) sig = candidateLine.trim();
|
|
91
|
+
if (!sig && /^\s*class\s+\w+/.test(candidateLine)) sig = candidateLine.trim();
|
|
92
|
+
if (!sig && /^\s*import\s+/.test(candidateLine)) sig = candidateLine.trim().slice(0, 120);
|
|
67
93
|
}
|
|
68
94
|
if (!sig && (!language || language === 'py' || language === 'python')) {
|
|
69
|
-
const m =
|
|
70
|
-
if (m) sig =
|
|
95
|
+
const m = candidateLine.match(PY_RE);
|
|
96
|
+
if (m) sig = candidateLine.trim();
|
|
71
97
|
}
|
|
72
98
|
if (!sig && (!language || language === 'go')) {
|
|
73
|
-
const m =
|
|
74
|
-
if (m) sig =
|
|
99
|
+
const m = candidateLine.match(GO_RE);
|
|
100
|
+
if (m) sig = candidateLine.trim();
|
|
75
101
|
}
|
|
76
102
|
if (!sig && (!language || language === 'rust')) {
|
|
77
|
-
if (RUST_RE.test(
|
|
103
|
+
if (RUST_RE.test(candidateLine)) sig = candidateLine.trim();
|
|
78
104
|
}
|
|
79
105
|
if (!sig && (!language || language === 'java')) {
|
|
80
|
-
if (/^\s*(?:(?:public|protected|private|static|final|abstract|synchronized|native|default)\s+)+(?:class|interface|enum|record)\s+\w+/.test(
|
|
81
|
-
|| /^\s*(?:(?:public|protected|private|static|final|abstract|synchronized|native|default)\s+)+[\w.<>,\[\]?]+\s+\w+\s*\(/.test(
|
|
82
|
-
|| /^\s*(class|interface|enum|record)\s+\w+/.test(
|
|
106
|
+
if (/^\s*(?:(?:public|protected|private|static|final|abstract|synchronized|native|default)\s+)+(?:class|interface|enum|record)\s+\w+/.test(candidateLine)
|
|
107
|
+
|| /^\s*(?:(?:public|protected|private|static|final|abstract|synchronized|native|default)\s+)+[\w.<>,\[\]?]+\s+\w+\s*\(/.test(candidateLine)
|
|
108
|
+
|| /^\s*(class|interface|enum|record)\s+\w+/.test(candidateLine)) sig = candidateLine.trim();
|
|
83
109
|
}
|
|
84
110
|
// generic fallback: if no specific language, try all
|
|
85
111
|
if (!sig && !language) {
|
|
86
|
-
if (JS_FUNC_RE.test(
|
|
112
|
+
if (JS_FUNC_RE.test(candidateLine) || PY_RE.test(candidateLine) || GO_RE.test(candidateLine) || RUST_RE.test(candidateLine)) sig = candidateLine.trim();
|
|
87
113
|
}
|
|
114
|
+
|
|
88
115
|
if (sig) {
|
|
116
|
+
if (accumulatedIdx > idx) {
|
|
117
|
+
idx = accumulatedIdx; // advance loop past multiline signature lines
|
|
118
|
+
}
|
|
89
119
|
// If we have a pending comment, prepend it
|
|
90
120
|
if (pendingComment) {
|
|
91
121
|
const commentSig = pendingComment;
|
|
@@ -97,7 +127,7 @@ export function skeletonize(text, { maxDepth = 3, language, includeImports = tru
|
|
|
97
127
|
}
|
|
98
128
|
// normalize: trim trailing { : etc, keep signature short
|
|
99
129
|
sig = sig.replace(/\s*\{\s*$/, '').replace(/:\s*$/, '').trim();
|
|
100
|
-
if (sig.length >
|
|
130
|
+
if (sig.length > 180) sig = sig.slice(0, 177) + '...';
|
|
101
131
|
const key = sig;
|
|
102
132
|
if (!seen.has(key)) {
|
|
103
133
|
seen.add(key);
|
|
@@ -107,6 +137,7 @@ export function skeletonize(text, { maxDepth = 3, language, includeImports = tru
|
|
|
107
137
|
pendingComment = null;
|
|
108
138
|
}
|
|
109
139
|
}
|
|
140
|
+
|
|
110
141
|
// if nothing found, fallback to first N non-empty lines truncated
|
|
111
142
|
if (out.length === 0) {
|
|
112
143
|
const fallback = lines.filter((l) => l.trim()).slice(0, Math.min(20, maxDepth * 6)).map((l) => l.trim().slice(0, 120));
|
package/lib/client.js
CHANGED
|
@@ -61,6 +61,16 @@ window.__ModuleLoader__.load({
|
|
|
61
61
|
// #45: preview uses POST /dsh-context-lens/compress-preview (server compressor)
|
|
62
62
|
|
|
63
63
|
// #43: shared status UI for LensTab + HeaderChip popover
|
|
64
|
+
function getActiveLocale(ctx) {
|
|
65
|
+
try {
|
|
66
|
+
if (ctx && ctx.locale && typeof ctx.locale.getSnapshot === 'function') {
|
|
67
|
+
const snap = ctx.locale.getSnapshot();
|
|
68
|
+
if (snap && snap.active && snap.active.startsWith('ru')) return ru;
|
|
69
|
+
}
|
|
70
|
+
} catch {}
|
|
71
|
+
return en;
|
|
72
|
+
}
|
|
73
|
+
|
|
64
74
|
function StatusPanel({ stats, history, labels, onRefresh, title, showBadge, compact }) {
|
|
65
75
|
const L = labels || en;
|
|
66
76
|
const warn = !!(stats && stats.lowBudget);
|
|
@@ -165,6 +175,7 @@ window.__ModuleLoader__.load({
|
|
|
165
175
|
function LensTab({ ctx: _ctx, scope }) {
|
|
166
176
|
const [stats, setStats] = React.useState(null);
|
|
167
177
|
const [history, setHistory] = React.useState([]);
|
|
178
|
+
const L = React.useMemo(() => getActiveLocale(_ctx), [_ctx]);
|
|
168
179
|
const apply = (j) => {
|
|
169
180
|
if (j && j.stats) setStats(j.stats);
|
|
170
181
|
if (j && j.history) setHistory(j.history || []);
|
|
@@ -173,7 +184,7 @@ window.__ModuleLoader__.load({
|
|
|
173
184
|
React.useEffect(() => { refresh(); }, []);
|
|
174
185
|
return React.createElement('div', { style: { padding: 12 } },
|
|
175
186
|
React.createElement(StatusPanel, {
|
|
176
|
-
stats, history, labels:
|
|
187
|
+
stats, history, labels: L, onRefresh: refresh, title: 'Context Lens', showBadge: false, compact: false
|
|
177
188
|
})
|
|
178
189
|
);
|
|
179
190
|
}
|
|
@@ -183,6 +194,7 @@ window.__ModuleLoader__.load({
|
|
|
183
194
|
const [history, setHistory] = React.useState([]);
|
|
184
195
|
const [open, setOpen] = React.useState(false);
|
|
185
196
|
const ref = React.useRef(null);
|
|
197
|
+
const L = React.useMemo(() => getActiveLocale(_ctx), [_ctx]);
|
|
186
198
|
|
|
187
199
|
const fetchStatus = () => fetchLensStatus().then((j) => {
|
|
188
200
|
if (j && j.stats) setStats(j.stats);
|
|
@@ -192,9 +204,30 @@ window.__ModuleLoader__.load({
|
|
|
192
204
|
React.useEffect(() => {
|
|
193
205
|
let alive = true;
|
|
194
206
|
fetchStatus();
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
207
|
+
// Adaptive polling: poll every 4s when popover is open, 15s when closed, pause when tab hidden
|
|
208
|
+
const pollInterval = open ? 4000 : 15000;
|
|
209
|
+
const id = setInterval(() => {
|
|
210
|
+
if (!alive) return;
|
|
211
|
+
if (typeof document !== 'undefined' && document.visibilityState === 'hidden') return;
|
|
212
|
+
fetchStatus();
|
|
213
|
+
}, pollInterval);
|
|
214
|
+
|
|
215
|
+
const onVisChange = () => {
|
|
216
|
+
if (typeof document !== 'undefined' && document.visibilityState === 'visible' && alive) {
|
|
217
|
+
fetchStatus();
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
if (typeof document !== 'undefined') {
|
|
221
|
+
document.addEventListener('visibilitychange', onVisChange);
|
|
222
|
+
}
|
|
223
|
+
return () => {
|
|
224
|
+
alive = false;
|
|
225
|
+
clearInterval(id);
|
|
226
|
+
if (typeof document !== 'undefined') {
|
|
227
|
+
document.removeEventListener('visibilitychange', onVisChange);
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
}, [open]);
|
|
198
231
|
|
|
199
232
|
React.useEffect(() => {
|
|
200
233
|
if (!open) return;
|
|
@@ -221,14 +254,14 @@ window.__ModuleLoader__.load({
|
|
|
221
254
|
onClick: (e) => e.stopPropagation()
|
|
222
255
|
},
|
|
223
256
|
React.createElement(StatusPanel, {
|
|
224
|
-
stats, history, labels:
|
|
257
|
+
stats, history, labels: L, onRefresh: fetchStatus, title: 'Context Lens', showBadge: true, compact: true
|
|
225
258
|
})
|
|
226
259
|
) : null;
|
|
227
260
|
|
|
228
261
|
return React.createElement('div', { ref, style: { position: 'relative', display: 'inline-flex', alignItems: 'center' } },
|
|
229
262
|
React.createElement('button', {
|
|
230
263
|
type: 'button',
|
|
231
|
-
title: hasSavings ? `Context Lens: ${
|
|
264
|
+
title: hasSavings ? `Context Lens: ${L.saved} ${stats.savedTokens} ${L.tokens} (${stats.savedPercent}%)` : `Context Lens: ${L.active}`,
|
|
232
265
|
onClick: () => setOpen((v) => !v),
|
|
233
266
|
style: {
|
|
234
267
|
appearance: 'none', font: 'inherit', cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: 4,
|
|
@@ -320,13 +353,24 @@ window.__ModuleLoader__.load({
|
|
|
320
353
|
async function onSave() {
|
|
321
354
|
if (!scope) { setSaveErr('Settings unavailable'); return; }
|
|
322
355
|
setSaving(true); setSaveErr('');
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
356
|
+
try {
|
|
357
|
+
if (typeof scope.patch === 'function') {
|
|
358
|
+
await scope.patch(draft);
|
|
359
|
+
} else if (typeof scope.setAll === 'function') {
|
|
360
|
+
await scope.setAll(draft);
|
|
361
|
+
} else {
|
|
362
|
+
const keys = Object.keys(draft);
|
|
363
|
+
const errs = [];
|
|
364
|
+
for (const k of keys) {
|
|
365
|
+
try { await scope.set(k, draft[k]); } catch (e) { errs.push(k + ': ' + (e && e.message || String(e))); }
|
|
366
|
+
}
|
|
367
|
+
if (errs.length) throw new Error(errs.join('; '));
|
|
368
|
+
}
|
|
369
|
+
} catch (e) {
|
|
370
|
+
setSaveErr(e && e.message || String(e));
|
|
371
|
+
} finally {
|
|
372
|
+
setSaving(false);
|
|
327
373
|
}
|
|
328
|
-
setSaving(false);
|
|
329
|
-
if (errs.length) setSaveErr(errs.join('; '));
|
|
330
374
|
}
|
|
331
375
|
|
|
332
376
|
async function onPreview() {
|
|
@@ -493,19 +537,17 @@ window.__ModuleLoader__.load({
|
|
|
493
537
|
const svc = sctx && sctx.betterSidebar;
|
|
494
538
|
if (!svc || typeof svc.registerTab !== 'function') return;
|
|
495
539
|
try {
|
|
496
|
-
const
|
|
540
|
+
const tabDef = {
|
|
497
541
|
id: 'dsh-context-lens:tab',
|
|
498
542
|
title: () => 'Lens',
|
|
499
543
|
icon: () => React.createElement('span', null, '◐'),
|
|
500
544
|
order: 50,
|
|
501
545
|
component: ({ scope }) => React.createElement(LensTab, { ctx, scope })
|
|
502
|
-
}
|
|
546
|
+
};
|
|
503
547
|
if (typeof sctx.effect === 'function') {
|
|
504
|
-
sctx.effect(
|
|
505
|
-
} else if (typeof ctx.effect === 'function') {
|
|
506
|
-
ctx.effect(register);
|
|
548
|
+
sctx.effect(() => svc.registerTab(tabDef));
|
|
507
549
|
} else {
|
|
508
|
-
|
|
550
|
+
svc.registerTab(tabDef);
|
|
509
551
|
}
|
|
510
552
|
} catch (e) {
|
|
511
553
|
console.warn('[dsh-context-lens] betterSidebar registerTab failed', e && e.message || e);
|
package/lib/index.js
CHANGED
|
@@ -20,13 +20,16 @@ const NS = '@goodandready/dsh-context-lens';
|
|
|
20
20
|
|
|
21
21
|
// #37/#18: focus keyed by session id (not process-global singleton)
|
|
22
22
|
const focusBySession = new Map();
|
|
23
|
+
let lastActiveSession = '__default__';
|
|
23
24
|
|
|
24
25
|
function sessionKey(params, meta) {
|
|
25
|
-
|
|
26
|
+
const k = (
|
|
26
27
|
(params && (params.sessionId || params.session_id)) ||
|
|
27
28
|
(meta && (meta.sessionId || meta.session_id || (meta.session && meta.session.id))) ||
|
|
28
29
|
'__default__'
|
|
29
30
|
);
|
|
31
|
+
if (k !== '__default__') lastActiveSession = k;
|
|
32
|
+
return k;
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
function getFocus(key) {
|
|
@@ -142,6 +145,26 @@ export function apply(ctx, config) {
|
|
|
142
145
|
}
|
|
143
146
|
});
|
|
144
147
|
|
|
148
|
+
ctx.tools.register({
|
|
149
|
+
name: 'context_lens_reset',
|
|
150
|
+
description: 'Reset context-lens savings stats and history (e.g. at the start of a new task)',
|
|
151
|
+
parameters: {
|
|
152
|
+
type: 'object',
|
|
153
|
+
properties: {
|
|
154
|
+
sessionId: { type: 'string', description: 'Optional session id to also reset session focus' }
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
output: { schema: OUTPUT_SCHEMA, render: renderOutput },
|
|
158
|
+
execute: async (params, meta) => {
|
|
159
|
+
tracker.reset();
|
|
160
|
+
const key = sessionKey(params || {}, meta);
|
|
161
|
+
if (params && params.sessionId) {
|
|
162
|
+
focusBySession.delete(key);
|
|
163
|
+
}
|
|
164
|
+
return { success: true, message: 'Context lens stats and history reset', sessionId: key };
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
145
168
|
ctx.tools.register({
|
|
146
169
|
name: 'context_lens_stats',
|
|
147
170
|
description: 'Show session token savings stats for context-lens',
|
|
@@ -179,13 +202,19 @@ export function apply(ctx, config) {
|
|
|
179
202
|
handler: (req, res) => {
|
|
180
203
|
const cfg = getConfig();
|
|
181
204
|
if (typeof cfg.budgetLimit === 'number') tracker.setBudgetLimit(cfg.budgetLimit);
|
|
205
|
+
let sId = '__default__';
|
|
206
|
+
try {
|
|
207
|
+
const u = new URL(req.url, 'http://127.0.0.1');
|
|
208
|
+
sId = u.searchParams.get('sessionId') || u.searchParams.get('session_id') || lastActiveSession || '__default__';
|
|
209
|
+
} catch {}
|
|
182
210
|
res.setHeader('content-type', 'application/json');
|
|
183
211
|
res.end(JSON.stringify({
|
|
184
212
|
ok: true,
|
|
185
213
|
plugin: 'dsh-context-lens',
|
|
186
214
|
stats: tracker.getStats(cfg.budgetLimit),
|
|
187
215
|
history: tracker.getHistory(),
|
|
188
|
-
focus: getFocus(
|
|
216
|
+
focus: getFocus(sId),
|
|
217
|
+
sessionId: sId
|
|
189
218
|
}));
|
|
190
219
|
}
|
|
191
220
|
}), 'dsh-context-lens status route');
|
package/package.json
CHANGED