dsh-context-fold 0.1.3 → 0.1.5
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.d.ts +0 -32
- package/lib/client.js +502 -177
- package/lib/index.js +13 -6
- package/package.json +1 -1
package/lib/client.d.ts
CHANGED
|
@@ -3,38 +3,6 @@ window.__ModuleLoader__.load({
|
|
|
3
3
|
factory: (require) => {
|
|
4
4
|
var module = { exports: {} };
|
|
5
5
|
var exports = module.exports;
|
|
6
|
-
/**
|
|
7
|
-
* `conversation.view` tab: what this plugin folded, what it saved, and the full
|
|
8
|
-
* text behind each fold.
|
|
9
|
-
*
|
|
10
|
-
* ## Why the page reads from the server route, not the session log
|
|
11
|
-
*
|
|
12
|
-
* The browser client cannot walk a session log: the fold decisions are durable
|
|
13
|
-
* plugin records, and the full original reasoning lives in the log's assistant
|
|
14
|
-
* events. Both are server-side. The tab therefore renders exactly what
|
|
15
|
-
* `/api/context-fold/report` returns and does no derivation of its own.
|
|
16
|
-
*
|
|
17
|
-
* ## What it shows, and how honestly
|
|
18
|
-
*
|
|
19
|
-
* The token comparison is a **counterfactual estimate**, not a measured A/B.
|
|
20
|
-
* The "before" case never ran. The panel says so in the UI, lists the
|
|
21
|
-
* assumptions the number depends on, and marks whether the session reported real
|
|
22
|
-
* provider usage at all — because a number built on heuristics must not look
|
|
23
|
-
* like a bill.
|
|
24
|
-
*
|
|
25
|
-
* The original text of every fold is shown on demand, fetched by its recovery
|
|
26
|
-
* path, so the tab doubles as the answer to "what exactly did it remove".
|
|
27
|
-
*
|
|
28
|
-
* ## How this file is shipped
|
|
29
|
-
*
|
|
30
|
-
* `tsdown` wraps the output in the shell's module-factory form
|
|
31
|
-
* (`window.__ModuleLoader__.load({id, factory(require){…}})`), which is the only
|
|
32
|
-
* shape the client module system executes. Imports are therefore converted into
|
|
33
|
-
* `require` calls resolved against the shared platform module table, and the
|
|
34
|
-
* plugin's exports (`name`/`inject`/`apply`) are what the factory returns.
|
|
35
|
-
*
|
|
36
|
-
* @module dsh-context-fold/client
|
|
37
|
-
*/
|
|
38
6
|
/** One fold row as the server reports it. */
|
|
39
7
|
interface FoldRow {
|
|
40
8
|
/** Surface seq. */
|
package/lib/client.js
CHANGED
|
@@ -4,7 +4,380 @@ window.__ModuleLoader__.load({
|
|
|
4
4
|
var module = { exports: {} };
|
|
5
5
|
var exports = module.exports;
|
|
6
6
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
// src/panel-css.ts
|
|
8
|
+
/**
|
|
9
|
+
* The conversation tab's stylesheet.
|
|
10
|
+
*
|
|
11
|
+
* ## Why a stylesheet instead of inline styles
|
|
12
|
+
*
|
|
13
|
+
* The first version rendered with inline `style` objects: no hover states, no
|
|
14
|
+
* dark-theme variants, no transitions, and a monospace wall that ignored the
|
|
15
|
+
* shell's design language. The shell already ships a token layer
|
|
16
|
+
* (`--dsw-alias-*` for colour, `--ds-transition-duration` / `--ds-ease-in-out`
|
|
17
|
+
* for motion) plus a `data-ds-dark-theme` body attribute, so the tab should
|
|
18
|
+
* consume those instead of inventing a palette.
|
|
19
|
+
*
|
|
20
|
+
* ## How it reaches the page
|
|
21
|
+
*
|
|
22
|
+
* A client bundle cannot import a CSS file — the loader executes the bundle as a
|
|
23
|
+
* classic script — so the sheet is injected once at module scope as
|
|
24
|
+
* `<style data-plugin-css>`. The `data-plugin-css` marker makes the injection
|
|
25
|
+
* idempotent across HMR and re-mounts, which is the same mechanism the bundled
|
|
26
|
+
* first-party plugins use.
|
|
27
|
+
*
|
|
28
|
+
* @module dsh-context-fold/panel-css
|
|
29
|
+
*/
|
|
30
|
+
/** Unique key so the sheet is injected at most once per document. */
|
|
31
|
+
const CSS_KEY = "dsh-context-fold/panel.css";
|
|
32
|
+
/**
|
|
33
|
+
* Every rule is scoped under `.cf-`, so nothing here can leak into the shell.
|
|
34
|
+
*
|
|
35
|
+
* Colours come from the alias tokens with literal fallbacks: if a token is ever
|
|
36
|
+
* renamed, the tab still renders legibly rather than going transparent.
|
|
37
|
+
*/
|
|
38
|
+
const CSS = `
|
|
39
|
+
.cf-root {
|
|
40
|
+
box-sizing: border-box;
|
|
41
|
+
display: flex;
|
|
42
|
+
flex-direction: column;
|
|
43
|
+
gap: 14px;
|
|
44
|
+
height: 100%;
|
|
45
|
+
overflow-y: auto;
|
|
46
|
+
padding: 16px 20px 32px;
|
|
47
|
+
font-size: 13px;
|
|
48
|
+
line-height: 1.55;
|
|
49
|
+
color: var(--dsw-alias-label-primary, #e8e8ea);
|
|
50
|
+
background: var(--dsw-alias-bg-base, transparent);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.cf-head {
|
|
54
|
+
display: flex;
|
|
55
|
+
align-items: baseline;
|
|
56
|
+
gap: 10px;
|
|
57
|
+
flex-wrap: wrap;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.cf-head-title {
|
|
61
|
+
font-size: 15px;
|
|
62
|
+
font-weight: 600;
|
|
63
|
+
letter-spacing: .01em;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.cf-head-note {
|
|
67
|
+
font-size: 12px;
|
|
68
|
+
color: var(--dsw-alias-label-tertiary, #8b8b93);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.cf-card {
|
|
72
|
+
border: .5px solid var(--dsw-alias-border-l3, #3a3a40);
|
|
73
|
+
border-radius: 10px;
|
|
74
|
+
background: var(--dsw-alias-bg-layer-1, #1c1c20);
|
|
75
|
+
padding: 14px 16px;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.cf-card-title {
|
|
79
|
+
display: flex;
|
|
80
|
+
align-items: center;
|
|
81
|
+
gap: 8px;
|
|
82
|
+
margin-bottom: 12px;
|
|
83
|
+
font-size: 12px;
|
|
84
|
+
font-weight: 600;
|
|
85
|
+
letter-spacing: .04em;
|
|
86
|
+
text-transform: uppercase;
|
|
87
|
+
color: var(--dsw-alias-label-secondary, #a9a9b2);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.cf-grid {
|
|
91
|
+
display: grid;
|
|
92
|
+
grid-template-columns: repeat(auto-fit, minmax(132px, 1fr));
|
|
93
|
+
gap: 10px;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.cf-metric {
|
|
97
|
+
border: .5px solid var(--dsw-alias-border-l2, #303036);
|
|
98
|
+
border-radius: 8px;
|
|
99
|
+
background: var(--dsw-alias-bg-layer-2, #202024);
|
|
100
|
+
padding: 10px 12px;
|
|
101
|
+
transition: background-color var(--ds-transition-duration, .2s) var(--ds-ease-in-out, ease-in-out),
|
|
102
|
+
border-color var(--ds-transition-duration, .2s) var(--ds-ease-in-out, ease-in-out);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.cf-metric:hover {
|
|
106
|
+
background: var(--dsw-alias-interactive-bg-hover, #26262c);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.cf-metric-label {
|
|
110
|
+
font-size: 11px;
|
|
111
|
+
color: var(--dsw-alias-label-tertiary, #8b8b93);
|
|
112
|
+
margin-bottom: 4px;
|
|
113
|
+
white-space: nowrap;
|
|
114
|
+
overflow: hidden;
|
|
115
|
+
text-overflow: ellipsis;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.cf-metric-value {
|
|
119
|
+
font-size: 18px;
|
|
120
|
+
font-weight: 600;
|
|
121
|
+
font-variant-numeric: tabular-nums;
|
|
122
|
+
line-height: 1.2;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.cf-metric-sub {
|
|
126
|
+
margin-top: 2px;
|
|
127
|
+
font-size: 11px;
|
|
128
|
+
color: var(--dsw-alias-label-tertiary, #8b8b93);
|
|
129
|
+
font-variant-numeric: tabular-nums;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.cf-good { color: var(--dsw-alias-state-success-primary, #4ade80); }
|
|
133
|
+
.cf-bad { color: var(--dsw-alias-state-error-primary, #f87171); }
|
|
134
|
+
.cf-warn { color: var(--dsw-alias-state-warn-primary, #facc15); }
|
|
135
|
+
.cf-dim { color: var(--dsw-alias-label-tertiary, #8b8b93); }
|
|
136
|
+
|
|
137
|
+
.cf-bar {
|
|
138
|
+
display: flex;
|
|
139
|
+
height: 8px;
|
|
140
|
+
overflow: hidden;
|
|
141
|
+
border-radius: 999px;
|
|
142
|
+
background: var(--dsw-alias-bg-layer-3, #2a2a30);
|
|
143
|
+
margin-top: 10px;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.cf-bar-seg {
|
|
147
|
+
height: 100%;
|
|
148
|
+
transition: width var(--ds-transition-duration, .2s) var(--ds-ease-in-out, ease-in-out);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.cf-bar-legend {
|
|
152
|
+
display: flex;
|
|
153
|
+
flex-wrap: wrap;
|
|
154
|
+
gap: 14px;
|
|
155
|
+
margin-top: 10px;
|
|
156
|
+
font-size: 11px;
|
|
157
|
+
color: var(--dsw-alias-label-secondary, #a9a9b2);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.cf-legend-item {
|
|
161
|
+
display: inline-flex;
|
|
162
|
+
align-items: center;
|
|
163
|
+
gap: 6px;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.cf-dot {
|
|
167
|
+
width: 8px;
|
|
168
|
+
height: 8px;
|
|
169
|
+
border-radius: 2px;
|
|
170
|
+
flex: none;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.cf-notice {
|
|
174
|
+
display: flex;
|
|
175
|
+
gap: 8px;
|
|
176
|
+
border-radius: 8px;
|
|
177
|
+
border: .5px solid var(--dsw-alias-border-l3, #3a3a40);
|
|
178
|
+
background: var(--dsw-alias-bg-layer-2, #202024);
|
|
179
|
+
padding: 10px 12px;
|
|
180
|
+
font-size: 12px;
|
|
181
|
+
line-height: 1.5;
|
|
182
|
+
color: var(--dsw-alias-label-secondary, #a9a9b2);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.cf-notice-strong {
|
|
186
|
+
font-weight: 600;
|
|
187
|
+
color: var(--dsw-alias-label-primary, #e8e8ea);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.cf-notice.cf-notice-warn {
|
|
191
|
+
border-color: var(--dsw-alias-state-warn-primary, #facc15);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.cf-folds {
|
|
195
|
+
display: flex;
|
|
196
|
+
flex-direction: column;
|
|
197
|
+
gap: 8px;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.cf-fold {
|
|
201
|
+
border: .5px solid var(--dsw-alias-border-l2, #303036);
|
|
202
|
+
border-radius: 8px;
|
|
203
|
+
background: var(--dsw-alias-bg-layer-1, #1c1c20);
|
|
204
|
+
overflow: hidden;
|
|
205
|
+
transition: border-color var(--ds-transition-duration, .2s) var(--ds-ease-in-out, ease-in-out);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.cf-fold:hover {
|
|
209
|
+
border-color: var(--dsw-alias-border-l4, #4a4a52);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.cf-fold[open] {
|
|
213
|
+
border-color: var(--dsw-alias-border-l4, #4a4a52);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.cf-fold-head {
|
|
217
|
+
display: flex;
|
|
218
|
+
align-items: center;
|
|
219
|
+
gap: 10px;
|
|
220
|
+
cursor: pointer;
|
|
221
|
+
padding: 9px 12px;
|
|
222
|
+
list-style: none;
|
|
223
|
+
user-select: none;
|
|
224
|
+
transition: background-color var(--ds-transition-duration, .2s) var(--ds-ease-in-out, ease-in-out);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.cf-fold-head::-webkit-details-marker { display: none; }
|
|
228
|
+
|
|
229
|
+
.cf-fold-head:hover {
|
|
230
|
+
background: var(--dsw-alias-interactive-bg-hover, #26262c);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.cf-caret {
|
|
234
|
+
flex: none;
|
|
235
|
+
width: 10px;
|
|
236
|
+
font-size: 9px;
|
|
237
|
+
color: var(--dsw-alias-label-tertiary, #8b8b93);
|
|
238
|
+
transition: transform var(--ds-transition-duration, .2s) var(--ds-ease-in-out, ease-in-out);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.cf-fold[open] .cf-caret { transform: rotate(90deg); }
|
|
242
|
+
|
|
243
|
+
.cf-tag {
|
|
244
|
+
flex: none;
|
|
245
|
+
border-radius: 5px;
|
|
246
|
+
padding: 1px 7px;
|
|
247
|
+
font-size: 11px;
|
|
248
|
+
font-weight: 500;
|
|
249
|
+
line-height: 1.6;
|
|
250
|
+
background: var(--dsw-alias-bg-layer-3, #2a2a30);
|
|
251
|
+
color: var(--dsw-alias-label-secondary, #a9a9b2);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.cf-tag-reasoning {
|
|
255
|
+
background: color-mix(in srgb, var(--dsw-alias-brand-primary, #6b8afd) 18%, transparent);
|
|
256
|
+
color: var(--dsw-alias-brand-primary, #8aa2ff);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.cf-fold-pos {
|
|
260
|
+
flex: none;
|
|
261
|
+
font-size: 11px;
|
|
262
|
+
font-variant-numeric: tabular-nums;
|
|
263
|
+
color: var(--dsw-alias-label-tertiary, #8b8b93);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
.cf-fold-saving {
|
|
267
|
+
margin-left: auto;
|
|
268
|
+
flex: none;
|
|
269
|
+
font-size: 11px;
|
|
270
|
+
font-variant-numeric: tabular-nums;
|
|
271
|
+
color: var(--dsw-alias-state-success-primary, #4ade80);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.cf-fold-body {
|
|
275
|
+
padding: 0 12px 12px;
|
|
276
|
+
border-top: .5px solid var(--dsw-alias-border-l2, #303036);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.cf-label {
|
|
280
|
+
margin: 10px 0 4px;
|
|
281
|
+
font-size: 11px;
|
|
282
|
+
letter-spacing: .04em;
|
|
283
|
+
text-transform: uppercase;
|
|
284
|
+
color: var(--dsw-alias-label-tertiary, #8b8b93);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.cf-pre {
|
|
288
|
+
margin: 0;
|
|
289
|
+
padding: 9px 10px;
|
|
290
|
+
border-radius: 6px;
|
|
291
|
+
background: var(--dsw-alias-bg-layer-3, #2a2a30);
|
|
292
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
293
|
+
font-size: 12px;
|
|
294
|
+
line-height: 1.5;
|
|
295
|
+
white-space: pre-wrap;
|
|
296
|
+
word-break: break-word;
|
|
297
|
+
max-height: 16em;
|
|
298
|
+
overflow: auto;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
.cf-pre-original {
|
|
302
|
+
max-height: 26em;
|
|
303
|
+
border-left: 2px solid var(--dsw-alias-brand-primary, #6b8afd);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.cf-locator {
|
|
307
|
+
margin-top: 8px;
|
|
308
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
309
|
+
font-size: 11px;
|
|
310
|
+
color: var(--dsw-alias-label-tertiary, #8b8b93);
|
|
311
|
+
word-break: break-all;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.cf-empty {
|
|
315
|
+
padding: 24px 4px;
|
|
316
|
+
text-align: center;
|
|
317
|
+
font-size: 12px;
|
|
318
|
+
color: var(--dsw-alias-label-tertiary, #8b8b93);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
.cf-empty-title {
|
|
322
|
+
font-size: 13px;
|
|
323
|
+
font-weight: 600;
|
|
324
|
+
color: var(--dsw-alias-label-secondary, #a9a9b2);
|
|
325
|
+
margin-bottom: 6px;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.cf-aside {
|
|
329
|
+
font-size: 11px;
|
|
330
|
+
color: var(--dsw-alias-label-tertiary, #8b8b93);
|
|
331
|
+
}
|
|
332
|
+
`;
|
|
333
|
+
/**
|
|
334
|
+
* Inject the sheet once.
|
|
335
|
+
*
|
|
336
|
+
* Safe to call on every mount: the `data-plugin-css` key makes a second call a
|
|
337
|
+
* no-op, and a non-browser environment (tests, SSR) simply returns.
|
|
338
|
+
*/
|
|
339
|
+
function ensurePanelCss() {
|
|
340
|
+
if (typeof document === "undefined") return;
|
|
341
|
+
if (document.querySelector(`style[data-plugin-css=${JSON.stringify(CSS_KEY)}]`) !== null) return;
|
|
342
|
+
const style = document.createElement("style");
|
|
343
|
+
style.dataset.plugin = "dsh-context-fold";
|
|
344
|
+
style.dataset.pluginCss = CSS_KEY;
|
|
345
|
+
style.textContent = CSS;
|
|
346
|
+
document.head.appendChild(style);
|
|
347
|
+
}
|
|
7
348
|
// src/client.ts
|
|
349
|
+
/**
|
|
350
|
+
* `conversation.view` tab: what this plugin folded, what it saved, and the full
|
|
351
|
+
* text behind each fold.
|
|
352
|
+
*
|
|
353
|
+
* ## Why the page reads from the server route, not the session log
|
|
354
|
+
*
|
|
355
|
+
* The browser client cannot walk a session log: the fold decisions are durable
|
|
356
|
+
* plugin records, and the full original reasoning lives in the log's assistant
|
|
357
|
+
* events. Both are server-side. The tab therefore renders exactly what
|
|
358
|
+
* `/api/context-fold/report` returns and does no derivation of its own.
|
|
359
|
+
*
|
|
360
|
+
* ## What it shows, and how honestly
|
|
361
|
+
*
|
|
362
|
+
* The token comparison is a **counterfactual estimate**, not a measured A/B.
|
|
363
|
+
* The "before" case never ran. The panel says so in the UI, lists the
|
|
364
|
+
* assumptions the number depends on, and marks whether the session reported real
|
|
365
|
+
* provider usage at all — because a number built on heuristics must not look
|
|
366
|
+
* like a bill.
|
|
367
|
+
*
|
|
368
|
+
* The original text of every fold is shown on demand, fetched by its recovery
|
|
369
|
+
* path, so the tab doubles as the answer to "what exactly did it remove".
|
|
370
|
+
*
|
|
371
|
+
* ## How this file is shipped
|
|
372
|
+
*
|
|
373
|
+
* `tsdown` wraps the output in the shell's module-factory form
|
|
374
|
+
* (`window.__ModuleLoader__.load({id, factory(require){…}})`), which is the only
|
|
375
|
+
* shape the client module system executes. Imports are therefore converted into
|
|
376
|
+
* `require` calls resolved against the shared platform module table, and the
|
|
377
|
+
* plugin's exports (`name`/`inject`/`apply`) are what the factory returns.
|
|
378
|
+
*
|
|
379
|
+
* @module dsh-context-fold/client
|
|
380
|
+
*/
|
|
8
381
|
/** Locale namespace owned by this plugin's client half. */
|
|
9
382
|
const NS = "contextFold";
|
|
10
383
|
/** Dictionary: keep keys aligned with `locale/*.json`. */
|
|
@@ -12,16 +385,7 @@ const zh = { "tab.contextFold": "上下文折叠" };
|
|
|
12
385
|
/** Dictionary: keep keys aligned with `locale/*.json`. */
|
|
13
386
|
const en = { "tab.contextFold": "Context fold" };
|
|
14
387
|
const react = require("react");
|
|
15
|
-
/**
|
|
16
|
-
* Fetch the report for one session.
|
|
17
|
-
*
|
|
18
|
-
* Failures are reported rather than swallowed: before the routes existed the
|
|
19
|
-
* tab rendered "no report available", which reads like "this session folded
|
|
20
|
-
* nothing" instead of "the request never landed".
|
|
21
|
-
*
|
|
22
|
-
* @param sessionId - session to report on.
|
|
23
|
-
* @returns the payload, or a message describing why there is none.
|
|
24
|
-
*/
|
|
388
|
+
/** Fetch the report for one session. */
|
|
25
389
|
async function fetchReport(sessionId) {
|
|
26
390
|
let response;
|
|
27
391
|
try {
|
|
@@ -52,131 +416,112 @@ async function fetchOriginal(sessionId, locator) {
|
|
|
52
416
|
const body = await response.json();
|
|
53
417
|
return body.ok === true ? body.value?.text : void 0;
|
|
54
418
|
}
|
|
55
|
-
/** Format a number
|
|
419
|
+
/** Format a number with thousands separators and at most one decimal. */
|
|
56
420
|
function num(value) {
|
|
57
421
|
return value.toLocaleString("en-US", { maximumFractionDigits: 1 });
|
|
58
422
|
}
|
|
59
|
-
/** A signed figure with an explicit sign, so a loss never
|
|
423
|
+
/** A signed figure with an explicit sign, so a loss never reads as a gain. */
|
|
60
424
|
function signed(value) {
|
|
61
425
|
const rounded = Math.round(value * 10) / 10;
|
|
62
426
|
return `${rounded > 0 ? "+" : ""}${num(rounded)}`;
|
|
63
427
|
}
|
|
428
|
+
/** Compact token count: 1.2M rather than 1200000. */
|
|
429
|
+
function compact(value) {
|
|
430
|
+
if (Math.abs(value) >= 1e6) return `${(value / 1e6).toFixed(2)}M`;
|
|
431
|
+
if (Math.abs(value) >= 1e3) return `${(value / 1e3).toFixed(1)}K`;
|
|
432
|
+
return num(value);
|
|
433
|
+
}
|
|
64
434
|
/** Read the active session id out of the slot props. */
|
|
65
435
|
function sessionIdOf(props) {
|
|
66
436
|
const value = props?.sessionId;
|
|
67
437
|
return typeof value === "string" && value !== "" ? value : void 0;
|
|
68
438
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
},
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
},
|
|
92
|
-
table: { borderCollapse: "collapse" },
|
|
93
|
-
labelCell: {
|
|
94
|
-
padding: "1px 12px 1px 0",
|
|
95
|
-
color: "#aaa"
|
|
96
|
-
},
|
|
97
|
-
warning: {
|
|
98
|
-
marginTop: "6px",
|
|
99
|
-
color: "#facc15"
|
|
100
|
-
},
|
|
101
|
-
error: {
|
|
102
|
-
marginTop: "6px",
|
|
103
|
-
color: "#f87171"
|
|
104
|
-
},
|
|
105
|
-
reread: {
|
|
106
|
-
marginTop: "6px",
|
|
107
|
-
color: "#f87171"
|
|
108
|
-
},
|
|
109
|
-
fold: {
|
|
110
|
-
marginBottom: "6px",
|
|
111
|
-
padding: "6px",
|
|
112
|
-
border: "1px solid #2a2a2a",
|
|
113
|
-
borderRadius: "6px"
|
|
114
|
-
},
|
|
115
|
-
foldSummary: { cursor: "pointer" },
|
|
116
|
-
inlinePre: {
|
|
117
|
-
whiteSpace: "pre-wrap",
|
|
118
|
-
margin: "6px 0",
|
|
119
|
-
color: "#bbb",
|
|
120
|
-
maxHeight: "12em",
|
|
121
|
-
overflow: "auto"
|
|
122
|
-
},
|
|
123
|
-
originalPre: {
|
|
124
|
-
whiteSpace: "pre-wrap",
|
|
125
|
-
margin: "6px 0",
|
|
126
|
-
color: "#ddd",
|
|
127
|
-
maxHeight: "24em",
|
|
128
|
-
overflow: "auto",
|
|
129
|
-
borderTop: "1px dashed #333",
|
|
130
|
-
paddingTop: "6px"
|
|
131
|
-
},
|
|
132
|
-
locator: {
|
|
133
|
-
color: "#666",
|
|
134
|
-
fontSize: "11px",
|
|
135
|
-
wordBreak: "break-all"
|
|
136
|
-
},
|
|
137
|
-
notRecoverable: {
|
|
138
|
-
color: "#f87171",
|
|
139
|
-
fontSize: "11px"
|
|
140
|
-
},
|
|
141
|
-
detailsToggle: {
|
|
142
|
-
cursor: "pointer",
|
|
143
|
-
color: "#888",
|
|
144
|
-
marginTop: "6px"
|
|
145
|
-
},
|
|
146
|
-
assumptionList: {
|
|
147
|
-
margin: "4px 0 0 16px",
|
|
148
|
-
padding: 0,
|
|
149
|
-
color: "#999"
|
|
150
|
-
}
|
|
151
|
-
};
|
|
152
|
-
/** The counterfactual summary block. */
|
|
439
|
+
const el = react.createElement;
|
|
440
|
+
/** One labelled metric tile. */
|
|
441
|
+
function Metric(props) {
|
|
442
|
+
const toneClass = props.tone === void 0 ? "" : ` cf-${props.tone}`;
|
|
443
|
+
const children = [el("div", {
|
|
444
|
+
className: "cf-metric-label",
|
|
445
|
+
key: "l"
|
|
446
|
+
}, props.label), el("div", {
|
|
447
|
+
className: `cf-metric-value${toneClass}`,
|
|
448
|
+
key: "v"
|
|
449
|
+
}, props.value)];
|
|
450
|
+
if (props.sub !== void 0) children.push(el("div", {
|
|
451
|
+
className: "cf-metric-sub",
|
|
452
|
+
key: "s"
|
|
453
|
+
}, props.sub));
|
|
454
|
+
return el("div", { className: "cf-metric" }, ...children);
|
|
455
|
+
}
|
|
456
|
+
/** A notice strip, optionally warning-toned. */
|
|
457
|
+
function Notice(props) {
|
|
458
|
+
return el("div", { className: props.warn === true ? "cf-notice cf-notice-warn" : "cf-notice" }, props.children);
|
|
459
|
+
}
|
|
460
|
+
/** The counterfactual summary card. */
|
|
153
461
|
function Summary(props) {
|
|
154
462
|
const cf = props.report.counterfactual;
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
["Steps still paying the folded prompt", num(cf.remainingSteps)],
|
|
160
|
-
["Gross saving", signed(cf.grossSaving)],
|
|
161
|
-
["Prefix invalidation cost", signed(-cf.invalidationCost)],
|
|
162
|
-
["Net", signed(cf.netSaving)]
|
|
163
|
-
];
|
|
164
|
-
return react.createElement("section", { style: styles.box }, react.createElement("div", {
|
|
165
|
-
style: styles.heading,
|
|
463
|
+
const savedChars = props.report.folds.reduce((total, fold) => total + (fold.charsBefore - fold.charsAfter), 0);
|
|
464
|
+
const netTone = cf.netSaving >= 0 ? "good" : "bad";
|
|
465
|
+
return el("section", { className: "cf-card" }, el("div", {
|
|
466
|
+
className: "cf-card-title",
|
|
166
467
|
key: "h"
|
|
167
|
-
}, "Token comparison
|
|
168
|
-
|
|
169
|
-
key: "
|
|
170
|
-
},
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
468
|
+
}, "Token comparison"), el("div", {
|
|
469
|
+
className: "cf-grid",
|
|
470
|
+
key: "g"
|
|
471
|
+
}, el(Metric, {
|
|
472
|
+
key: "now",
|
|
473
|
+
label: "Folded content now",
|
|
474
|
+
value: `${compact(cf.tokensNow)} tok`,
|
|
475
|
+
sub: "what the window carries"
|
|
476
|
+
}), el(Metric, {
|
|
477
|
+
key: "before",
|
|
478
|
+
label: "If never folded",
|
|
479
|
+
value: `${compact(cf.tokensIfUnfolded)} tok`,
|
|
480
|
+
sub: "counterfactual"
|
|
481
|
+
}), el(Metric, {
|
|
482
|
+
key: "per",
|
|
483
|
+
label: "Saving per step",
|
|
484
|
+
value: `${num(cf.tokensSavedPerStep)} tok`,
|
|
485
|
+
sub: `${num(cf.remainingSteps)} steps left`
|
|
486
|
+
}), el(Metric, {
|
|
487
|
+
key: "gross",
|
|
488
|
+
label: "Gross saving",
|
|
489
|
+
value: signed(cf.grossSaving),
|
|
490
|
+
sub: "before invalidation",
|
|
491
|
+
tone: cf.grossSaving >= 0 ? "good" : "bad"
|
|
492
|
+
}), el(Metric, {
|
|
493
|
+
key: "cost",
|
|
494
|
+
label: "Invalidation cost",
|
|
495
|
+
value: signed(-cf.invalidationCost),
|
|
496
|
+
sub: "one-off prefix re-charge",
|
|
497
|
+
tone: "warn"
|
|
498
|
+
}), el(Metric, {
|
|
499
|
+
key: "net",
|
|
500
|
+
label: "Net",
|
|
501
|
+
value: signed(cf.netSaving),
|
|
502
|
+
sub: cf.netSaving >= 0 ? "net win" : "net loss — folded too late",
|
|
503
|
+
tone: netTone
|
|
504
|
+
})), el("div", {
|
|
505
|
+
style: { marginTop: "12px" },
|
|
506
|
+
key: "n"
|
|
507
|
+
}, el(Notice, { warn: !cf.hasRealUsage }, el("span", null, cf.hasRealUsage ? el("span", null, el("span", { className: "cf-notice-strong" }, "Priced against real usage. "), `Cache-read share ${cf.cacheReadShare === void 0 ? "n/a" : `${(cf.cacheReadShare * 100).toFixed(1)}%`}.`) : el("span", null, el("span", { className: "cf-notice-strong" }, "Heuristic only. "), "This session reported no provider usage, so these are token counts — not a bill.")))), el("div", {
|
|
508
|
+
className: "cf-aside",
|
|
509
|
+
style: { marginTop: "10px" },
|
|
510
|
+
key: "a"
|
|
511
|
+
}, `${num(savedChars)} characters removed across ${String(props.report.folds.length)} fold(s).`), props.report.reReads > 0 ? el("div", {
|
|
512
|
+
className: "cf-notice cf-notice-warn",
|
|
513
|
+
style: { marginTop: "10px" },
|
|
178
514
|
key: "r"
|
|
179
|
-
},
|
|
515
|
+
}, el("span", null, el("span", { className: "cf-notice-strong" }, `${String(props.report.reReads)} re-read signal(s). `), "Folded content was fetched again after being folded — folding it may have cost more than it saved.")) : null, el("details", {
|
|
516
|
+
key: "asm",
|
|
517
|
+
style: { marginTop: "10px" }
|
|
518
|
+
}, el("summary", { className: "cf-aside" }, "Assumptions"), el("ul", { style: {
|
|
519
|
+
margin: "6px 0 0 18px",
|
|
520
|
+
padding: 0
|
|
521
|
+
} }, ...cf.assumptions.map((assumption, index) => el("li", {
|
|
522
|
+
className: "cf-aside",
|
|
523
|
+
key: String(index)
|
|
524
|
+
}, assumption)))));
|
|
180
525
|
}
|
|
181
526
|
/** One fold row; the original text loads when the row is opened. */
|
|
182
527
|
function FoldItem(props) {
|
|
@@ -191,48 +536,42 @@ function FoldItem(props) {
|
|
|
191
536
|
setOriginal(text ?? "(could not read the stored original)");
|
|
192
537
|
});
|
|
193
538
|
};
|
|
194
|
-
const
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
} else if (!fold.recoverable) children.push(react.createElement("div", {
|
|
212
|
-
style: styles.notRecoverable,
|
|
213
|
-
key: "w"
|
|
214
|
-
}, "No recovery path: the original is in the session log but no tool exposes it."));
|
|
215
|
-
return react.createElement("details", {
|
|
216
|
-
style: styles.fold,
|
|
539
|
+
const isReasoning = fold.kind === "reasoning";
|
|
540
|
+
const tagClass = isReasoning ? "cf-tag cf-tag-reasoning" : "cf-tag";
|
|
541
|
+
const saved = fold.tokensBefore - fold.tokensAfter;
|
|
542
|
+
const body = [el("div", {
|
|
543
|
+
className: "cf-fold-body",
|
|
544
|
+
key: "b"
|
|
545
|
+
}, el("div", { className: "cf-label" }, "What stays in the window"), el("pre", { className: "cf-pre" }, fold.inline), fold.locator !== void 0 ? el("div", null, el("div", { className: "cf-label" }, "Full original"), original === null ? el("div", { className: "cf-aside" }, "Expand to load from the recovery path.") : el("pre", { className: "cf-pre cf-pre-original" }, original), el("div", { className: "cf-locator" }, fold.locator)) : el("div", {
|
|
546
|
+
className: "cf-aside cf-bad",
|
|
547
|
+
style: { marginTop: "8px" }
|
|
548
|
+
}, "No recovery path: the original is in the session log but no tool exposes it."))];
|
|
549
|
+
const children = [el("summary", {
|
|
550
|
+
className: "cf-fold-head",
|
|
551
|
+
key: "h"
|
|
552
|
+
}, el("span", { className: "cf-caret" }, "▶"), el("span", { className: tagClass }, isReasoning ? "reasoning" : fold.toolName ?? "tool"), el("span", { className: "cf-fold-pos" }, `turn ${String(fold.turn)}.${String(fold.step)}`), el("span", { className: "cf-fold-pos" }, `${compact(fold.charsBefore)} → ${compact(fold.charsAfter)} chars`), el("span", { className: "cf-fold-saving" }, `−${compact(Math.max(0, saved))} tok`))];
|
|
553
|
+
children.push(...body);
|
|
554
|
+
return el("details", {
|
|
555
|
+
className: "cf-fold",
|
|
217
556
|
onToggle
|
|
218
557
|
}, ...children);
|
|
219
558
|
}
|
|
220
|
-
/** The per-fold list. */
|
|
559
|
+
/** The per-fold list card. */
|
|
221
560
|
function FoldList(props) {
|
|
222
561
|
const { report, sessionId } = props;
|
|
223
|
-
const
|
|
224
|
-
|
|
562
|
+
const title = el("div", {
|
|
563
|
+
className: "cf-card-title",
|
|
225
564
|
key: "h"
|
|
226
|
-
}, `Folded content
|
|
227
|
-
if (report.folds.length === 0) return
|
|
228
|
-
|
|
565
|
+
}, `Folded content · ${String(report.folds.length)}`);
|
|
566
|
+
if (report.folds.length === 0) return el("section", { className: "cf-card" }, title, el("div", {
|
|
567
|
+
className: "cf-empty",
|
|
229
568
|
key: "e"
|
|
230
|
-
}, "
|
|
231
|
-
return
|
|
569
|
+
}, el("div", { className: "cf-empty-title" }, "Nothing folded in this session yet"), el("div", null, "Folds appear once a step batch is old enough and large enough to be worth replacing.")));
|
|
570
|
+
return el("section", { className: "cf-card" }, title, el("div", { className: "cf-folds" }, ...report.folds.map((fold) => el(FoldItem, {
|
|
232
571
|
key: String(fold.seq),
|
|
233
572
|
sessionId,
|
|
234
573
|
fold
|
|
235
|
-
})));
|
|
574
|
+
}))));
|
|
236
575
|
}
|
|
237
576
|
/**
|
|
238
577
|
* The tab body.
|
|
@@ -264,41 +603,26 @@ function ContextFoldTab(props) {
|
|
|
264
603
|
cancelled = true;
|
|
265
604
|
};
|
|
266
605
|
}, [sessionId]);
|
|
267
|
-
const children = [
|
|
268
|
-
|
|
269
|
-
key: "
|
|
270
|
-
}, "Context fold")];
|
|
271
|
-
if (sessionId === void 0) children.push(
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
}, "
|
|
275
|
-
else if (state.status === "loading") children.push(react.createElement("div", {
|
|
276
|
-
style: styles.muted,
|
|
277
|
-
key: "m"
|
|
278
|
-
}, "Loading…"));
|
|
279
|
-
else if (state.status === "error") children.push(react.createElement("div", {
|
|
280
|
-
style: styles.error,
|
|
281
|
-
key: "e"
|
|
282
|
-
}, `Could not load the report: ${state.error ?? "unknown"}`));
|
|
283
|
-
else if (state.status !== "ready" || state.report === void 0) children.push(react.createElement("div", {
|
|
284
|
-
style: styles.muted,
|
|
285
|
-
key: "m"
|
|
286
|
-
}, "This session has no folds to report."));
|
|
606
|
+
const children = [el("div", {
|
|
607
|
+
className: "cf-head",
|
|
608
|
+
key: "head"
|
|
609
|
+
}, el("span", { className: "cf-head-title" }, "Context fold"), sessionId === void 0 ? null : el("span", { className: "cf-head-note" }, state.status === "ready" ? `${String(state.report?.folds.length ?? 0)} fold(s) on the current surface` : "stage-boundary folding"))];
|
|
610
|
+
if (sessionId === void 0) children.push(el("div", { className: "cf-card" }, el("div", { className: "cf-empty" }, el("div", { className: "cf-empty-title" }, "No active session"), el("div", null, "Open a conversation to see its folds."))));
|
|
611
|
+
else if (state.status === "loading") children.push(el("div", { className: "cf-card" }, el("div", { className: "cf-empty" }, "Loading…")));
|
|
612
|
+
else if (state.status === "error") children.push(el("div", { className: "cf-card" }, el("div", { className: "cf-empty" }, el("div", { className: "cf-empty-title cf-bad" }, "Could not load the report"), el("div", null, state.error ?? "unknown error"))));
|
|
613
|
+
else if (state.status !== "ready" || state.report === void 0) children.push(el("div", { className: "cf-card" }, el("div", { className: "cf-empty" }, "This session has no folds to report.")));
|
|
287
614
|
else {
|
|
288
|
-
children.push(
|
|
615
|
+
children.push(el(Summary, {
|
|
289
616
|
key: "summary",
|
|
290
617
|
report: state.report
|
|
291
618
|
}));
|
|
292
|
-
children.push(
|
|
619
|
+
children.push(el(FoldList, {
|
|
293
620
|
key: "folds",
|
|
294
621
|
sessionId,
|
|
295
622
|
report: state.report
|
|
296
623
|
}));
|
|
297
624
|
}
|
|
298
|
-
return
|
|
299
|
-
className: "context-fold-tab",
|
|
300
|
-
style: styles.root
|
|
301
|
-
}, ...children);
|
|
625
|
+
return el("div", { className: "cf-root" }, ...children);
|
|
302
626
|
}
|
|
303
627
|
/** Client plugin name. */
|
|
304
628
|
const name = "context-fold-client";
|
|
@@ -313,6 +637,7 @@ const inject = ["slots", "locale"];
|
|
|
313
637
|
* @param ctx - client plugin context.
|
|
314
638
|
*/
|
|
315
639
|
function apply(ctx) {
|
|
640
|
+
ensurePanelCss();
|
|
316
641
|
const registerDictionaries = () => {
|
|
317
642
|
ctx.locale.register(NS, {
|
|
318
643
|
zh,
|
package/lib/index.js
CHANGED
|
@@ -1351,19 +1351,26 @@ function counterfactualOf(folds, steps, options = {}) {
|
|
|
1351
1351
|
const savedTokenRate = cacheReadShare === void 0 ? price.uncachedInput : cacheReadShare * price.cacheRead + (1 - cacheReadShare) * price.uncachedInput;
|
|
1352
1352
|
const grossSaving = tokensSavedPerStep * remainingSteps * savedTokenRate;
|
|
1353
1353
|
let invalidationCost = 0;
|
|
1354
|
-
if (foldSteps !== void 0)
|
|
1355
|
-
const
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1354
|
+
if (foldSteps !== void 0) {
|
|
1355
|
+
const distinct = [...new Set(foldSteps)];
|
|
1356
|
+
for (const foldStep of distinct) {
|
|
1357
|
+
const at = steps.find((step) => step.step >= foldStep);
|
|
1358
|
+
if (at === void 0) continue;
|
|
1359
|
+
const prompt = promptTokensOf(at);
|
|
1360
|
+
invalidationCost += prompt * (price.uncachedInput - price.cacheRead);
|
|
1361
|
+
}
|
|
1359
1362
|
}
|
|
1360
1363
|
const counterfactualPromptCost = actualPromptCost + tokensSavedPerStep * remainingSteps * savedTokenRate;
|
|
1361
1364
|
const assumptions = [];
|
|
1362
1365
|
assumptions.push(hasRealUsage ? `Prices are DeepSeek ratios (cache read ${price.cacheRead}×, write ${price.cacheWrite}×, uncached ${price.uncachedInput}×), applied to this session's observed usage.` : "This session reported no provider usage, so even the \"actual\" figures are heuristic token counts, not billed amounts.");
|
|
1363
1366
|
if (cacheReadShare !== void 0) assumptions.push(`Saved tokens are priced at this session's observed cache-read share (${(cacheReadShare * 100).toFixed(1)}%), not at the uncached rate.`);
|
|
1364
1367
|
if (foldSteps === void 0 || foldSteps.length === 0) assumptions.push("Fold step numbers were unavailable, so every observed step is credited with the saving. Treat the figure as an upper bound.");
|
|
1365
|
-
else
|
|
1368
|
+
else {
|
|
1369
|
+
const distinctFoldSteps = new Set(foldSteps).size;
|
|
1370
|
+
assumptions.push(`Each of the ${String(distinctFoldSteps)} distinct fold step(s) is charged one prefix invalidation: the prompt after it re-billed from cache-read to uncached once. Folds sharing a step share that charge, because they invalidate the same prefix.`);
|
|
1371
|
+
}
|
|
1366
1372
|
assumptions.push("The counterfactual assumes the same number of steps and the same cache behaviour as the real session — a folded prompt might have hit cache differently.");
|
|
1373
|
+
assumptions.push("\"Saving per step\" is the total prompt reduction on the surface, not a per-fold figure: it is multiplied by the steps that followed each fold.");
|
|
1367
1374
|
return {
|
|
1368
1375
|
tokensNow,
|
|
1369
1376
|
tokensIfUnfolded,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-context-fold",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "dsh-context-fold: stage-boundary folding of completed tool results into a conclusion plus a spill path, deterministic and replay-safe.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|