dsh-context-fold 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js ADDED
@@ -0,0 +1,319 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-context-fold",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ // src/client.ts
8
+ /** Locale namespace owned by this plugin's client half. */
9
+ const NS = "contextFold";
10
+ /** Dictionary: keep keys aligned with `locale/*.json`. */
11
+ const zh = { "tab.contextFold": "上下文折叠" };
12
+ /** Dictionary: keep keys aligned with `locale/*.json`. */
13
+ const en = { "tab.contextFold": "Context fold" };
14
+ const react = require("react");
15
+ /** Fetch the report for one session. */
16
+ async function fetchReport(sessionId) {
17
+ const response = await fetch("/api/context-fold/report", {
18
+ method: "POST",
19
+ headers: { "content-type": "application/json" },
20
+ body: JSON.stringify({ sessionId })
21
+ });
22
+ if (!response.ok) return void 0;
23
+ const body = await response.json();
24
+ return body.ok === true ? body.value : void 0;
25
+ }
26
+ /** Fetch the full text behind one fold. */
27
+ async function fetchOriginal(sessionId, locator) {
28
+ const response = await fetch("/api/context-fold/original", {
29
+ method: "POST",
30
+ headers: { "content-type": "application/json" },
31
+ body: JSON.stringify({
32
+ sessionId,
33
+ locator
34
+ })
35
+ });
36
+ if (!response.ok) return void 0;
37
+ const body = await response.json();
38
+ return body.ok === true ? body.value?.text : void 0;
39
+ }
40
+ /** Format a number for display. */
41
+ function num(value) {
42
+ return value.toLocaleString("en-US", { maximumFractionDigits: 1 });
43
+ }
44
+ /** A signed figure with an explicit sign, so a loss never looks like a gain. */
45
+ function signed(value) {
46
+ const rounded = Math.round(value * 10) / 10;
47
+ return `${rounded > 0 ? "+" : ""}${num(rounded)}`;
48
+ }
49
+ /** Read the active session id out of the slot props. */
50
+ function sessionIdOf(props) {
51
+ const value = props?.sessionId;
52
+ return typeof value === "string" && value !== "" ? value : void 0;
53
+ }
54
+ /** Inline styles, kept as plain objects because no CSS module ships here. */
55
+ const styles = {
56
+ root: {
57
+ padding: "12px",
58
+ font: "13px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace",
59
+ overflow: "auto",
60
+ height: "100%"
61
+ },
62
+ title: {
63
+ fontWeight: 600,
64
+ marginBottom: "8px"
65
+ },
66
+ muted: { color: "#888" },
67
+ box: {
68
+ marginBottom: "12px",
69
+ padding: "8px",
70
+ border: "1px solid #333",
71
+ borderRadius: "6px"
72
+ },
73
+ heading: {
74
+ fontWeight: 600,
75
+ marginBottom: "6px"
76
+ },
77
+ table: { borderCollapse: "collapse" },
78
+ labelCell: {
79
+ padding: "1px 12px 1px 0",
80
+ color: "#aaa"
81
+ },
82
+ warning: {
83
+ marginTop: "6px",
84
+ color: "#facc15"
85
+ },
86
+ reread: {
87
+ marginTop: "6px",
88
+ color: "#f87171"
89
+ },
90
+ fold: {
91
+ marginBottom: "6px",
92
+ padding: "6px",
93
+ border: "1px solid #2a2a2a",
94
+ borderRadius: "6px"
95
+ },
96
+ foldSummary: { cursor: "pointer" },
97
+ inlinePre: {
98
+ whiteSpace: "pre-wrap",
99
+ margin: "6px 0",
100
+ color: "#bbb",
101
+ maxHeight: "12em",
102
+ overflow: "auto"
103
+ },
104
+ originalPre: {
105
+ whiteSpace: "pre-wrap",
106
+ margin: "6px 0",
107
+ color: "#ddd",
108
+ maxHeight: "24em",
109
+ overflow: "auto",
110
+ borderTop: "1px dashed #333",
111
+ paddingTop: "6px"
112
+ },
113
+ locator: {
114
+ color: "#666",
115
+ fontSize: "11px",
116
+ wordBreak: "break-all"
117
+ },
118
+ notRecoverable: {
119
+ color: "#f87171",
120
+ fontSize: "11px"
121
+ },
122
+ detailsToggle: {
123
+ cursor: "pointer",
124
+ color: "#888",
125
+ marginTop: "6px"
126
+ },
127
+ assumptionList: {
128
+ margin: "4px 0 0 16px",
129
+ padding: 0,
130
+ color: "#999"
131
+ }
132
+ };
133
+ /** The counterfactual summary block. */
134
+ function Summary(props) {
135
+ const cf = props.report.counterfactual;
136
+ const rows = [
137
+ ["Folded content, now", `${num(cf.tokensNow)} tokens`],
138
+ ["If it had stayed unfolded", `${num(cf.tokensIfUnfolded)} tokens`],
139
+ ["Prompt reduction per step", `${num(cf.tokensSavedPerStep)} tokens`],
140
+ ["Steps still paying the folded prompt", num(cf.remainingSteps)],
141
+ ["Gross saving", signed(cf.grossSaving)],
142
+ ["Prefix invalidation cost", signed(-cf.invalidationCost)],
143
+ ["Net", signed(cf.netSaving)]
144
+ ];
145
+ return react.createElement("section", { style: styles.box }, react.createElement("div", {
146
+ style: styles.heading,
147
+ key: "h"
148
+ }, "Token comparison (estimate)"), react.createElement("table", {
149
+ style: styles.table,
150
+ key: "t"
151
+ }, ...rows.map(([label, value]) => react.createElement("tr", { key: label }, react.createElement("td", { style: styles.labelCell }, label), react.createElement("td", label === "Net" ? { style: {
152
+ fontWeight: 600,
153
+ color: cf.netSaving >= 0 ? "#4ade80" : "#f87171"
154
+ } } : null, value)))), react.createElement("div", {
155
+ style: styles.warning,
156
+ key: "f"
157
+ }, cf.hasRealUsage ? `Priced against this session's real usage${cf.cacheReadShare === void 0 ? "" : ` (cache-read share ${(cf.cacheReadShare * 100).toFixed(1)}%)`}.` : "This session reported NO provider usage — every figure here is heuristic token counts, not a bill."), react.createElement("details", { key: "a" }, react.createElement("summary", { style: styles.detailsToggle }, "Assumptions"), react.createElement("ul", { style: styles.assumptionList }, ...cf.assumptions.map((assumption, index) => react.createElement("li", { key: String(index) }, assumption)))), props.report.reReads > 0 ? react.createElement("div", {
158
+ style: styles.reread,
159
+ key: "r"
160
+ }, `Re-read signals: ${String(props.report.reReads)} — folded content was fetched again after being folded.`) : null);
161
+ }
162
+ /** One fold row; the original text loads when the row is opened. */
163
+ function FoldItem(props) {
164
+ const { fold, sessionId } = props;
165
+ const [original, setOriginal] = react.useState(null);
166
+ const onToggle = (event) => {
167
+ if (event.currentTarget?.open !== true || original !== null) return;
168
+ const locator = fold.locator;
169
+ if (locator === void 0) return;
170
+ setOriginal("");
171
+ fetchOriginal(sessionId, locator).then((text) => {
172
+ setOriginal(text ?? "(could not read the stored original)");
173
+ });
174
+ };
175
+ const summary = `${fold.kind === "reasoning" ? "reasoning" : fold.toolName ?? "tool"} · turn ${String(fold.turn)}.${String(fold.step)} · ${num(fold.charsBefore)} → ${num(fold.charsAfter)} chars · ${fold.recoverable ? "recoverable" : "NOT recoverable"}`;
176
+ const children = [react.createElement("summary", {
177
+ style: styles.foldSummary,
178
+ key: "s"
179
+ }, summary), react.createElement("pre", {
180
+ style: styles.inlinePre,
181
+ key: "i"
182
+ }, fold.inline)];
183
+ if (fold.locator !== void 0) {
184
+ children.push(react.createElement("div", {
185
+ style: styles.locator,
186
+ key: "l"
187
+ }, fold.locator));
188
+ if (original !== null) children.push(react.createElement("pre", {
189
+ style: styles.originalPre,
190
+ key: "o"
191
+ }, original));
192
+ } else if (!fold.recoverable) children.push(react.createElement("div", {
193
+ style: styles.notRecoverable,
194
+ key: "w"
195
+ }, "No recovery path: the original is in the session log but no tool exposes it."));
196
+ return react.createElement("details", {
197
+ style: styles.fold,
198
+ onToggle
199
+ }, ...children);
200
+ }
201
+ /** The per-fold list. */
202
+ function FoldList(props) {
203
+ const { report, sessionId } = props;
204
+ const heading = react.createElement("div", {
205
+ style: styles.heading,
206
+ key: "h"
207
+ }, `Folded content (${String(report.folds.length)})`);
208
+ if (report.folds.length === 0) return react.createElement("section", null, heading, react.createElement("div", {
209
+ style: styles.muted,
210
+ key: "e"
211
+ }, "Nothing has been folded in this session yet."));
212
+ return react.createElement("section", null, heading, ...report.folds.map((fold) => react.createElement(FoldItem, {
213
+ key: String(fold.seq),
214
+ sessionId,
215
+ fold
216
+ })));
217
+ }
218
+ /**
219
+ * The tab body.
220
+ *
221
+ * The active session id arrives through the standard slot props, and the report
222
+ * is refetched whenever that id changes because the tab outlives navigation.
223
+ */
224
+ function ContextFoldTab(props) {
225
+ const sessionId = sessionIdOf(props);
226
+ const [state, setState] = react.useState({ status: "loading" });
227
+ react.useEffect(() => {
228
+ if (sessionId === void 0) {
229
+ setState({ status: "empty" });
230
+ return;
231
+ }
232
+ let cancelled = false;
233
+ setState({ status: "loading" });
234
+ fetchReport(sessionId).then((report) => {
235
+ if (cancelled) return;
236
+ setState(report === void 0 ? { status: "empty" } : {
237
+ status: "ready",
238
+ report
239
+ });
240
+ });
241
+ return () => {
242
+ cancelled = true;
243
+ };
244
+ }, [sessionId]);
245
+ const children = [react.createElement("div", {
246
+ style: styles.title,
247
+ key: "title"
248
+ }, "Context fold")];
249
+ if (sessionId === void 0) children.push(react.createElement("div", {
250
+ style: styles.muted,
251
+ key: "m"
252
+ }, "No active session."));
253
+ else if (state.status === "loading") children.push(react.createElement("div", {
254
+ style: styles.muted,
255
+ key: "m"
256
+ }, "Loading…"));
257
+ else if (state.status !== "ready" || state.report === void 0) children.push(react.createElement("div", {
258
+ style: styles.muted,
259
+ key: "m"
260
+ }, "No report available for this session."));
261
+ else {
262
+ children.push(react.createElement(Summary, {
263
+ key: "summary",
264
+ report: state.report
265
+ }));
266
+ children.push(react.createElement(FoldList, {
267
+ key: "folds",
268
+ sessionId,
269
+ report: state.report
270
+ }));
271
+ }
272
+ return react.createElement("div", {
273
+ className: "context-fold-tab",
274
+ style: styles.root
275
+ }, ...children);
276
+ }
277
+ /** Client plugin name. */
278
+ const name = "context-fold-client";
279
+ /** The tab needs the slot registry and the locale service. */
280
+ const inject = ["slots", "locale"];
281
+ /**
282
+ * Mount the conversation tab.
283
+ *
284
+ * Dictionaries are registered from inside this bundle: the locale service reads
285
+ * them from the plugin registry, not from `locale/*.json` on disk.
286
+ *
287
+ * @param ctx - client plugin context.
288
+ */
289
+ function apply(ctx) {
290
+ const registerDictionaries = () => {
291
+ ctx.locale.register(NS, {
292
+ zh,
293
+ en
294
+ });
295
+ };
296
+ if (typeof ctx.effect === "function") ctx.effect(registerDictionaries, "context-fold: dictionaries");
297
+ else registerDictionaries();
298
+ const t = (key) => {
299
+ try {
300
+ return ctx.locale.bind(NS)(key) || (en[key] ?? key);
301
+ } catch {
302
+ return en[key] ?? key;
303
+ }
304
+ };
305
+ ctx.slots.inject("conversation.view", () => ctx.slots.register({
306
+ name: "conversation.view",
307
+ id: "context-fold",
308
+ order: 40,
309
+ locale: NS,
310
+ label: () => t("tab.contextFold")
311
+ }, ContextFoldTab));
312
+ }
313
+ exports.apply = apply;
314
+ exports.inject = inject;
315
+ exports.name = name;
316
+
317
+ return module.exports;
318
+ },
319
+ });