@goodandready/dsh-context-lens 0.1.10 → 0.1.12
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 +18 -0
- package/README.ru.md +10 -0
- package/lib/ast/skeletonizer.js +4 -2
- package/lib/auto-compress.js +7 -0
- package/lib/client.js +198 -156
- package/lib/compression/log-compressor.js +2 -4
- package/lib/index.js +94 -30
- package/lib/tokens/estimate.js +6 -0
- package/lib/tokens/tracker.js +10 -5
- package/package.json +1 -2
package/README.md
CHANGED
|
@@ -147,3 +147,21 @@ dsh-context-lens:
|
|
|
147
147
|
## 📄 License
|
|
148
148
|
|
|
149
149
|
MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
## Changed in v0.1.11
|
|
153
|
+
|
|
154
|
+
Audit fixes (#33–#47, #18):
|
|
155
|
+
|
|
156
|
+
- Auto-compress no longer forces `balanced`; uses configured `compressionMode`.
|
|
157
|
+
- `budgetLimit` stops token counting after the limit (matches settings description).
|
|
158
|
+
- Focus state is per-session (`sessionId`), not process-global.
|
|
159
|
+
- Settings card exposes `budgetLimit`, `autoCollapse`, `autoCompressThreshold`; low-budget shows a warning instead of force-closing the card.
|
|
160
|
+
- Client `betterSidebar` inject is optional; preview uses server `/dsh-context-lens/compress-preview`.
|
|
161
|
+
- Python imports kept in skeletons; Java locals no longer mistaken for signatures.
|
|
162
|
+
- Shared `estimateTokens` helper; unused `dsh-credentials` peer removed.
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
## Changed in v0.1.12
|
|
166
|
+
|
|
167
|
+
#43: extract shared `StatusPanel` used by LensTab and HeaderChip popover (budget bar, history, refresh).
|
package/README.ru.md
CHANGED
|
@@ -144,3 +144,13 @@ dsh-context-lens:
|
|
|
144
144
|
## 📄 Лицензия
|
|
145
145
|
|
|
146
146
|
MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
## Changed in v0.1.11
|
|
150
|
+
|
|
151
|
+
Исправления audit #33–#47 / #18: режим auto-compress не форсирует balanced; budgetLimit останавливает подсчёт; focus per-session; поля settings card; optional betterSidebar; серверный preview; Python imports / Java locals; общий estimateTokens; удалён unused peer dsh-credentials.
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
## Changed in v0.1.12
|
|
155
|
+
|
|
156
|
+
#43: extract shared `StatusPanel` used by LensTab and HeaderChip popover (budget bar, history, refresh).
|
package/lib/ast/skeletonizer.js
CHANGED
|
@@ -3,7 +3,7 @@ const JS_FUNC_RE = /^\s*(export\s+)?(async\s+)?(function\s+(\w+)|const\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))/;
|
|
5
5
|
const RUST_RE = /^\s*(pub(\([^)]+\))?\s+)?(async\s+)?(fn|struct|enum|impl|trait|type|const|static)\s+\w+/;
|
|
6
|
-
const IMPORT_RE = /^\s*import\s+.*from\s+['"].*['"]|^\s*import\s+['"].*['"]|^\s*export\s+.*from\s+['"]|^\s*use\s+[a-zA-Z0-9_:]+/;
|
|
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
9
|
function indentDepth(line) {
|
|
@@ -77,7 +77,9 @@ export function skeletonize(text, { maxDepth = 3, language, includeImports = tru
|
|
|
77
77
|
if (RUST_RE.test(line)) sig = line.trim();
|
|
78
78
|
}
|
|
79
79
|
if (!sig && (!language || language === 'java')) {
|
|
80
|
-
if (/^\s*(public|protected|private|static
|
|
80
|
+
if (/^\s*(?:(?:public|protected|private|static|final|abstract|synchronized|native|default)\s+)+(?:class|interface|enum|record)\s+\w+/.test(line)
|
|
81
|
+
|| /^\s*(?:(?:public|protected|private|static|final|abstract|synchronized|native|default)\s+)+[\w.<>,\[\]?]+\s+\w+\s*\(/.test(line)
|
|
82
|
+
|| /^\s*(class|interface|enum|record)\s+\w+/.test(line)) sig = line.trim();
|
|
81
83
|
}
|
|
82
84
|
// generic fallback: if no specific language, try all
|
|
83
85
|
if (!sig && !language) {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Decide whether auto-compress should trigger for a log blob. */
|
|
2
|
+
export function shouldAutoCompress(text, threshold) {
|
|
3
|
+
if (!threshold || threshold <= 0) return false;
|
|
4
|
+
return (text || '').length > threshold || (text || '').split('\n').length > 100;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export default { shouldAutoCompress };
|
package/lib/client.js
CHANGED
|
@@ -20,7 +20,16 @@ window.__ModuleLoader__.load({
|
|
|
20
20
|
ready: 'Ready',
|
|
21
21
|
budget: 'Budget',
|
|
22
22
|
history: 'Recent ops',
|
|
23
|
-
lowBudget: 'Budget nearly exhausted'
|
|
23
|
+
lowBudget: 'Budget nearly exhausted',
|
|
24
|
+
threshold: 'Auto-compress threshold (chars)',
|
|
25
|
+
autoCollapse: 'Warn when budget is nearly exhausted',
|
|
26
|
+
previewApprox: 'Server preview',
|
|
27
|
+
noData: 'No data yet',
|
|
28
|
+
refresh: 'Refresh',
|
|
29
|
+
ops: 'ops',
|
|
30
|
+
active: 'Active',
|
|
31
|
+
readyState: 'Ready',
|
|
32
|
+
lowBudgetBadge: 'Low Budget ⚠'
|
|
24
33
|
};
|
|
25
34
|
const ru = {
|
|
26
35
|
title: 'Context Lens & Token Guard',
|
|
@@ -37,47 +46,135 @@ window.__ModuleLoader__.load({
|
|
|
37
46
|
ready: 'Готово',
|
|
38
47
|
budget: 'Бюджет',
|
|
39
48
|
history: 'Последние операции',
|
|
40
|
-
lowBudget: 'Бюджет почти исчерпан'
|
|
49
|
+
lowBudget: 'Бюджет почти исчерпан',
|
|
50
|
+
threshold: 'Порог авто-сжатия (символы)',
|
|
51
|
+
autoCollapse: 'Предупреждать при почти исчерпанном бюджете',
|
|
52
|
+
previewApprox: 'Серверный предпросмотр',
|
|
53
|
+
noData: 'Пока нет данных',
|
|
54
|
+
refresh: 'Обновить',
|
|
55
|
+
ops: 'оп.',
|
|
56
|
+
active: 'Активен',
|
|
57
|
+
readyState: 'Готов',
|
|
58
|
+
lowBudgetBadge: 'Мало бюджета ⚠'
|
|
41
59
|
};
|
|
42
60
|
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
61
|
+
// #45: preview uses POST /dsh-context-lens/compress-preview (server compressor)
|
|
62
|
+
|
|
63
|
+
// #43: shared status UI for LensTab + HeaderChip popover
|
|
64
|
+
function StatusPanel({ stats, history, labels, onRefresh, title, showBadge, compact }) {
|
|
65
|
+
const L = labels || en;
|
|
66
|
+
const warn = !!(stats && stats.lowBudget);
|
|
67
|
+
const hasSavings = !!(stats && stats.savedTokens > 0);
|
|
68
|
+
const barColor = warn ? '#d73a4a' : 'var(--dsw-alias-label-primary)';
|
|
69
|
+
const previewLen = compact ? 28 : 40;
|
|
70
|
+
const barH = compact ? 5 : 6;
|
|
71
|
+
const children = [];
|
|
72
|
+
if (title || showBadge) {
|
|
73
|
+
children.push(React.createElement('div', {
|
|
74
|
+
key: 'head',
|
|
75
|
+
style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 8 }
|
|
76
|
+
},
|
|
77
|
+
title
|
|
78
|
+
? React.createElement('div', { style: { fontWeight: 600, fontSize: 13, display: 'flex', alignItems: 'center', gap: 6, color: 'var(--dsw-alias-label-primary)' } },
|
|
79
|
+
React.createElement('span', { style: { opacity: 0.8 } }, '◐'),
|
|
80
|
+
title
|
|
81
|
+
)
|
|
82
|
+
: React.createElement('span'),
|
|
83
|
+
showBadge
|
|
84
|
+
? React.createElement('span', {
|
|
85
|
+
style: {
|
|
86
|
+
fontSize: 11, padding: '1px 6px', borderRadius: 4,
|
|
87
|
+
background: warn ? 'rgba(215, 58, 74, 0.15)' : 'var(--dsw-alias-bg-layer-2)',
|
|
88
|
+
color: warn ? '#d73a4a' : 'var(--dsw-alias-label-tertiary)',
|
|
89
|
+
border: '1px solid ' + (warn ? 'rgba(215, 58, 74, 0.3)' : 'var(--dsw-alias-border-l2)')
|
|
90
|
+
}
|
|
91
|
+
}, warn ? L.lowBudgetBadge : (stats ? L.active : L.readyState))
|
|
92
|
+
: null
|
|
93
|
+
));
|
|
52
94
|
}
|
|
53
|
-
|
|
54
|
-
|
|
95
|
+
children.push(
|
|
96
|
+
hasSavings
|
|
97
|
+
? React.createElement('div', { key: 'sum', style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)', marginBottom: compact ? 8 : 0 } },
|
|
98
|
+
`${L.saved} ${stats.savedTokens} ${L.tokens} (${stats.savedPercent}%) · ${stats.calls} ${L.ops}`)
|
|
99
|
+
: 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)
|
|
100
|
+
);
|
|
101
|
+
if (stats && stats.lowBudget && !showBadge) {
|
|
102
|
+
children.push(React.createElement('div', { key: 'warn', style: { marginTop: 6, fontSize: 11, color: '#d73a4a' } }, '⚠ ' + L.lowBudget));
|
|
103
|
+
}
|
|
104
|
+
if (stats && stats.budgetLimit) {
|
|
105
|
+
children.push(React.createElement('div', { key: 'budget', style: { marginTop: compact ? 6 : 8, marginBottom: compact ? 8 : 0 } },
|
|
106
|
+
React.createElement('div', {
|
|
107
|
+
style: {
|
|
108
|
+
display: compact ? 'flex' : 'block',
|
|
109
|
+
justifyContent: 'space-between',
|
|
110
|
+
fontSize: 11,
|
|
111
|
+
color: warn ? '#d73a4a' : 'var(--dsw-alias-label-secondary)',
|
|
112
|
+
marginBottom: 4
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
compact
|
|
116
|
+
? React.createElement(React.Fragment, null,
|
|
117
|
+
React.createElement('span', null, L.budget),
|
|
118
|
+
React.createElement('span', null, `${stats.budgetUsed || 0} / ${stats.budgetLimit} (${stats.budgetPercent || 0}%)`)
|
|
119
|
+
)
|
|
120
|
+
: `${L.budget} ${stats.budgetUsed}/${stats.budgetLimit} ${L.tokens} (${stats.budgetPercent}%)${warn ? ' ⚠' : ''}`
|
|
121
|
+
),
|
|
122
|
+
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' } },
|
|
123
|
+
React.createElement('div', { style: { height: '100%', width: Math.min(100, stats.budgetPercent || 0) + '%', background: barColor, transition: 'width .2s' } })
|
|
124
|
+
)
|
|
125
|
+
));
|
|
126
|
+
}
|
|
127
|
+
if (history && history.length) {
|
|
128
|
+
children.push(React.createElement('div', {
|
|
129
|
+
key: 'hist',
|
|
130
|
+
style: { marginTop: compact ? 8 : 10, paddingTop: compact ? 8 : 0, borderTop: compact ? '1px solid var(--dsw-alias-border-l3)' : undefined }
|
|
131
|
+
},
|
|
132
|
+
React.createElement('div', { style: { fontSize: 11, color: 'var(--dsw-alias-label-tertiary)', marginBottom: 4 } }, L.history),
|
|
133
|
+
...history.slice(0, 3).map((h) => React.createElement('div', {
|
|
134
|
+
key: h.id,
|
|
135
|
+
style: { fontSize: 11, color: 'var(--dsw-alias-label-secondary)', padding: '2px 0', overflow: compact ? 'hidden' : undefined, textOverflow: compact ? 'ellipsis' : undefined, whiteSpace: compact ? 'nowrap' : undefined }
|
|
136
|
+
}, `−${h.savedTokens} tk (${h.savedPercent}%) · ${(h.preview || '').slice(0, previewLen)}`))
|
|
137
|
+
));
|
|
138
|
+
}
|
|
139
|
+
if (onRefresh) {
|
|
140
|
+
children.push(React.createElement('div', {
|
|
141
|
+
key: 'act',
|
|
142
|
+
style: { marginTop: compact ? 10 : 12, display: 'flex', gap: 8, justifyContent: compact ? 'flex-end' : undefined }
|
|
143
|
+
},
|
|
144
|
+
React.createElement('button', {
|
|
145
|
+
type: 'button',
|
|
146
|
+
onClick: onRefresh,
|
|
147
|
+
style: {
|
|
148
|
+
appearance: 'none', cursor: 'pointer', fontSize: compact ? 11 : 12,
|
|
149
|
+
padding: compact ? '3px 8px' : '4px 10px', borderRadius: 6,
|
|
150
|
+
border: '1px solid var(--dsw-alias-border-l2)',
|
|
151
|
+
background: compact ? 'var(--dsw-alias-bg-layer-2)' : 'var(--dsw-alias-bg-layer-3)',
|
|
152
|
+
color: compact ? 'var(--dsw-alias-label-secondary)' : 'var(--dsw-alias-label-primary)'
|
|
153
|
+
}
|
|
154
|
+
}, L.refresh)
|
|
155
|
+
));
|
|
156
|
+
}
|
|
157
|
+
return React.createElement(React.Fragment, null, ...children);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function fetchLensStatus() {
|
|
161
|
+
return fetch('/dsh-context-lens/status', { headers: { accept: 'application/json' } })
|
|
162
|
+
.then((r) => r.ok ? r.json() : null);
|
|
55
163
|
}
|
|
56
164
|
|
|
57
165
|
function LensTab({ ctx: _ctx, scope }) {
|
|
58
166
|
const [stats, setStats] = React.useState(null);
|
|
59
167
|
const [history, setHistory] = React.useState([]);
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const
|
|
168
|
+
const apply = (j) => {
|
|
169
|
+
if (j && j.stats) setStats(j.stats);
|
|
170
|
+
if (j && j.history) setHistory(j.history || []);
|
|
171
|
+
};
|
|
172
|
+
const refresh = () => fetchLensStatus().then(apply).catch(() => {});
|
|
173
|
+
React.useEffect(() => { refresh(); }, []);
|
|
65
174
|
return React.createElement('div', { style: { padding: 12 } },
|
|
66
|
-
React.createElement(
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
React.createElement('div', { style: { fontSize: 11, color: stats.lowBudget ? '#d73a4a' : 'var(--dsw-alias-label-secondary)', marginBottom: 4 } }, `Budget ${stats.budgetUsed}/${stats.budgetLimit} tokens (${stats.budgetPercent}%)${stats.lowBudget ? ' ⚠' : ''}`),
|
|
70
|
-
React.createElement('div', { style: { height: 6, borderRadius: 3, background: 'var(--dsw-alias-bg-layer-2, var(--dsw-alias-bg-layer-3))', overflow: 'hidden' } },
|
|
71
|
-
React.createElement('div', { style: { height: '100%', width: Math.min(100, stats.budgetPercent) + '%', background: barColor, transition: 'width .2s' } })
|
|
72
|
-
)
|
|
73
|
-
) : null,
|
|
74
|
-
history.length ? React.createElement('div', { style: { marginTop: 10 } },
|
|
75
|
-
React.createElement('div', { style: { fontSize: 11, color: 'var(--dsw-alias-label-tertiary)', marginBottom: 4 } }, 'Recent ops'),
|
|
76
|
-
...history.slice(0, 3).map((h) => React.createElement('div', { key: h.id, style: { fontSize: 11, color: 'var(--dsw-alias-label-secondary)', padding: '2px 0' } }, `−${h.savedTokens} tk (${h.savedPercent}%) · ${(h.preview || '').slice(0, 40)}`))
|
|
77
|
-
) : null,
|
|
78
|
-
React.createElement('div', { style: { marginTop: 12, display: 'flex', gap: 8 } },
|
|
79
|
-
React.createElement('button', { onClick: refresh, style: { fontSize: 12, padding: '4px 10px', borderRadius: 6, border: '1px solid var(--dsw-alias-border-l2)', background: 'var(--dsw-alias-bg-layer-3)', color: 'var(--dsw-alias-label-primary)', cursor: 'pointer' } }, 'Refresh')
|
|
80
|
-
)
|
|
175
|
+
React.createElement(StatusPanel, {
|
|
176
|
+
stats, history, labels: en, onRefresh: refresh, title: 'Context Lens', showBadge: false, compact: false
|
|
177
|
+
})
|
|
81
178
|
);
|
|
82
179
|
}
|
|
83
180
|
|
|
@@ -87,15 +184,10 @@ window.__ModuleLoader__.load({
|
|
|
87
184
|
const [open, setOpen] = React.useState(false);
|
|
88
185
|
const ref = React.useRef(null);
|
|
89
186
|
|
|
90
|
-
const fetchStatus = () => {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
if (j && j.stats) setStats(j.stats);
|
|
95
|
-
if (j && j.history) setHistory(j.history);
|
|
96
|
-
})
|
|
97
|
-
.catch(() => {});
|
|
98
|
-
};
|
|
187
|
+
const fetchStatus = () => fetchLensStatus().then((j) => {
|
|
188
|
+
if (j && j.stats) setStats(j.stats);
|
|
189
|
+
if (j && j.history) setHistory(j.history || []);
|
|
190
|
+
}).catch(() => {});
|
|
99
191
|
|
|
100
192
|
React.useEffect(() => {
|
|
101
193
|
let alive = true;
|
|
@@ -121,115 +213,28 @@ window.__ModuleLoader__.load({
|
|
|
121
213
|
|
|
122
214
|
const popover = open ? React.createElement('div', {
|
|
123
215
|
style: {
|
|
124
|
-
position: 'absolute',
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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'
|
|
216
|
+
position: 'absolute', top: 'calc(100% + 6px)', right: 0, width: 270,
|
|
217
|
+
background: 'var(--dsw-alias-bg-layer-3)', border: '1px solid var(--dsw-alias-border-l2)',
|
|
218
|
+
borderRadius: 10, boxShadow: '0 8px 24px rgba(0, 0, 0, 0.28)', padding: '12px 14px',
|
|
219
|
+
zIndex: 1000, fontSize: 12, color: 'var(--dsw-alias-label-primary)', cursor: 'default', textAlign: 'left'
|
|
138
220
|
},
|
|
139
221
|
onClick: (e) => e.stopPropagation()
|
|
140
222
|
},
|
|
141
|
-
React.createElement(
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
)
|
|
223
|
+
React.createElement(StatusPanel, {
|
|
224
|
+
stats, history, labels: en, onRefresh: fetchStatus, title: 'Context Lens', showBadge: true, compact: true
|
|
225
|
+
})
|
|
211
226
|
) : null;
|
|
212
227
|
|
|
213
228
|
return React.createElement('div', { ref, style: { position: 'relative', display: 'inline-flex', alignItems: 'center' } },
|
|
214
229
|
React.createElement('button', {
|
|
215
230
|
type: 'button',
|
|
216
|
-
title: hasSavings ? `Context Lens:
|
|
231
|
+
title: hasSavings ? `Context Lens: ${en.saved} ${stats.savedTokens} tk (${stats.savedPercent}%)` : 'Context Lens: Active',
|
|
217
232
|
onClick: () => setOpen((v) => !v),
|
|
218
233
|
style: {
|
|
219
|
-
appearance: 'none',
|
|
220
|
-
|
|
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',
|
|
234
|
+
appearance: 'none', font: 'inherit', cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: 4,
|
|
235
|
+
padding: '2px 8px', borderRadius: 999, fontSize: 11, lineHeight: '16px',
|
|
229
236
|
background: open ? 'var(--dsw-alias-bg-layer-3)' : 'var(--dsw-alias-bg-layer-2)',
|
|
230
|
-
color,
|
|
231
|
-
border: '1px solid ' + borderColor,
|
|
232
|
-
marginLeft: 6
|
|
237
|
+
color, border: '1px solid ' + borderColor, marginLeft: 6
|
|
233
238
|
}
|
|
234
239
|
},
|
|
235
240
|
React.createElement('span', { style: { fontSize: 10, opacity: 0.8 } }, '◐'),
|
|
@@ -243,13 +248,14 @@ window.__ModuleLoader__.load({
|
|
|
243
248
|
function PluginCard({ ctx: _ctx, t }) {
|
|
244
249
|
const [expanded, setExpanded] = React.useState(false);
|
|
245
250
|
// hooks must be before any return — React 310
|
|
246
|
-
const [draft, setDraft] = React.useState({ compressionMode: 'balanced', astSkeletonMaxDepth: 3, tokenSavingsTracking: true, budgetLimit: 100000, autoCollapse: true });
|
|
251
|
+
const [draft, setDraft] = React.useState({ compressionMode: 'balanced', astSkeletonMaxDepth: 3, tokenSavingsTracking: true, budgetLimit: 100000, autoCollapse: true, autoCompressThreshold: 4000 });
|
|
247
252
|
const [status, setStatus] = React.useState('loading');
|
|
248
253
|
const [saving, setSaving] = React.useState(false);
|
|
249
254
|
const [saveErr, setSaveErr] = React.useState('');
|
|
250
255
|
const [previewIn, setPreviewIn] = React.useState('FAIL src/app.test.js\n ● should handle\n Expected 1 got 2\n at Object.<anonymous> (src/app.test.js:10:5)\nPASS src/ok.test.js\n');
|
|
251
256
|
const [previewOut, setPreviewOut] = React.useState('');
|
|
252
257
|
const [stats, setStats] = React.useState(null);
|
|
258
|
+
const [budgetWarn, setBudgetWarn] = React.useState(false);
|
|
253
259
|
|
|
254
260
|
const scopeRef = React.useRef(null);
|
|
255
261
|
if (!scopeRef.current && _ctx && _ctx.settingsScope) {
|
|
@@ -283,16 +289,15 @@ window.__ModuleLoader__.load({
|
|
|
283
289
|
}, [scope]);
|
|
284
290
|
|
|
285
291
|
React.useEffect(() => {
|
|
286
|
-
if (expanded)
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
}, [expanded]);
|
|
292
|
+
if (!expanded) return;
|
|
293
|
+
fetch('/dsh-context-lens/status').then((r) => r.ok ? r.json() : null).then((j) => {
|
|
294
|
+
if (j && j.stats) {
|
|
295
|
+
setStats(j.stats);
|
|
296
|
+
// #41: warn instead of force-closing the card
|
|
297
|
+
setBudgetWarn(!!(j.stats.lowBudget && draft.autoCollapse !== false));
|
|
298
|
+
}
|
|
299
|
+
}).catch(() => {});
|
|
300
|
+
}, [expanded, draft.autoCollapse]);
|
|
296
301
|
|
|
297
302
|
const tt = t || ((k) => (en[k] || k));
|
|
298
303
|
|
|
@@ -320,9 +325,18 @@ window.__ModuleLoader__.load({
|
|
|
320
325
|
if (errs.length) setSaveErr(errs.join('; '));
|
|
321
326
|
}
|
|
322
327
|
|
|
323
|
-
function onPreview() {
|
|
324
|
-
|
|
325
|
-
|
|
328
|
+
async function onPreview() {
|
|
329
|
+
try {
|
|
330
|
+
const res = await fetch('/dsh-context-lens/compress-preview', {
|
|
331
|
+
method: 'POST',
|
|
332
|
+
headers: { 'content-type': 'application/json' },
|
|
333
|
+
body: JSON.stringify({ text: previewIn, mode: draft.compressionMode || 'balanced' })
|
|
334
|
+
});
|
|
335
|
+
const j = await res.json();
|
|
336
|
+
setPreviewOut((j && j.compressed) || (j && j.error) || '');
|
|
337
|
+
} catch (e) {
|
|
338
|
+
setPreviewOut(String(e && e.message || e));
|
|
339
|
+
}
|
|
326
340
|
}
|
|
327
341
|
|
|
328
342
|
// styles: ponytail minimal, theme vars only
|
|
@@ -340,6 +354,7 @@ window.__ModuleLoader__.load({
|
|
|
340
354
|
React.createElement(Chevron, { open: expanded })
|
|
341
355
|
),
|
|
342
356
|
expanded ? React.createElement('div', { className: 'cl-body', style: { borderTop: '1px solid var(--dsw-alias-border-l2)', margin: '0 16px', paddingBottom: 8 } },
|
|
357
|
+
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,
|
|
343
358
|
status === 'loading' ? React.createElement('div', { style: { padding: 12, color: 'var(--dsw-alias-label-secondary)' } }, 'Loading…') :
|
|
344
359
|
status === 'unavailable' ? React.createElement('div', { style: { padding: 12, color: 'var(--dsw-alias-label-secondary)' } }, 'Settings unavailable — plugin not registered on host yet') :
|
|
345
360
|
React.createElement(React.Fragment, null,
|
|
@@ -375,8 +390,35 @@ window.__ModuleLoader__.load({
|
|
|
375
390
|
}),
|
|
376
391
|
React.createElement('label', { htmlFor: 'cl-tracking', style: { fontSize: 13 } }, tt('tracking'))
|
|
377
392
|
),
|
|
393
|
+
React.createElement('div', { className: 'cl-field', style: { display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 8, padding: '12px 0' } },
|
|
394
|
+
React.createElement('input', {
|
|
395
|
+
type: 'checkbox',
|
|
396
|
+
checked: !!draft.autoCollapse,
|
|
397
|
+
onChange: (e) => setDraft((d) => ({ ...d, autoCollapse: e.target.checked })),
|
|
398
|
+
id: 'cl-autocollapse'
|
|
399
|
+
}),
|
|
400
|
+
React.createElement('label', { htmlFor: 'cl-autocollapse', style: { fontSize: 13 } }, tt('autoCollapse'))
|
|
401
|
+
),
|
|
402
|
+
React.createElement('div', { className: 'cl-field', style: { display: 'flex', flexDirection: 'column', gap: 6, padding: '12px 0' } },
|
|
403
|
+
React.createElement('label', { style: { fontSize: 13, color: 'var(--dsw-alias-label-secondary)' } }, tt('budget') + ' (tokens)'),
|
|
404
|
+
React.createElement('input', {
|
|
405
|
+
className: 'cl-input', type: 'number', min: 1000, step: 1000,
|
|
406
|
+
value: draft.budgetLimit,
|
|
407
|
+
onChange: (e) => setDraft((d) => ({ ...d, budgetLimit: parseInt(e.target.value, 10) || 100000 })),
|
|
408
|
+
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 }
|
|
409
|
+
})
|
|
410
|
+
),
|
|
411
|
+
React.createElement('div', { className: 'cl-field', style: { display: 'flex', flexDirection: 'column', gap: 6, padding: '12px 0' } },
|
|
412
|
+
React.createElement('label', { style: { fontSize: 13, color: 'var(--dsw-alias-label-secondary)' } }, tt('threshold')),
|
|
413
|
+
React.createElement('input', {
|
|
414
|
+
className: 'cl-input', type: 'number', min: 0, step: 500,
|
|
415
|
+
value: draft.autoCompressThreshold,
|
|
416
|
+
onChange: (e) => setDraft((d) => ({ ...d, autoCompressThreshold: parseInt(e.target.value, 10) || 0 })),
|
|
417
|
+
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 }
|
|
418
|
+
})
|
|
419
|
+
),
|
|
378
420
|
React.createElement('div', { className: 'cl-field', style: { display: 'flex', flexDirection: 'column', gap: 6, padding: '12px 0' } },
|
|
379
|
-
React.createElement('label', { style: { fontSize: 13, color: 'var(--dsw-alias-label-secondary)' } }, tt('preview')),
|
|
421
|
+
React.createElement('label', { style: { fontSize: 13, color: 'var(--dsw-alias-label-secondary)' } }, tt('preview') + ' (' + tt('previewApprox') + ')'),
|
|
380
422
|
React.createElement('textarea', {
|
|
381
423
|
value: previewIn,
|
|
382
424
|
onChange: (e) => setPreviewIn(e.target.value),
|
|
@@ -404,7 +446,7 @@ window.__ModuleLoader__.load({
|
|
|
404
446
|
);
|
|
405
447
|
}
|
|
406
448
|
|
|
407
|
-
module.exports.inject = ['slots', 'locale'
|
|
449
|
+
module.exports.inject = ['slots', 'locale'];
|
|
408
450
|
module.exports.apply = function apply(ctx) {
|
|
409
451
|
try { ctx.locale.register(NS, { en, ru }); } catch (e) { console.warn('[dsh-context-lens] locale register failed', e && e.message || e); }
|
|
410
452
|
if (!ctx.slots) return;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { estimateTokens } from '../tokens/estimate.js';
|
|
1
2
|
// ponytail: heuristic line filter, not ML — O(n) scan, no deps
|
|
2
3
|
const ANSI_RE = /\u001b\[[0-9;]*[a-zA-Z]/g;
|
|
3
4
|
const KEEP_RE = /(FAIL|FAILED|Error|AssertionError|Exception|Traceback|panic|npm ERR!|ERR!|Expected|Received|missing|×|●|✕|FAIL:|--- FAIL|not ok|at\s+.*:\d+:\d+|stack|Caused by|test result:\s*FAILED|running \d+ test|test .* \.\.\. FAILED|BUILD FAILED|Tests run:|FAILURE:|cargo:.*error|error\[E\d+\]|thread '.*' panicked)/i;
|
|
@@ -15,10 +16,6 @@ function keepLine(line) {
|
|
|
15
16
|
return false;
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
export function estimateTokens(text) {
|
|
19
|
-
return Math.ceil((text || '').length / 4);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
19
|
export function compressLog(text, { mode = 'balanced', maxLines = 400 } = {}) {
|
|
23
20
|
if (!text || typeof text !== 'string') return { compressed: '', originalTokens: 0, compressedTokens: 0, savedTokens: 0, savedPercent: 0, keptLines: 0, totalLines: 0 };
|
|
24
21
|
const lines = text.split(/\r?\n/);
|
|
@@ -91,4 +88,5 @@ export function compressLog(text, { mode = 'balanced', maxLines = 400 } = {}) {
|
|
|
91
88
|
return { compressed, originalTokens, compressedTokens, savedTokens: Math.max(0, originalTokens - compressedTokens), savedPercent: originalTokens ? Math.round((1 - compressedTokens / originalTokens) * 100) : 0, keptLines: out.length, totalLines };
|
|
92
89
|
}
|
|
93
90
|
|
|
91
|
+
export { estimateTokens };
|
|
94
92
|
export default { compressLog, estimateTokens };
|
package/lib/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import z from '@deepseek-ai/schemastery';
|
|
|
2
2
|
import { compressLog, estimateTokens as estLog } from './compression/log-compressor.js';
|
|
3
3
|
import { skeletonize } from './ast/skeletonizer.js';
|
|
4
4
|
import * as tracker from './tokens/tracker.js';
|
|
5
|
+
import { shouldAutoCompress } from './auto-compress.js';
|
|
5
6
|
|
|
6
7
|
export const name = '@goodandready/dsh-context-lens';
|
|
7
8
|
export const inject = ['tools', 'settings', 'webServer'];
|
|
@@ -11,23 +12,31 @@ export const Config = z.object({
|
|
|
11
12
|
astSkeletonMaxDepth: z.number().default(3).description('Max depth for AST skeleton generation'),
|
|
12
13
|
tokenSavingsTracking: z.boolean().default(true).description('Track and display token budget savings'),
|
|
13
14
|
autoCompressThreshold: z.number().default(4000).description('Auto-compress threshold in chars (0 to disable)'),
|
|
14
|
-
budgetLimit: z.number().default(100000).description('Session token budget
|
|
15
|
-
autoCollapse: z.boolean().default(true).description('
|
|
15
|
+
budgetLimit: z.number().default(100000).description('Session token budget; compressions stop counting after this limit'),
|
|
16
|
+
autoCollapse: z.boolean().default(true).description('Warn in UI when budget is nearly exhausted (does not force-close the settings card)')
|
|
16
17
|
});
|
|
17
18
|
|
|
18
19
|
const NS = '@goodandready/dsh-context-lens';
|
|
19
20
|
|
|
20
|
-
//
|
|
21
|
-
|
|
21
|
+
// #37/#18: focus keyed by session id (not process-global singleton)
|
|
22
|
+
const focusBySession = new Map();
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
function sessionKey(params, meta) {
|
|
25
|
+
return (
|
|
26
|
+
(params && (params.sessionId || params.session_id)) ||
|
|
27
|
+
(meta && (meta.sessionId || meta.session_id || (meta.session && meta.session.id))) ||
|
|
28
|
+
'__default__'
|
|
29
|
+
);
|
|
30
|
+
}
|
|
25
31
|
|
|
26
|
-
function
|
|
27
|
-
if (!
|
|
28
|
-
return
|
|
32
|
+
function getFocus(key) {
|
|
33
|
+
if (!focusBySession.has(key)) focusBySession.set(key, { paths: [], updatedAt: null });
|
|
34
|
+
return focusBySession.get(key);
|
|
29
35
|
}
|
|
30
36
|
|
|
37
|
+
const OUTPUT_SCHEMA = { type: 'object', properties: { success: { type: 'boolean' } }, additionalProperties: true };
|
|
38
|
+
const renderOutput = (_args, result) => JSON.stringify(result, null, 2);
|
|
39
|
+
|
|
31
40
|
export function apply(ctx, config) {
|
|
32
41
|
let getConfig = () => config;
|
|
33
42
|
|
|
@@ -36,32 +45,38 @@ export function apply(ctx, config) {
|
|
|
36
45
|
getConfig = () => scope.get() ?? config;
|
|
37
46
|
});
|
|
38
47
|
|
|
39
|
-
// helper to record savings if enabled
|
|
40
48
|
function maybeTrack(original, compressed) {
|
|
41
49
|
try {
|
|
42
50
|
const cfg = getConfig();
|
|
43
51
|
if (cfg && cfg.tokenSavingsTracking === false) return null;
|
|
52
|
+
if (cfg && typeof cfg.budgetLimit === 'number') tracker.setBudgetLimit(cfg.budgetLimit);
|
|
44
53
|
return tracker.record(original, compressed);
|
|
45
|
-
} catch {
|
|
54
|
+
} catch {
|
|
55
|
+
// #39: never record on config/read errors
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
46
58
|
}
|
|
47
59
|
|
|
48
60
|
if (ctx.tools) {
|
|
49
61
|
ctx.tools.register({
|
|
50
62
|
name: 'context_lens_focus',
|
|
51
|
-
description: 'Set focus files/folders; other context will be auto-collapsed via skeletonizer',
|
|
63
|
+
description: 'Set focus files/folders for a session; other context will be auto-collapsed via skeletonizer',
|
|
52
64
|
parameters: {
|
|
53
65
|
type: 'object',
|
|
54
66
|
properties: {
|
|
55
67
|
paths: { type: 'array', items: { type: 'string' }, description: 'Focused file/folder paths' },
|
|
56
|
-
maxDepth: { type: 'number', description: 'Max skeleton depth (overrides settings)' }
|
|
68
|
+
maxDepth: { type: 'number', description: 'Max skeleton depth (overrides settings)' },
|
|
69
|
+
sessionId: { type: 'string', description: 'Session id (defaults to host session when available)' }
|
|
57
70
|
},
|
|
58
71
|
required: ['paths']
|
|
59
72
|
},
|
|
60
73
|
output: { schema: OUTPUT_SCHEMA, render: renderOutput },
|
|
61
|
-
execute: async (params) => {
|
|
74
|
+
execute: async (params, meta) => {
|
|
75
|
+
const key = sessionKey(params, meta);
|
|
62
76
|
const paths = Array.isArray(params.paths) ? params.paths : [];
|
|
63
|
-
|
|
64
|
-
|
|
77
|
+
const focus = { paths, updatedAt: new Date().toISOString() };
|
|
78
|
+
focusBySession.set(key, focus);
|
|
79
|
+
return { success: true, sessionId: key, focus, hint: 'Use skeletonize helper via compress or read files with focus set' };
|
|
65
80
|
}
|
|
66
81
|
});
|
|
67
82
|
|
|
@@ -75,7 +90,7 @@ export function apply(ctx, config) {
|
|
|
75
90
|
log: { type: 'string', description: 'Alias for text' },
|
|
76
91
|
mode: { type: 'string', enum: ['raw', 'balanced', 'aggressive'], description: 'Compression aggressiveness' },
|
|
77
92
|
maxLines: { type: 'number', description: 'Max output lines' },
|
|
78
|
-
auto: { type: 'boolean', description: 'Auto-compress if large (uses threshold)' }
|
|
93
|
+
auto: { type: 'boolean', description: 'Auto-compress if large (uses threshold); keeps configured compressionMode' }
|
|
79
94
|
}
|
|
80
95
|
},
|
|
81
96
|
output: { schema: OUTPUT_SCHEMA, render: renderOutput },
|
|
@@ -84,10 +99,10 @@ export function apply(ctx, config) {
|
|
|
84
99
|
const inputText = params.text || params.log || '';
|
|
85
100
|
const mode = params.mode || cfg.compressionMode || 'balanced';
|
|
86
101
|
const maxLines = params.maxLines || 400;
|
|
87
|
-
// Auto-compress check (#15)
|
|
88
102
|
const threshold = cfg.autoCompressThreshold ?? 4000;
|
|
89
103
|
const useAuto = params.auto !== false && shouldAutoCompress(inputText, threshold);
|
|
90
|
-
|
|
104
|
+
// #33: never force balanced — keep configured/explicit mode
|
|
105
|
+
const effectiveMode = mode;
|
|
91
106
|
const res = compressLog(inputText, { mode: effectiveMode, maxLines });
|
|
92
107
|
maybeTrack(inputText, res.compressed);
|
|
93
108
|
return { success: true, mode: effectiveMode, autoCompressed: useAuto, ...res };
|
|
@@ -103,51 +118,100 @@ export function apply(ctx, config) {
|
|
|
103
118
|
code: { type: 'string', description: 'Source code to skeletonize' },
|
|
104
119
|
language: { type: 'string', enum: ['js', 'ts', 'py', 'python', 'go', 'rust', 'java'], description: 'Language hint' },
|
|
105
120
|
maxDepth: { type: 'number', description: 'Max depth' },
|
|
106
|
-
filePath: { type: 'string', description: 'File path to check focus (if in focus, returns full code)' }
|
|
121
|
+
filePath: { type: 'string', description: 'File path to check focus (if in focus, returns full code)' },
|
|
122
|
+
sessionId: { type: 'string', description: 'Session id for focus lookup' }
|
|
107
123
|
},
|
|
108
124
|
required: ['code']
|
|
109
125
|
},
|
|
110
126
|
output: { schema: OUTPUT_SCHEMA, render: renderOutput },
|
|
111
|
-
execute: async (params) => {
|
|
127
|
+
execute: async (params, meta) => {
|
|
112
128
|
const cfg = getConfig();
|
|
113
129
|
const maxDepth = params.maxDepth ?? cfg.astSkeletonMaxDepth ?? 3;
|
|
114
|
-
|
|
115
|
-
const
|
|
130
|
+
const key = sessionKey(params, meta);
|
|
131
|
+
const focusState = getFocus(key);
|
|
132
|
+
const isFocused = params.filePath && focusState.paths.length > 0
|
|
133
|
+
? focusState.paths.some(p => params.filePath.includes(p) || p.includes(params.filePath))
|
|
134
|
+
: false;
|
|
116
135
|
if (isFocused) {
|
|
117
136
|
maybeTrack(params.code, params.code);
|
|
118
|
-
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' };
|
|
137
|
+
return { success: true, skeleton: params.code, originalTokens: estLog(params.code), skeletonTokens: estLog(params.code), maxDepth, focused: true, sessionId: key, hint: 'File is in focus, returned full code' };
|
|
119
138
|
}
|
|
120
139
|
const skeleton = skeletonize(params.code, { maxDepth, language: params.language });
|
|
121
140
|
maybeTrack(params.code, skeleton);
|
|
122
|
-
return { success: true, skeleton, originalTokens: estLog(params.code), skeletonTokens: estLog(skeleton), maxDepth, focused: false };
|
|
141
|
+
return { success: true, skeleton, originalTokens: estLog(params.code), skeletonTokens: estLog(skeleton), maxDepth, focused: false, sessionId: key };
|
|
123
142
|
}
|
|
124
143
|
});
|
|
125
144
|
|
|
126
145
|
ctx.tools.register({
|
|
127
146
|
name: 'context_lens_stats',
|
|
128
147
|
description: 'Show session token savings stats for context-lens',
|
|
129
|
-
parameters: {
|
|
148
|
+
parameters: {
|
|
149
|
+
type: 'object',
|
|
150
|
+
properties: {
|
|
151
|
+
sessionId: { type: 'string', description: 'Session id for focus snapshot' }
|
|
152
|
+
}
|
|
153
|
+
},
|
|
130
154
|
output: { schema: OUTPUT_SCHEMA, render: renderOutput },
|
|
131
|
-
execute: async () => {
|
|
155
|
+
execute: async (params, meta) => {
|
|
132
156
|
const cfg = getConfig();
|
|
157
|
+
if (typeof cfg.budgetLimit === 'number') tracker.setBudgetLimit(cfg.budgetLimit);
|
|
133
158
|
const stats = tracker.getStats(cfg.budgetLimit);
|
|
134
159
|
const history = tracker.getHistory();
|
|
135
|
-
|
|
160
|
+
const key = sessionKey(params || {}, meta);
|
|
161
|
+
return {
|
|
162
|
+
success: true,
|
|
163
|
+
...stats,
|
|
164
|
+
history,
|
|
165
|
+
trackingEnabled: cfg.tokenSavingsTracking !== false,
|
|
166
|
+
autoCollapse: cfg.autoCollapse !== false,
|
|
167
|
+
focus: getFocus(key),
|
|
168
|
+
sessionId: key,
|
|
169
|
+
autoCompressThreshold: cfg.autoCompressThreshold
|
|
170
|
+
};
|
|
136
171
|
}
|
|
137
172
|
});
|
|
138
173
|
}
|
|
139
174
|
|
|
140
|
-
// optional status route
|
|
141
175
|
if (ctx.webServer) {
|
|
142
176
|
ctx.effect(() => ctx.webServer.register({
|
|
143
177
|
kind: 'exact',
|
|
144
178
|
path: '/dsh-context-lens/status',
|
|
145
179
|
handler: (req, res) => {
|
|
146
180
|
const cfg = getConfig();
|
|
181
|
+
if (typeof cfg.budgetLimit === 'number') tracker.setBudgetLimit(cfg.budgetLimit);
|
|
147
182
|
res.setHeader('content-type', 'application/json');
|
|
148
|
-
res.end(JSON.stringify({
|
|
183
|
+
res.end(JSON.stringify({
|
|
184
|
+
ok: true,
|
|
185
|
+
plugin: 'dsh-context-lens',
|
|
186
|
+
stats: tracker.getStats(cfg.budgetLimit),
|
|
187
|
+
history: tracker.getHistory(),
|
|
188
|
+
focus: getFocus('__default__')
|
|
189
|
+
}));
|
|
149
190
|
}
|
|
150
191
|
}), 'dsh-context-lens status route');
|
|
192
|
+
|
|
193
|
+
// #45: server-backed preview (same compressor as tools)
|
|
194
|
+
ctx.effect(() => ctx.webServer.register({
|
|
195
|
+
kind: 'exact',
|
|
196
|
+
path: '/dsh-context-lens/compress-preview',
|
|
197
|
+
handler: async (req, res) => {
|
|
198
|
+
try {
|
|
199
|
+
const chunks = [];
|
|
200
|
+
for await (const c of req) chunks.push(c);
|
|
201
|
+
const body = JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}');
|
|
202
|
+
const cfg = getConfig();
|
|
203
|
+
const mode = body.mode || cfg.compressionMode || 'balanced';
|
|
204
|
+
const text = body.text || '';
|
|
205
|
+
const out = compressLog(text, { mode, maxLines: body.maxLines || 400 });
|
|
206
|
+
res.setHeader('content-type', 'application/json');
|
|
207
|
+
res.end(JSON.stringify({ ok: true, mode, ...out }));
|
|
208
|
+
} catch (e) {
|
|
209
|
+
res.statusCode = 400;
|
|
210
|
+
res.setHeader('content-type', 'application/json');
|
|
211
|
+
res.end(JSON.stringify({ ok: false, error: e && e.message || String(e) }));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}), 'dsh-context-lens compress-preview route');
|
|
151
215
|
}
|
|
152
216
|
}
|
|
153
217
|
|
package/lib/tokens/tracker.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
// ponytail: in-memory per-process tracker + history, no DB
|
|
2
|
+
import { estimateTokens } from './estimate.js';
|
|
3
|
+
|
|
2
4
|
let totalOriginal = 0;
|
|
3
5
|
let totalCompressed = 0;
|
|
4
6
|
let calls = 0;
|
|
5
7
|
const history = []; // last 10
|
|
6
8
|
const MAX_HISTORY = 10;
|
|
7
|
-
let defaultBudgetLimit = 100000; // tokens
|
|
9
|
+
let defaultBudgetLimit = 100000; // tokens
|
|
8
10
|
|
|
9
|
-
export
|
|
10
|
-
return Math.ceil((text || '').length / 4);
|
|
11
|
-
}
|
|
11
|
+
export { estimateTokens };
|
|
12
12
|
|
|
13
13
|
export function setBudgetLimit(limit) {
|
|
14
14
|
if (typeof limit === 'number' && limit > 0) {
|
|
@@ -20,6 +20,10 @@ export function record(originalText, compressedText) {
|
|
|
20
20
|
const o = estimateTokens(originalText);
|
|
21
21
|
const c = estimateTokens(compressedText);
|
|
22
22
|
const saved = Math.max(0, o - c);
|
|
23
|
+
// #34: stop counting after budgetLimit (description promised this)
|
|
24
|
+
if (totalCompressed >= defaultBudgetLimit) {
|
|
25
|
+
return { originalTokens: o, compressedTokens: c, savedTokens: 0, stopped: true };
|
|
26
|
+
}
|
|
23
27
|
totalOriginal += o;
|
|
24
28
|
totalCompressed += c;
|
|
25
29
|
calls++;
|
|
@@ -57,7 +61,8 @@ export function getStats(budgetLimitOverride) {
|
|
|
57
61
|
budgetRemaining,
|
|
58
62
|
budgetPercent,
|
|
59
63
|
lowBudget,
|
|
60
|
-
budgetLimit: limit
|
|
64
|
+
budgetLimit: limit,
|
|
65
|
+
countingStopped: budgetUsed >= limit
|
|
61
66
|
};
|
|
62
67
|
}
|
|
63
68
|
|
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.12",
|
|
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",
|
|
@@ -50,7 +50,6 @@
|
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
52
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
53
|
-
"@deepseek-ai/dsh-credentials": "^0.1.0-rc.6",
|
|
54
53
|
"@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6",
|
|
55
54
|
"@deepseek-ai/dsh-settings": "^0.1.0-rc.6",
|
|
56
55
|
"@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
|