dsh-auto-compaction 0.1.3 → 0.2.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,961 @@
1
+ /* dsh-auto-compaction client bundle.
2
+ * Hand-written in the ModuleLoader wrapper shape DSH's client runtime uses.
3
+ *
4
+ * Renders the 设置 → 会话压缩 section. All persistence goes through the
5
+ * package's own RPC channel ("/dsh-auto-compaction"), whose node half writes
6
+ * ~/.dsh-handoff-compaction/settings.json — the same file the engine reads on
7
+ * every decision, so a save takes effect without restarting DSH.
8
+ */
9
+ window.__ModuleLoader__.load({
10
+ id: "dsh-auto-compaction",
11
+ factory: (require) => {
12
+ "use strict";
13
+ var module = { exports: {} };
14
+ var exports = module.exports;
15
+
16
+ var React = require("react");
17
+ var ReactDOM = require("react-dom");
18
+ var h = React.createElement;
19
+
20
+ var name = "dsh-auto-compaction";
21
+ var inject = ["slots", "connection"];
22
+ var CHANNEL = "/dsh-auto-compaction";
23
+
24
+ var C = {
25
+ text: "var(--dsh-text-primary, #e8e9ea)",
26
+ muted: "var(--dsh-text-secondary, #9aa0a6)",
27
+ faint: "var(--dsh-text-tertiary, #7f858c)",
28
+ line: "var(--dsh-border, #2c2e33)",
29
+ panel: "var(--dsh-fill-secondary, #191a1d)",
30
+ accent: "#4c8dff",
31
+ ok: "#3fb950",
32
+ };
33
+
34
+ var DEFAULT_TEMPLATE = "~/.dsh-handoff-compaction/template.md";
35
+ var PRESETS = [
36
+ { id: "lightweight-local", label: "轻量本地" },
37
+ { id: "lightweight-standard", label: "轻量标准" },
38
+ ];
39
+ var DISPLAY_OPTIONS = [
40
+ { id: "orbit", label: "轨道环" },
41
+ { id: "strip", label: "状态条" },
42
+ { id: "ring", label: "进度环" },
43
+ { id: "topbar", label: "顶部条" },
44
+ { id: "corner", label: "右上角" },
45
+ ];
46
+ var EXAMPLE_WINDOW = 250000, EXAMPLE_RESERVED = 20000, EXAMPLE_HEADROOM = 10000;
47
+
48
+ var DISPLAY_STYLES = ["orbit", "strip", "ring", "topbar", "corner"];
49
+ var displayStyle = "orbit";
50
+ var displayStyleListeners = [];
51
+
52
+ function normalizeDisplayStyle(value) {
53
+ return DISPLAY_STYLES.indexOf(value) >= 0 ? value : "orbit";
54
+ }
55
+ function publishDisplayStyle(value) {
56
+ var next = normalizeDisplayStyle(value);
57
+ if (next === displayStyle) return;
58
+ displayStyle = next;
59
+ for (var i = 0; i < displayStyleListeners.length; i += 1) {
60
+ try { displayStyleListeners[i](displayStyle); } catch (error) { /* a bad subscriber must not break the dock */ }
61
+ }
62
+ }
63
+ function subscribeDisplayStyle(listener) {
64
+ displayStyleListeners.push(listener);
65
+ return function unsubscribe() {
66
+ var index = displayStyleListeners.indexOf(listener);
67
+ if (index >= 0) displayStyleListeners.splice(index, 1);
68
+ };
69
+ }
70
+ function loadDisplayStyle(connection) {
71
+ if (!connection || !connection.rpc || typeof connection.rpc.call !== "function") return;
72
+ call(connection, "get").then(function (value) {
73
+ if (value && typeof value === "object") publishDisplayStyle(value.displayStyle);
74
+ }).catch(function () { /* keep the default orbit */ });
75
+ }
76
+
77
+ function clampPercent(raw) {
78
+ var n = Number(raw);
79
+ if (!Number.isFinite(n)) return 0;
80
+ return Math.max(0, Math.min(100, Math.round(n)));
81
+ }
82
+ function snapPercent(raw) {
83
+ return Math.max(0, Math.min(100, Math.round(Number(raw) / 5) * 5));
84
+ }
85
+ function fmt(n) { return Math.round(n).toLocaleString("en-US"); }
86
+
87
+ function call(connection, endpoint, payload, timeoutMs) {
88
+ var signal;
89
+ try {
90
+ if (typeof AbortSignal !== "undefined" && AbortSignal.timeout) {
91
+ signal = AbortSignal.timeout(typeof timeoutMs === "number" ? timeoutMs : 8000);
92
+ }
93
+ } catch (e) { signal = undefined; }
94
+ return Promise.resolve(connection.rpc.call(CHANNEL, endpoint, payload || {}, signal)).then(function (res) {
95
+ if (res && res.ok === false) throw new Error((res.error && res.error.message) || "RPC failed");
96
+ return res && typeof res === "object" && "value" in res ? res.value : res;
97
+ });
98
+ }
99
+
100
+ /* ---------- compaction status dock ---------- */
101
+
102
+ var STATUS_STYLE_ID = "dsh-auto-compaction-status-style";
103
+ var STATUS_TEXT = "var(--dsh-text-primary, var(--dsw-alias-label-primary, #e8e9ea))";
104
+ var STATUS_MUTED = "var(--dsh-text-secondary, var(--dsw-alias-label-secondary, #9aa0a6))";
105
+ var STATUS_FAINT = "var(--dsh-text-tertiary, var(--dsw-alias-label-tertiary, #7f858c))";
106
+ var STATUS_BORDER = "var(--dsh-border, var(--dsw-alias-border-l2, #2c2e33))";
107
+ // Opaque elevated surface. The menu token is intentionally translucent
108
+ // (menu layers add their own backdrop blur); the status surfaces must stay
109
+ // readable over live conversation text without a blur.
110
+ var STATUS_SURFACE = "var(--dsw-alias-bg-layer-3, #353638)";
111
+ var STATUS_PANEL = STATUS_SURFACE;
112
+ var STATUS_ACCENT = "var(--dsw-alias-state-business-primary, #4c8dff)";
113
+ var STATUS_SUCCESS = "var(--dsw-alias-state-success-primary, #3fb950)";
114
+ var STATUS_WARN = "var(--dsw-alias-state-warn-primary, #d29922)";
115
+ var STATUS_ERROR = "var(--dsw-alias-state-error-primary, #e5534b)";
116
+
117
+ function ensureStatusStyles() {
118
+ try {
119
+ if (typeof document === "undefined" || document.getElementById(STATUS_STYLE_ID) !== null) return;
120
+ var style = document.createElement("style");
121
+ style.id = STATUS_STYLE_ID;
122
+ style.textContent = [
123
+ "@keyframes dshHandoffPulse{0%,100%{opacity:.35;transform:scale(.82)}50%{opacity:1;transform:scale(1)}}",
124
+ "@keyframes dshHandoffShimmer{from{transform:translateX(-100%)}to{transform:translateX(100%)}}",
125
+ "@keyframes dshHandoffSpin{to{transform:rotate(360deg)}}",
126
+ ".dsh-handoff-pulse{animation:dshHandoffPulse 1.25s ease-in-out infinite}",
127
+ ".dsh-handoff-fill::after{content:\"\";position:absolute;inset:0;background:linear-gradient(90deg,transparent,rgba(255,255,255,.45),transparent);animation:dshHandoffShimmer 1.5s linear infinite}",
128
+ ".dsh-handoff-orbit{position:relative;width:56px;height:56px;border-radius:50%;border:2px solid var(--dsh-border,#303338)}",
129
+ ".dsh-handoff-orbit::before{content:\"\";position:absolute;inset:-2px;border-radius:50%;border:2px solid transparent;border-top-color:var(--dsw-alias-state-business-primary,#4c8dff);border-right-color:rgba(76,141,255,.45);animation:dshHandoffSpin 1.15s linear infinite}",
130
+ ".dsh-handoff-orbit::after{content:\"\";position:absolute;inset:13px;border-radius:50%;border:2px solid transparent;border-bottom-color:var(--dsw-alias-state-business-primary,#4c8dff);animation:dshHandoffSpin 1.8s linear infinite reverse}",
131
+ ".dsh-handoff-orbit-core{position:absolute;left:50%;top:50%;width:8px;height:8px;margin:-4px 0 0 -4px;border-radius:50%;background:var(--dsw-alias-state-business-primary,#4c8dff);box-shadow:0 0 12px rgba(76,141,255,.7);animation:dshHandoffPulse 1.25s ease-in-out infinite}",
132
+ ".dsh-handoff-mini-orbit{position:relative;width:26px;height:26px;border-radius:50%;border:1.5px solid var(--dsh-border,#303338)}",
133
+ ".dsh-handoff-mini-orbit::before{content:\"\";position:absolute;inset:-1.5px;border-radius:50%;border:1.5px solid transparent;border-top-color:var(--dsw-alias-state-business-primary,#4c8dff);animation:dshHandoffSpin 1.15s linear infinite}",
134
+ ".dsh-handoff-mini-orbit::after{content:\"\";position:absolute;inset:7px;border-radius:50%;border:1.5px solid transparent;border-bottom-color:var(--dsw-alias-state-business-primary,#4c8dff);animation:dshHandoffSpin 1.8s linear infinite reverse}",
135
+ ".dsh-handoff-opt{width:12px;height:12px;display:inline-block;position:relative;flex:0 0 auto}",
136
+ ".dsh-handoff-opt-strip::before{content:\"\";position:absolute;left:0;right:0;top:4px;height:4px;border-radius:2px;border:1px solid currentColor}",
137
+ ".dsh-handoff-opt-strip::after{content:\"\";position:absolute;left:2px;right:2px;top:6px;height:1px;background:currentColor;opacity:.7}",
138
+ ".dsh-handoff-opt-ring{border:1.5px solid currentColor;border-radius:50%}",
139
+ ".dsh-handoff-opt-ring::after{content:\"\";position:absolute;inset:3px;border-radius:50%;border:1px solid currentColor;opacity:.5}",
140
+ ".dsh-handoff-opt-orbit{border:1px solid currentColor;border-radius:50%}",
141
+ ".dsh-handoff-opt-orbit::after{content:\"\";position:absolute;inset:-1px;border-radius:50%;border:1px solid transparent;border-top-color:currentColor}",
142
+ ".dsh-handoff-opt-top::before{content:\"\";position:absolute;left:0;right:0;top:1px;height:2px;background:currentColor;border-radius:1px}",
143
+ ".dsh-handoff-opt-top::after{content:\"\";position:absolute;left:2px;right:2px;bottom:1px;height:5px;border:1px solid currentColor;border-radius:2px;opacity:.6}",
144
+ "@media (prefers-reduced-motion:reduce){.dsh-handoff-pulse,.dsh-handoff-orbit::before,.dsh-handoff-orbit::after,.dsh-handoff-orbit-core,.dsh-handoff-mini-orbit::before,.dsh-handoff-mini-orbit::after{animation:none}.dsh-handoff-fill::after{animation:none}}",
145
+ ].join("");
146
+ document.head.appendChild(style);
147
+ } catch (error) { /* styling must never break the dock */ }
148
+ }
149
+
150
+ function clockText(startedAt) {
151
+ var start = Number(startedAt);
152
+ var seconds = Number.isFinite(start) && start > 0 ? Math.max(0, Math.floor((Date.now() - start) / 1000)) : 0;
153
+ return String(Math.floor(seconds / 60)).padStart(2, "0") + ":" + String(seconds % 60).padStart(2, "0");
154
+ }
155
+
156
+ function statusProgress(status) {
157
+ if (status && status.step === "commit") return 92;
158
+ if (status && status.retrying) return 70;
159
+ if (status && status.step === "summary") return 55;
160
+ return 12;
161
+ }
162
+
163
+ function statusStep(status) {
164
+ if (status && status.retrying) return "补全 2/3";
165
+ if (status && status.step === "prepare") return "1/3 读取缓存";
166
+ if (status && status.step === "commit") return "3/3 替换历史";
167
+ return "2/3 写文档";
168
+ }
169
+
170
+ function fetchStatus(connection, sessionId) {
171
+ return call(connection, "status", { sessionId: sessionId }, 3500);
172
+ }
173
+
174
+ /**
175
+ * Anchor the corner card to the conversation area instead of the window:
176
+ * its top lines up with the first message row (below the 对话/轨迹 tabs) and
177
+ * its right edge keeps the header's inset, so it never covers the tab row.
178
+ * The width shrinks to the gutter between the message column and the window
179
+ * edge when the window is narrow.
180
+ */
181
+ function cornerPlacement(desiredWidth) {
182
+ var right = (typeof window === "undefined" ? 0 : window.innerWidth) - 20;
183
+ var top = 96;
184
+ var width = desiredWidth;
185
+ try {
186
+ var body = document.querySelector("[data-conversation-content]");
187
+ var br = body && body.getBoundingClientRect();
188
+ if (br && br.height > 80) {
189
+ top = Math.round(br.top) + 8;
190
+ right = Math.round(br.right) - 20;
191
+ }
192
+ var card = document.querySelector("[data-composer-card]");
193
+ var cr = card && card.getBoundingClientRect();
194
+ if (cr && cr.width > 120) {
195
+ var clearance = parseFloat(getComputedStyle(card).getPropertyValue("--dsh-composer-side-clearance")) || 16;
196
+ var columnRight = Math.round(cr.right) - clearance;
197
+ width = Math.max(184, Math.min(desiredWidth, right - columnRight - 10));
198
+ }
199
+ } catch (error) { /* keep the window-corner fallback */ }
200
+ width = Math.max(160, Math.min(width, right - 12));
201
+ return { left: Math.max(8, Math.round(right - width)), top: Math.round(top), width: Math.round(width) };
202
+ }
203
+
204
+ function StatusDock(props) {
205
+ ensureStatusStyles();
206
+ var connection = props.connection;
207
+ var sessionId = props.sessionId;
208
+ var cancel = props.cancel;
209
+
210
+ var loaded = React.useState(null);
211
+ var status = loaded[0], setStatus = loaded[1];
212
+ var dismissed = React.useState(0);
213
+ var dismissedAt = dismissed[0], setDismissedAt = dismissed[1];
214
+ var clock = React.useState(0);
215
+ var tick = clock[0], setTick = clock[1];
216
+ void tick;
217
+ var styleState = React.useState(displayStyle);
218
+ var themeStyle = styleState[0], setThemeStyle = styleState[1];
219
+
220
+ React.useEffect(function () {
221
+ return subscribeDisplayStyle(setThemeStyle);
222
+ }, []);
223
+
224
+ React.useEffect(function () {
225
+ loadDisplayStyle(connection);
226
+ }, [connection]);
227
+
228
+ React.useEffect(function () {
229
+ if (!connection || !sessionId) return undefined;
230
+ var alive = true;
231
+ var timer;
232
+ function poll() {
233
+ fetchStatus(connection, sessionId).then(function (value) {
234
+ if (!alive) return;
235
+ var next = value && typeof value === "object" ? value : null;
236
+ setStatus(function (previous) {
237
+ if (previous === null || next === null) return next;
238
+ if (previous.updatedAt === next.updatedAt && previous.phase === next.phase) return previous;
239
+ return next;
240
+ });
241
+ timer = setTimeout(poll, next !== null && next.phase === "running" ? 1000 : 4000);
242
+ }).catch(function () {
243
+ if (!alive) return;
244
+ timer = setTimeout(poll, 5000);
245
+ });
246
+ }
247
+ poll();
248
+ return function () { alive = false; if (timer !== undefined) clearTimeout(timer); };
249
+ }, [connection, sessionId]);
250
+
251
+ React.useEffect(function () {
252
+ if (!status) return undefined;
253
+ var timer = setInterval(function () { setTick(function (value) { return value + 1; }); }, 1000);
254
+ return function () { clearInterval(timer); };
255
+ }, [status === null ? "" : status.phase]);
256
+
257
+ if (!status || !status.phase) return null;
258
+
259
+ var now = Date.now();
260
+ var startedAt = Number(status.startedAt || 0);
261
+ var timeoutMs = Number(status.timeoutMs) > 0 ? Number(status.timeoutMs) : 600000;
262
+ var staleRunning = status.phase === "running" && startedAt > 0 && now - startedAt > timeoutMs + 60000;
263
+ var view = staleRunning ? "failed" : status.phase;
264
+ if (view !== "running" && view !== "done" && view !== "failed") return null;
265
+ if (dismissedAt !== 0 && Number(status.updatedAt || 0) === dismissedAt) return null;
266
+
267
+ if (view === "done") {
268
+ var doneAt = Number(status.completedAt || status.updatedAt || 0);
269
+ if (doneAt > 0 && now - doneAt > 8000) return null;
270
+ }
271
+ if (view === "failed" && !staleRunning) {
272
+ var failedAt = Number(status.failedAt || status.updatedAt || 0);
273
+ if (failedAt > 0 && now - failedAt > 120000) return null;
274
+ }
275
+
276
+ var color = view === "running" ? STATUS_ACCENT : view === "done" ? STATUS_SUCCESS : STATUS_ERROR;
277
+ if (staleRunning) color = STATUS_WARN;
278
+
279
+ var title;
280
+ var sub;
281
+ var progress = statusProgress(status);
282
+ if (view === "running") {
283
+ title = status.retrying
284
+ ? (status.writing ? "正在从头重写交接文档" : "正在补全交接文档")
285
+ : "正在压缩历史会话";
286
+ var pieces = [];
287
+ if (Number(status.totalTokens) > 0) {
288
+ pieces.push("上下文 " + fmt(Number(status.totalTokens)) + (Number(status.thresholdTokens) > 0 ? " / " + fmt(Number(status.thresholdTokens)) : ""));
289
+ }
290
+ pieces.push(status.retrying ? "第 " + String(status.summaryAttempt || 2) + " 次请求" : "期间发送的消息会排队");
291
+ if (Array.isArray(status.missingSections) && status.missingSections.length > 0) {
292
+ pieces.push("缺:" + status.missingSections.join("、"));
293
+ }
294
+ sub = pieces.join(" · ");
295
+ } else if (view === "done") {
296
+ var nodes = Number(status.shadowedNodes) || 0;
297
+ var tokens = Number(status.shadowedTokens) || 0;
298
+ title = nodes > 0
299
+ ? "已压缩 " + fmt(nodes) + " 条历史记录(约 " + fmt(tokens) + " tokens)"
300
+ : (status.note === "no compactable range" ? "没有需要压缩的历史" : "压缩完成");
301
+ sub = "旧消息仍可用 rewind 回滚";
302
+ } else {
303
+ title = staleRunning ? "压缩可能已中断(超时)· 历史未改动" : "交接文档生成失败 · 历史未改动";
304
+ if (staleRunning) {
305
+ sub = "下一次空闲会重新尝试";
306
+ } else if (String(status.errorCode || "") === "INCOMPLETE_SUMMARY") {
307
+ sub = "摘要不完整,已放弃提交 · 下次空闲重试";
308
+ } else if (String(status.error || "").toLowerCase().indexOf("cancel") >= 0) {
309
+ sub = "压缩已取消 · 下次空闲重试";
310
+ } else {
311
+ sub = status.error ? String(status.error).slice(0, 140) : "下次空闲重试";
312
+ }
313
+ }
314
+
315
+ function handleCancel() {
316
+ if (typeof cancel !== "function") return;
317
+ try {
318
+ var pending = cancel();
319
+ if (pending && typeof pending.catch === "function") pending.catch(function () { /* status updates via polling */ });
320
+ } catch (error) { /* the stop button remains available */ }
321
+ }
322
+
323
+ function handleDismiss() {
324
+ setDismissedAt(Number(status.updatedAt || 0));
325
+ }
326
+
327
+ var ghost = {
328
+ border: "1px solid " + STATUS_BORDER,
329
+ background: "transparent",
330
+ color: STATUS_MUTED,
331
+ borderRadius: 6,
332
+ padding: "2px 8px",
333
+ fontSize: 11,
334
+ lineHeight: "16px",
335
+ fontFamily: "inherit",
336
+ cursor: "pointer",
337
+ flex: "0 0 auto",
338
+ };
339
+ var close = {
340
+ border: 0,
341
+ background: "transparent",
342
+ color: STATUS_FAINT,
343
+ fontSize: 14,
344
+ lineHeight: "20px",
345
+ width: 22,
346
+ height: 22,
347
+ padding: 0,
348
+ cursor: "pointer",
349
+ flex: "0 0 auto",
350
+ };
351
+
352
+ if (view === "running" && themeStyle !== "strip") {
353
+ var stepNumber = status.step === "prepare" ? 1 : status.step === "commit" ? 3 : 2;
354
+ var ringPct = stepNumber === 1 ? 33 : stepNumber === 3 ? 92 : 66;
355
+ var cardSub = status.retrying
356
+ ? (status.writing ? "正在重写摘要 · " : "正在补全摘要 · ") + clockText(startedAt)
357
+ : clockText(startedAt) + " · 期间消息会排队";
358
+ if (themeStyle === "topbar") {
359
+ var topLine = h("div", { style: {
360
+ position: "fixed",
361
+ left: 0,
362
+ top: 84,
363
+ width: "100vw",
364
+ height: 2,
365
+ zIndex: 70,
366
+ pointerEvents: "none",
367
+ background: "rgba(127,133,140,.28)",
368
+ } },
369
+ h("div", { className: "dsh-handoff-fill", style: { height: "100%", width: "62%", background: STATUS_ACCENT, position: "relative", overflow: "hidden" } })
370
+ );
371
+ var pill = h("div", { style: {
372
+ position: "fixed",
373
+ left: "50%",
374
+ top: 92,
375
+ transform: "translateX(-50%)",
376
+ zIndex: 70,
377
+ pointerEvents: "none",
378
+ } },
379
+ h("span", { style: {
380
+ display: "inline-flex", alignItems: "center", gap: 7, height: 26, padding: "0 11px",
381
+ borderRadius: 999, border: "1px solid " + STATUS_ACCENT,
382
+ background: STATUS_SURFACE, color: STATUS_TEXT, fontSize: 11, whiteSpace: "nowrap",
383
+ } },
384
+ h("span", { className: "dsh-handoff-pulse", style: { width: 7, height: 7, borderRadius: "50%", background: STATUS_ACCENT } }),
385
+ "压缩中 · " + statusStep(status) + " · " + clockText(startedAt)
386
+ )
387
+ );
388
+ return ReactDOM.createPortal(h(React.Fragment, null, topLine, pill), document.body);
389
+ }
390
+ if (themeStyle === "corner") {
391
+ var place = cornerPlacement(268);
392
+ return ReactDOM.createPortal(
393
+ h("div", {
394
+ role: "status",
395
+ "aria-live": "polite",
396
+ "data-dsh-handoff-status": "running",
397
+ title: "压缩期间发送的消息会排队",
398
+ style: {
399
+ position: "fixed",
400
+ left: place.left,
401
+ top: place.top,
402
+ zIndex: 70,
403
+ pointerEvents: "none",
404
+ boxSizing: "border-box",
405
+ width: place.width,
406
+ display: "flex",
407
+ alignItems: "flex-start",
408
+ gap: 8,
409
+ padding: "9px 11px",
410
+ borderRadius: 10,
411
+ background: STATUS_SURFACE,
412
+ border: "1px solid " + STATUS_BORDER,
413
+ borderLeft: "2px solid " + STATUS_ACCENT,
414
+ boxShadow: "0 14px 34px rgba(0,0,0,.38)",
415
+ },
416
+ },
417
+ h("span", { className: "dsh-handoff-pulse", style: { width: 8, height: 8, borderRadius: "50%", background: STATUS_ACCENT, flex: "0 0 auto", marginTop: 5 } }),
418
+ h("div", { style: { minWidth: 0, flex: 1 } },
419
+ h("div", { style: { fontSize: 12, fontWeight: 600, color: STATUS_TEXT, lineHeight: "17px", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" } }, title),
420
+ h("div", { style: { marginTop: 2, fontSize: 10.5, color: STATUS_MUTED, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" } },
421
+ clockText(startedAt) + " · " + statusStep(status) + (place.width >= 216 ? " · 消息会排队" : ""))
422
+ )
423
+ ),
424
+ document.body
425
+ );
426
+ }
427
+ var indicator = themeStyle === "ring"
428
+ ? h("div", { style: { position: "relative", width: 64, height: 64 } },
429
+ h("div", { style: {
430
+ position: "absolute", inset: 0, borderRadius: "50%",
431
+ background: "conic-gradient(" + STATUS_ACCENT + " 0 " + ringPct + "%, rgba(127,133,140,.28) " + ringPct + "% 100%)",
432
+ WebkitMask: "radial-gradient(circle, transparent 55%, #000 58%)",
433
+ mask: "radial-gradient(circle, transparent 55%, #000 58%)",
434
+ } }),
435
+ h("div", { style: {
436
+ position: "absolute", inset: 0, display: "flex", flexDirection: "column",
437
+ alignItems: "center", justifyContent: "center",
438
+ fontFamily: "ui-monospace,Consolas,monospace", fontSize: 13, fontWeight: 500,
439
+ color: STATUS_TEXT, fontVariantNumeric: "tabular-nums",
440
+ } },
441
+ stepNumber + "/3",
442
+ h("span", { style: { fontSize: 9, color: STATUS_MUTED, fontFamily: "inherit", fontWeight: 400 } }, "写文档")
443
+ )
444
+ )
445
+ : h("div", { className: "dsh-handoff-orbit" }, h("div", { className: "dsh-handoff-orbit-core" }));
446
+ var overlayStyle = {
447
+ position: "fixed",
448
+ left: 0,
449
+ top: 0,
450
+ width: "100vw",
451
+ height: "100vh",
452
+ zIndex: 70,
453
+ pointerEvents: "none",
454
+ display: "flex",
455
+ alignItems: "center",
456
+ justifyContent: "center",
457
+ boxSizing: "border-box",
458
+ paddingLeft: "min(200px, 15vw)",
459
+ // The overlay never blocks input, so no blur or dim: a modal-looking
460
+ // scrim here would falsely suggest the conversation is frozen.
461
+ };
462
+ var cardStyle = {
463
+ width: 264,
464
+ maxWidth: "82%",
465
+ background: STATUS_SURFACE,
466
+ border: "1px solid " + STATUS_BORDER,
467
+ borderRadius: 14,
468
+ padding: "16px 16px 14px",
469
+ boxShadow: "0 20px 46px rgba(0,0,0,.45), 0 2px 8px rgba(0,0,0,.3)",
470
+ display: "flex",
471
+ flexDirection: "column",
472
+ alignItems: "center",
473
+ gap: 7,
474
+ textAlign: "center",
475
+ pointerEvents: "none",
476
+ };
477
+ return ReactDOM.createPortal(
478
+ h("div", { role: "status", "aria-live": "polite", "data-dsh-handoff-status": "running", style: overlayStyle },
479
+ h("div", { style: cardStyle },
480
+ indicator,
481
+ h("div", { style: { fontSize: 12.5, fontWeight: 600, color: STATUS_TEXT } }, title),
482
+ h("div", { style: { fontSize: 11, color: STATUS_MUTED } }, cardSub),
483
+ h("div", { style: { fontSize: 10.5, color: STATUS_MUTED, opacity: 0.85 } }, "完成后整段历史收成一条交接文档")
484
+ )
485
+ ),
486
+ document.body
487
+ );
488
+ }
489
+
490
+ return h("div", {
491
+ role: "status",
492
+ "aria-live": "polite",
493
+ "data-dsh-handoff-status": view,
494
+ style: {
495
+ boxSizing: "border-box",
496
+ width: "100%",
497
+ maxWidth: "var(--dsh-composer-card-max-width, 860px)",
498
+ margin: "0 auto 6px",
499
+ padding: "7px 10px 8px",
500
+ border: "1px solid " + STATUS_BORDER,
501
+ borderLeft: "3px solid " + color,
502
+ borderRadius: 8,
503
+ background: STATUS_PANEL,
504
+ boxShadow: "0 8px 22px rgba(0,0,0,.28)",
505
+ color: STATUS_TEXT,
506
+ fontSize: 12,
507
+ lineHeight: "18px",
508
+ display: "flex",
509
+ flexDirection: "column",
510
+ gap: 5,
511
+ },
512
+ },
513
+ h("div", { style: { display: "flex", alignItems: "center", gap: 8, minWidth: 0 } },
514
+ h("span", {
515
+ className: view === "running" ? "dsh-handoff-pulse" : "",
516
+ "aria-hidden": true,
517
+ style: { width: 8, height: 8, borderRadius: "50%", background: color, flex: "0 0 auto" },
518
+ }),
519
+ h("div", { style: { flex: 1, minWidth: 0 } },
520
+ h("div", { style: { display: "flex", alignItems: "center", gap: 6, minWidth: 0 } },
521
+ h("span", { style: { fontSize: 12, fontWeight: 500, color: STATUS_TEXT, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", minWidth: 0, flex: 1 } }, title),
522
+ view === "running"
523
+ ? h("span", { style: { fontSize: 11, color: color, border: "1px solid " + color, borderRadius: 999, padding: "0 6px", lineHeight: "16px", flex: "0 0 auto", opacity: 0.9 } }, statusStep(status))
524
+ : null
525
+ ),
526
+ sub
527
+ ? h("div", { style: { fontSize: 11, color: STATUS_MUTED, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" } }, sub)
528
+ : null
529
+ ),
530
+ view === "running"
531
+ ? h("span", { "aria-live": "off", style: { fontSize: 11, color: STATUS_MUTED, fontVariantNumeric: "tabular-nums", flex: "0 0 auto" } }, clockText(startedAt))
532
+ : null,
533
+ view === "running" && typeof cancel === "function"
534
+ ? h("button", { type: "button", onClick: handleCancel, style: ghost, title: "取消压缩(历史保持原样,排队的消息会保留)" }, "取消")
535
+ : null,
536
+ view !== "running"
537
+ ? h("button", { type: "button", onClick: handleDismiss, style: close, "aria-label": "关闭状态提示", title: "关闭" }, "×")
538
+ : null
539
+ ),
540
+ view === "running"
541
+ ? h("div", { style: { height: 3, borderRadius: 99, background: "rgba(127,133,140,.28)", overflow: "hidden" } },
542
+ h("div", {
543
+ className: "dsh-handoff-fill",
544
+ style: {
545
+ height: "100%",
546
+ width: progress + "%",
547
+ borderRadius: 99,
548
+ background: color,
549
+ position: "relative",
550
+ overflow: "hidden",
551
+ transition: "width .25s ease",
552
+ },
553
+ })
554
+ )
555
+ : null
556
+ );
557
+ }
558
+
559
+ function ThemePreview(props) {
560
+ var style = normalizeDisplayStyle(props.theme);
561
+ var chatChildren = [
562
+ h("div", { key: "user", style: { marginLeft: "auto", maxWidth: "62%", border: "1px solid rgba(76,141,255,.24)", background: "rgba(76,141,255,.14)", borderRadius: "7px 7px 2px 7px", padding: "4px 6px", fontSize: 9, color: "#d9dbde" } }, "请完整读取这个文件并总结…"),
563
+ h("div", { key: "lines", style: { display: "flex", flexDirection: "column", gap: 4, marginTop: 6 } },
564
+ h("i", { key: "l1", style: { height: 4, borderRadius: 99, background: C.line } }),
565
+ h("i", { key: "l2", style: { height: 4, borderRadius: 99, background: C.line, width: "88%" } }),
566
+ h("i", { key: "l3", style: { height: 4, borderRadius: 99, background: C.line, width: "62%" } })
567
+ )
568
+ ];
569
+ var overlay = null;
570
+ if (style === "ring") {
571
+ overlay = h("div", { style: { position: "absolute", inset: 0, display: "flex", alignItems: "center", justifyContent: "center" } },
572
+ h("div", { style: { width: 104, border: "1px solid " + C.line, borderRadius: 8, background: "#23252a", padding: "7px 6px", display: "flex", flexDirection: "column", alignItems: "center", gap: 3, textAlign: "center" } },
573
+ h("div", { style: { position: "relative", width: 28, height: 28 } },
574
+ h("div", { style: { width: 28, height: 28, borderRadius: "50%", background: "conic-gradient(" + C.accent + " 0 66%, #2c2e33 66% 100%)", WebkitMask: "radial-gradient(circle, transparent 52%, #000 55%)", mask: "radial-gradient(circle, transparent 52%, #000 55%)" } }),
575
+ h("div", { style: { position: "absolute", inset: 0, display: "grid", placeItems: "center", fontSize: 7.5, fontFamily: "ui-monospace,Consolas,monospace" } }, "2/3")
576
+ ),
577
+ h("div", { style: { fontSize: 8.5, fontWeight: 500 } }, "正在压缩历史会话"),
578
+ h("div", { style: { fontSize: 7.5, color: C.muted } }, "00:42 · 消息会排队")
579
+ )
580
+ );
581
+ } else if (style === "orbit") {
582
+ overlay = h("div", { style: { position: "absolute", inset: 0, display: "flex", alignItems: "center", justifyContent: "center" } },
583
+ h("div", { style: { width: 104, border: "1px solid " + C.line, borderRadius: 8, background: "#23252a", padding: "7px 6px", display: "flex", flexDirection: "column", alignItems: "center", gap: 4, textAlign: "center" } },
584
+ h("div", { className: "dsh-handoff-mini-orbit" },
585
+ h("span", { style: { position: "absolute", left: "50%", top: "50%", width: 4, height: 4, margin: "-2px 0 0 -2px", borderRadius: "50%", background: C.accent } })
586
+ ),
587
+ h("div", { style: { fontSize: 8.5, fontWeight: 500 } }, "正在压缩历史会话…"),
588
+ h("div", { style: { fontSize: 7.5, color: C.muted } }, "00:42 · 消息会排队")
589
+ )
590
+ );
591
+ } else if (style === "topbar") {
592
+ overlay = h(React.Fragment, null,
593
+ h("div", { key: "line", style: { position: "absolute", top: 0, left: 0, right: 0, height: 2, background: "linear-gradient(90deg, " + C.accent + " 0 62%, " + C.line + " 62% 100%)" } }),
594
+ h("div", { key: "pillrow", style: { display: "flex", justifyContent: "center", paddingTop: 5 } },
595
+ h("span", { style: { display: "inline-flex", alignItems: "center", gap: 4, height: 16, padding: "0 6px", borderRadius: 999, border: "1px solid rgba(76,141,255,.45)", background: "#23252a", fontSize: 8 } },
596
+ h("span", { className: "dsh-handoff-pulse", style: { width: 4, height: 4, borderRadius: "50%", background: C.accent } }),
597
+ "压缩中 · 2/3 · 00:42"
598
+ )
599
+ )
600
+ );
601
+ } else if (style === "corner") {
602
+ overlay = h("div", { style: { position: "absolute", right: 8, top: 9, boxSizing: "border-box", width: 104, display: "flex", alignItems: "flex-start", gap: 4, padding: "4px 5px", borderRadius: 5, background: "#23252a", border: "1px solid " + C.line, borderLeft: "2px solid " + C.accent } },
603
+ h("span", { className: "dsh-handoff-pulse", style: { width: 4, height: 4, borderRadius: "50%", background: C.accent, marginTop: 4, flex: "0 0 auto" } }),
604
+ h("div", { style: { minWidth: 0 } },
605
+ h("div", { style: { fontSize: 7, fontWeight: 600, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" } }, "正在压缩历史会话"),
606
+ h("div", { style: { fontSize: 6.5, color: C.muted, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" } }, "00:42 · 2/3 写文档")
607
+ )
608
+ );
609
+ }
610
+ var strip = style === "strip"
611
+ ? h("div", { style: { margin: "0 8px 4px", border: "1px solid " + C.line, borderLeft: "2px solid " + C.accent, borderRadius: 5, background: "#212226", padding: "4px 6px" } },
612
+ h("div", { style: { display: "flex", alignItems: "center", gap: 5 } },
613
+ h("span", { className: "dsh-handoff-pulse", style: { width: 5, height: 5, borderRadius: "50%", background: C.accent } }),
614
+ h("span", { style: { fontSize: 9, fontWeight: 500 } }, "正在压缩历史会话"),
615
+ h("span", { style: { marginLeft: "auto", fontSize: 8, color: C.accent } }, "2/3")
616
+ ),
617
+ h("div", { style: { fontSize: 8, color: C.muted, marginTop: 1 } }, "10,735 / 30,000 · 消息会排队"),
618
+ h("div", { style: { height: 2, borderRadius: 99, background: C.line, marginTop: 3, overflow: "hidden" } },
619
+ h("div", { style: { width: "64%", height: "100%", background: C.accent, borderRadius: 99 } })
620
+ )
621
+ )
622
+ : null;
623
+ return h("div", { style: { marginTop: 9, border: "1px solid " + C.line, borderRadius: 8, background: "#151619", overflow: "hidden" } },
624
+ h("div", { style: { height: 128, display: "flex", flexDirection: "column" } },
625
+ h("div", { style: { height: 24, flex: "0 0 24px", display: "flex", alignItems: "center", gap: 6, padding: "0 8px", borderBottom: "1px solid " + C.line, background: "#191a1d" } },
626
+ h("span", { style: { width: 5, height: 5, borderRadius: "50%", background: C.ok } }),
627
+ h("span", { style: { fontSize: 9.5, fontWeight: 500, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" } }, "读取大文件并提取校验串"),
628
+ h("span", { style: { marginLeft: "auto", fontSize: 8.5, color: C.muted, border: "1px solid " + C.line, borderRadius: 999, padding: "0 6px" } }, "轻量本地")
629
+ ),
630
+ h("div", { style: { flex: 1, minHeight: 0, padding: (style === "topbar" ? 22 : 7) + "px 8px 7px", position: "relative", overflow: "hidden" } },
631
+ (style === "topbar" || style === "corner") ? overlay : null,
632
+ chatChildren,
633
+ (style === "ring" || style === "orbit") ? overlay : null
634
+ ),
635
+ strip,
636
+ h("div", { style: { flex: "0 0 26px", display: "flex", alignItems: "center", gap: 6, padding: "4px 8px", borderTop: "1px solid " + C.line, background: "#191a1d" } },
637
+ h("div", { style: { flex: 1, height: 18, border: "1px solid " + C.line, borderRadius: 5, background: "#111214", color: C.faint, fontSize: 8, display: "flex", alignItems: "center", padding: "0 6px", whiteSpace: "nowrap", overflow: "hidden" } }, "发消息或创建任务…"),
638
+ h("span", { style: { width: 16, height: 16, borderRadius: "50%", background: C.accent, flex: "0 0 auto" } })
639
+ )
640
+ )
641
+ );
642
+ }
643
+
644
+ function ToggleSwitch(props) {
645
+ var on = props.checked === true;
646
+ return h("button", {
647
+ type: "button",
648
+ role: "switch",
649
+ "aria-checked": on ? "true" : "false",
650
+ "aria-label": props.label,
651
+ onClick: function () { props.onChange(!on); },
652
+ style: {
653
+ position: "relative",
654
+ width: 44,
655
+ height: 24,
656
+ flex: "0 0 auto",
657
+ padding: 0,
658
+ borderRadius: 999,
659
+ border: "1px solid " + (on ? "rgba(76,141,255,.7)" : C.line),
660
+ background: on ? C.accent : "#3a3d42",
661
+ cursor: "pointer",
662
+ transition: "background .16s ease, border-color .16s ease",
663
+ },
664
+ },
665
+ h("span", {
666
+ "aria-hidden": "true",
667
+ style: {
668
+ position: "absolute",
669
+ top: 2,
670
+ left: on ? 22 : 2,
671
+ width: 18,
672
+ height: 18,
673
+ borderRadius: "50%",
674
+ background: "#fff",
675
+ boxShadow: "0 1px 3px rgba(0,0,0,.4)",
676
+ transition: "left .16s ease",
677
+ },
678
+ })
679
+ );
680
+ }
681
+
682
+ function Section(props) {
683
+ var connection = props.connection;
684
+ var loaded = React.useState(null);
685
+ var settings = loaded[0], setSettings = loaded[1];
686
+ var draftState = React.useState(null);
687
+ var draft = draftState[0], setDraft = draftState[1];
688
+ var statusState = React.useState({ loading: true, error: "", saving: false, saved: false });
689
+ var status = statusState[0], setStatus = statusState[1];
690
+ ensureStatusStyles();
691
+
692
+ React.useEffect(function () {
693
+ var alive = true;
694
+ if (!connection) { setStatus({ loading: false, error: "连接服务不可用", saving: false, saved: false }); return; }
695
+ call(connection, "get").then(function (value) {
696
+ if (!alive) return;
697
+ setSettings(value); setDraft(value);
698
+ publishDisplayStyle(value && value.displayStyle);
699
+ setStatus({ loading: false, error: "", saving: false, saved: false });
700
+ }).catch(function (error) {
701
+ if (alive) setStatus({ loading: false, error: String(error && error.message || error), saving: false, saved: false });
702
+ });
703
+ return function () { alive = false; };
704
+ }, [connection]);
705
+
706
+ if (status.loading) return h("div", { style: { color: C.muted, padding: 18, fontSize: 13 } }, "读取设置中…");
707
+ if (status.error && !draft) return h("div", { style: { color: "#e0b341", padding: 18, fontSize: 13, lineHeight: 1.8 } }, "无法读取设置:" + status.error);
708
+
709
+ var base = draft || { enabled: true, thresholdRatio: 0.9, scope: "global", presets: [], templateFile: "", displayStyle: "orbit" };
710
+ var enabled = base.enabled !== false;
711
+ var ratio = Math.round((Number(base.thresholdRatio) || 0.9) * 100);
712
+ var scopePreset = base.scope === "preset";
713
+ var presets = Array.isArray(base.presets) ? base.presets : [];
714
+ var template = base.templateFile || "";
715
+ var defaultTemplate = base.defaultTemplate || DEFAULT_TEMPLATE;
716
+ var themeStyle = normalizeDisplayStyle(base.displayStyle);
717
+
718
+ var budget = EXAMPLE_WINDOW - EXAMPLE_RESERVED;
719
+ var ceiling = budget - EXAMPLE_HEADROOM;
720
+ var eff = Math.min(Math.round(budget * ratio / 100), ceiling);
721
+ var capped = Math.round(budget * ratio / 100) > ceiling;
722
+ var dirty = settings === null || (settings.enabled !== false) !== enabled ||
723
+ Math.round((settings.thresholdRatio || 0.9) * 100) !== ratio ||
724
+ settings.scope !== base.scope ||
725
+ JSON.stringify(settings.presets || []) !== JSON.stringify(presets) ||
726
+ (settings.templateFile || "") !== template ||
727
+ normalizeDisplayStyle(settings.displayStyle) !== themeStyle;
728
+
729
+ function patch(next) { setDraft(Object.assign({}, base, next)); setStatus({ loading: false, error: "", saving: false, saved: false }); }
730
+ function setPreset(id) {
731
+ var next = presets.indexOf(id) >= 0 ? presets.filter(function (v) { return v !== id; }) : presets.concat([id]);
732
+ patch({ presets: next });
733
+ }
734
+ function save() {
735
+ setStatus({ loading: false, error: "", saving: true, saved: false });
736
+ call(connection, "set", {
737
+ enabled: enabled,
738
+ thresholdRatio: ratio,
739
+ scope: scopePreset ? "preset" : "global",
740
+ presets: presets,
741
+ templateFile: template,
742
+ displayStyle: themeStyle,
743
+ }).then(function (value) {
744
+ setSettings(value); setDraft(value);
745
+ publishDisplayStyle(value && value.displayStyle);
746
+ setStatus({ loading: false, error: "", saving: false, saved: true });
747
+ }).catch(function (error) {
748
+ setStatus({ loading: false, error: String(error && error.message || error), saving: false, saved: false });
749
+ });
750
+ }
751
+ function discard() { setDraft(settings); setStatus({ loading: false, error: "", saving: false, saved: false }); }
752
+ function open(kind) {
753
+ call(connection, "open", { kind: kind, path: template }).then(function () {
754
+ setStatus({ loading: false, error: "", saving: false, saved: false, note: "已打开" });
755
+ }).catch(function (error) {
756
+ setStatus({ loading: false, error: String(error && error.message || error), saving: false, saved: false });
757
+ });
758
+ }
759
+
760
+ var rowStyle = { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 16, padding: "14px 0", borderBottom: "1px solid " + C.line };
761
+ var labelStyle = { fontSize: 13.5, fontWeight: 600, color: C.text };
762
+ var subStyle = { fontSize: 12, color: C.muted, marginTop: 3, lineHeight: 1.6 };
763
+ var newBadgeStyle = { marginLeft: 6, fontSize: 10.5, color: "#8a5a00", background: "#fff3d6", border: "1px solid #f1d48a", borderRadius: 999, padding: "0 6px", lineHeight: "16px", fontWeight: 400 };
764
+ var btnBase = { border: "1px solid " + C.line, background: "#191a1d", color: C.text, fontSize: 12, padding: "6px 11px", borderRadius: 7, cursor: "pointer", font: "inherit", whiteSpace: "nowrap" };
765
+ var btnPrimary = Object.assign({}, btnBase, { borderColor: "rgba(76,141,255,.55)", background: "rgba(76,141,255,.14)" });
766
+ var seg = function (active, label, onClick) {
767
+ return h("button", {
768
+ type: "button", onClick: onClick,
769
+ style: { border: 0, cursor: "pointer", font: "inherit", fontSize: 12.5, padding: "7px 14px", background: active ? "rgba(76,141,255,.16)" : "#191a1d", color: active ? C.text : C.muted },
770
+ }, label);
771
+ };
772
+
773
+ var offBadgeStyle = { marginLeft: 6, fontSize: 10.5, color: C.muted, background: "rgba(127,133,140,.14)", border: "1px solid " + C.line, borderRadius: 999, padding: "0 6px", lineHeight: "16px", fontWeight: 400 };
774
+
775
+ return h("div", { style: { display: "flex", flexDirection: "column", paddingBottom: 12 } },
776
+
777
+ h("div", { style: rowStyle },
778
+ h("div", null,
779
+ h("div", { style: labelStyle }, "启用插件", !enabled ? h("span", { style: offBadgeStyle }, "已停用") : null),
780
+ h("div", { style: subStyle }, enabled
781
+ ? "达到触发点后,等 agent 空闲时把历史折叠成一份交接文档;压缩由本插件接管。"
782
+ : "关闭后压缩交回 DSH 原生实现:按原生阈值自动折叠,也可继续用 /compact 手动压缩。")
783
+ ),
784
+ h(ToggleSwitch, {
785
+ checked: enabled,
786
+ label: "启用插件",
787
+ onChange: function (value) { patch({ enabled: value }); },
788
+ })
789
+ ),
790
+
791
+ h("div", { style: rowStyle },
792
+ h("div", null,
793
+ h("div", { style: labelStyle }, "生效范围"),
794
+ h("div", { style: subStyle }, scopePreset ? "仅勾选的预设切到插件并关闭其原生压缩;其余预设保持原生" : "所有带 handoff slot 的可编辑预设都切到插件,共用这份设置")
795
+ ),
796
+ h("div", { style: { display: "inline-flex", border: "1px solid " + C.line, borderRadius: 8, overflow: "hidden" } },
797
+ seg(!scopePreset, "全局生效", function () { patch({ scope: "global" }); }),
798
+ seg(scopePreset, "按预设生效", function () { patch({ scope: "preset" }); })
799
+ )
800
+ ),
801
+ scopePreset ? h("div", { style: { display: "flex", gap: 8, flexWrap: "wrap", padding: "10px 0", borderBottom: "1px solid " + C.line } },
802
+ PRESETS.map(function (p) {
803
+ var on = presets.indexOf(p.id) >= 0;
804
+ return h("button", {
805
+ key: p.id, type: "button", onClick: function () { setPreset(p.id); },
806
+ style: { fontSize: 12, padding: "5px 11px", borderRadius: 999, cursor: "pointer", font: "inherit", border: "1px solid " + (on ? C.accent : C.line), background: on ? "rgba(76,141,255,.12)" : "#191a1d", color: on ? C.text : C.muted },
807
+ }, p.label);
808
+ })
809
+ ) : null,
810
+
811
+ h("div", { style: { padding: "12px 0", borderBottom: "1px solid " + C.line } },
812
+ h("div", null,
813
+ h("div", { style: labelStyle }, "压缩显示方式", h("span", { style: newBadgeStyle }, "新")),
814
+ h("div", { style: subStyle }, "选择压缩进行时在会话里的提示样式;保存后下一次压缩生效。")
815
+ ),
816
+ h("div", { style: { display: "flex", gap: 4, flexWrap: "wrap", padding: 3, border: "1px solid " + C.line, borderRadius: 8, background: "#191a1d", width: "fit-content", maxWidth: "100%", marginTop: 9 } },
817
+ DISPLAY_OPTIONS.map(function (option) {
818
+ var on = themeStyle === option.id;
819
+ return h("button", {
820
+ key: option.id, type: "button",
821
+ onClick: function () { patch({ displayStyle: option.id }); },
822
+ "aria-pressed": on ? "true" : "false",
823
+ style: { display: "inline-flex", alignItems: "center", gap: 6, height: 30, padding: "0 10px", border: 0, borderRadius: 6, background: on ? "rgba(76,141,255,.16)" : "transparent", color: on ? C.text : C.muted, cursor: "pointer", font: "inherit", fontSize: 12.5, whiteSpace: "nowrap" },
824
+ }, h("span", { className: "dsh-handoff-opt dsh-handoff-opt-" + option.id, "aria-hidden": "true" }), option.label);
825
+ })
826
+ ),
827
+ h(ThemePreview, { theme: themeStyle })
828
+ ),
829
+
830
+ h("div", { style: rowStyle },
831
+ h("div", null,
832
+ h("div", { style: labelStyle }, "压缩触发点"),
833
+ h("div", { style: subStyle }, "上下文用到「可用输入预算」的这个比例后,等当前回合结束再压缩")
834
+ ),
835
+ h("div", { style: { display: "flex", alignItems: "center", gap: 12 } },
836
+ h("input", {
837
+ type: "range", min: 0, max: 100, step: 1, value: ratio, "aria-label": "压缩触发点",
838
+ style: { width: 220, accentColor: C.accent },
839
+ onChange: function (e) { patch({ thresholdRatio: snapPercent(e.target.value) / 100 }); },
840
+ }),
841
+ h("div", { style: { display: "flex", alignItems: "center", border: "1px solid " + C.line, borderRadius: 8, background: "#151619", height: 32, overflow: "hidden" } },
842
+ h("input", {
843
+ type: "number", min: 0, max: 100, value: ratio, "aria-label": "压缩触发点百分比",
844
+ style: { width: 52, height: 30, border: 0, background: "transparent", color: C.text, textAlign: "right", padding: "0 4px 0 10px", fontSize: 13, outline: "none" },
845
+ onChange: function (e) {
846
+ var raw = e.target.value;
847
+ if (raw === "") return;
848
+ patch({ thresholdRatio: clampPercent(raw) / 100 });
849
+ },
850
+ }),
851
+ h("span", { style: { color: C.muted, fontSize: 12, padding: "0 9px 0 2px" } }, "%")
852
+ )
853
+ )
854
+ ),
855
+ h("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 16, padding: "10px 0", borderBottom: "1px solid " + C.line } },
856
+ h("div", { style: subStyle },
857
+ "示例:窗口 250K、输出预留 20K、headroom 10K → 预算 " + fmt(budget) + ";" + ratio + "% ≈ " + fmt(eff) + " tokens" +
858
+ (capped ? "(已封顶,再往上不会更晚触发)" : "(距输入上限余 " + fmt(budget - eff) + ")")
859
+ ),
860
+ h("div", { style: { fontSize: 20, fontWeight: 700, color: C.text, fontVariantNumeric: "tabular-nums" } }, ratio + "%")
861
+ ),
862
+
863
+ h("div", { style: { paddingTop: 14 } },
864
+ h("div", { style: labelStyle }, "交接模板"),
865
+ h("div", { style: Object.assign({}, subStyle, { marginBottom: 8 }) }, "压缩时让模型按这份模板写交接文档;每次压缩时读取,改完不用重启。"),
866
+ h("div", { style: { display: "flex", alignItems: "center", gap: 8, background: "#151619", border: "1px solid " + C.line, borderRadius: 8, padding: "7px 10px" } },
867
+ h("input", {
868
+ value: template, placeholder: defaultTemplate, "aria-label": "模板文件路径",
869
+ onChange: function (e) { patch({ templateFile: e.target.value }); },
870
+ style: { flex: 1, border: 0, background: "transparent", color: "#c9ced6", fontFamily: "ui-monospace,Consolas,monospace", fontSize: 12, outline: "none" },
871
+ }),
872
+ h("button", { type: "button", style: btnBase, onClick: function () { open("file"); } }, "打开文件"),
873
+ h("button", { type: "button", style: btnBase, onClick: function () { open("folder"); } }, "所在文件夹")
874
+ ),
875
+ h("div", { style: { marginTop: 8, fontSize: 11.5, color: C.faint, lineHeight: 1.6 } },
876
+ "留空使用默认路径;文件不存在时引擎会按内置模板生成一份。")
877
+ ),
878
+
879
+ h("div", { style: { marginTop: 14, background: C.panel, border: "1px solid " + C.line, borderRadius: 10, padding: "14px 16px" } },
880
+ h("div", { style: { fontSize: 12.5, fontWeight: 600, color: C.text, marginBottom: 8 } }, "它是怎么压缩的"),
881
+ h("ol", { style: { margin: 0, paddingLeft: 18, color: C.muted, fontSize: 12.5, lineHeight: 1.85 } },
882
+ h("li", null, "到点后不立即动手,等 agent 结束当前回合。"),
883
+ h("li", null, "用会话自己的前缀(命中缓存)让模型按模板写交接文档,不额外全量 prefill。"),
884
+ h("li", null, "把整段历史替换成这一条消息;旧消息仍在会话日志里,rewind 可回滚。"),
885
+ h("li", null, "下一轮只剩 系统提示词 + 工具定义 + 交接文档。")
886
+ ),
887
+ h("div", { style: { marginTop: 8, fontSize: 11.5, color: C.faint } }, "手动压缩:会话里发送 /compact 可随时折叠一次,不受这个比例限制。")
888
+ ),
889
+
890
+ h("div", { style: { display: "flex", alignItems: "center", gap: 10, marginTop: 14 } },
891
+ h("button", { type: "button", style: dirty ? btnPrimary : Object.assign({}, btnBase, { opacity: 0.55 }), onClick: dirty ? save : undefined }, status.saving ? "保存中…" : "保存"),
892
+ h("button", { type: "button", style: btnBase, onClick: discard }, "撤销改动"),
893
+ status.note ? h("span", { style: { fontSize: 12, color: C.ok } }, status.note) : status.saved ? h("span", { style: { fontSize: 12, color: C.ok } }, "已保存,下次判定生效") : null,
894
+ status.error ? h("span", { style: { fontSize: 12, color: "#e5534b" } }, status.error) : null
895
+ )
896
+ );
897
+ }
898
+
899
+ function apply(ctx) {
900
+ var slots = ctx.slots;
901
+ var connection = ctx.connection;
902
+
903
+ try {
904
+ slots.inject("settings.section", function () {
905
+ return slots.register({
906
+ name: "settings.section",
907
+ id: "auto-compaction",
908
+ order: 14,
909
+ label: function () { return "会话压缩"; },
910
+ inject: function () { return {}; },
911
+ }, function SectionView() { return h(Section, { connection: connection }); });
912
+ });
913
+ } catch (error) {
914
+ console.warn("[handoff-ui] settings section registration failed", error);
915
+ }
916
+
917
+ try {
918
+ var sessions;
919
+ try { sessions = ctx.sessions; } catch (error) { sessions = undefined; }
920
+ if (!sessions && typeof ctx.get === "function") {
921
+ try { sessions = ctx.get("sessions"); } catch (error) { sessions = undefined; }
922
+ }
923
+ function cancelFor(sessionId) {
924
+ if (!sessions || typeof sessions.scope !== "function") return undefined;
925
+ try {
926
+ var scope = sessions.scope(sessionId);
927
+ if (!scope || typeof scope.get !== "function") return undefined;
928
+ var conversation = scope.get("conversation");
929
+ if (!conversation || typeof conversation.cancel !== "function") return undefined;
930
+ return function () { return conversation.cancel(); };
931
+ } catch (error) {
932
+ return undefined;
933
+ }
934
+ }
935
+ slots.inject("conversation.input.dock", function () {
936
+ return slots.register({
937
+ name: "conversation.input.dock",
938
+ id: "auto-compaction-status",
939
+ order: 10,
940
+ inject: function (sessionId) {
941
+ return { sessionId: sessionId, cancel: cancelFor(sessionId) };
942
+ },
943
+ }, function StatusDockView(slotProps) {
944
+ return h(StatusDock, {
945
+ connection: connection,
946
+ sessionId: slotProps.sessionId,
947
+ cancel: slotProps.cancel,
948
+ });
949
+ });
950
+ });
951
+ } catch (error) {
952
+ console.warn("[handoff-ui] status dock registration failed", error);
953
+ }
954
+ }
955
+
956
+ exports.name = name;
957
+ exports.inject = inject;
958
+ exports.apply = apply;
959
+ return module.exports;
960
+ },
961
+ });