dsh-retrace 0.4.20 → 0.4.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -0
- package/README.zh.md +15 -0
- package/bin/retrace.mjs +0 -0
- package/lib/adapter/dsh.js +98 -2
- package/lib/client.bundle.js +381 -6
- package/lib/client.js +317 -5
- package/lib/close-guard-client.js +163 -0
- package/lib/close-guard.js +145 -0
- package/lib/dynamic-client.js +381 -6
- package/lib/dynamic-host.js +135 -14
- package/lib/host-core.js +135 -14
- package/lib/http.js +63 -3
- package/lib/index.js +37 -4
- package/package.json +9 -7
package/README.md
CHANGED
|
@@ -77,6 +77,21 @@ Full steps in [📦 Installation](#-installation).
|
|
|
77
77
|
| 🧭 | **Jump-to-conversation** | one click from a version to that point in the conversation (auto-loads history, anchor highlight) |
|
|
78
78
|
| 🧹 | **Bounded storage** | snapshots keep the most recent N versions (default 50); throttled background sweep prunes truncated ones |
|
|
79
79
|
|
|
80
|
+
**Close guard (don't lose work by accident)** — before you exit or reload, know what is still running:
|
|
81
|
+
|
|
82
|
+
| | What | |
|
|
83
|
+
|---|---|---|
|
|
84
|
+
| 🛡️ | **Running-work detection** | every session is scanned for live work: agent running, queued inbox items, background jobs, unclosed turns |
|
|
85
|
+
| 📋 | **Running banner** | sessions with live work show a persistent in-page banner (short session code + reasons), so you can see it before quitting |
|
|
86
|
+
| ⚠️ | **Exit prompt** | on plugin dispose (app exit / reload) a Chinese notice lists each running session and why it is considered busy — it only warns, it never cancels your running agent |
|
|
87
|
+
| 🔒 | **Page-close interception (Web)** | `beforeunload` interception: a strong confirm when work is running (details modal, `[仍关闭]` = confirm-and-go), a light confirm otherwise |
|
|
88
|
+
| 🔎 | **Query surface** | `retrace.runningState` (host RPC) + `GET|POST /api/plugins/retrace/runningState` (HTTP) — same shape on both transports |
|
|
89
|
+
|
|
90
|
+
> Desktop note: the Electron shell destroys the window on quit, so the page-level
|
|
91
|
+
> `beforeunload` hook cannot fire there and the host exposes no plugin quit-veto seam —
|
|
92
|
+
> Desktop is covered by the running banner plus the dispose notice; Web gets the full
|
|
93
|
+
> interception.
|
|
94
|
+
|
|
80
95
|
**Why it's different** (the interaction layer — the guarantees above are the storage layer):
|
|
81
96
|
|
|
82
97
|
- 🎯 **Whole-round recall** — removes the input *and* its output (tool rows included), not just a single bubble.
|
package/README.zh.md
CHANGED
|
@@ -233,6 +233,21 @@ Client 半区会依据包内 `dsh.client` 元数据被自动打包进 Web 客户
|
|
|
233
233
|
|
|
234
234
|
---
|
|
235
235
|
|
|
236
|
+
**关闭守卫(防误关丢进度)** —— 退出/重载前先看清还有什么在跑:
|
|
237
|
+
|
|
238
|
+
| | 是什么 | |
|
|
239
|
+
|---|---|---|
|
|
240
|
+
| 🛡️ | **运行中检测** | 逐会话扫描运行中工作:agent 正在跑 / inbox 排队 / 后台 jobs / 未闭合轮 |
|
|
241
|
+
| 📋 | **运行中横幅** | 有运行中工作的会话显示页面常驻横幅(会话短码 + 原因),退出前可见 |
|
|
242
|
+
| ⚠️ | **退出提示** | 插件 dispose(应用退出/重载)时中文提示列出每个运行中会话与原因——只提示,绝不代你取消 agent |
|
|
243
|
+
| 🔒 | **页面关闭拦截(Web)** | `beforeunload` 拦截:有运行中任务强确认(明细模态,`[仍关闭]` 即确认离开),无任务轻确认 |
|
|
244
|
+
| 🔎 | **查询面** | `retrace.runningState`(host RPC)+ `GET|POST /api/plugins/retrace/runningState`(HTTP),两入口同形状 |
|
|
245
|
+
|
|
246
|
+
> 桌面说明:Electron 宿主退出时销毁窗口,页面 `beforeunload` 不会触发,宿主也未暴露
|
|
247
|
+
> 插件可用的退出否决点——桌面侧由运行中横幅 + dispose 提示覆盖;Web 端拦截完整生效。
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
236
251
|
## 🗺️ 路线图
|
|
237
252
|
|
|
238
253
|
**当前已具备(0.4.x):**
|
package/bin/retrace.mjs
CHANGED
|
File without changes
|
package/lib/adapter/dsh.js
CHANGED
|
@@ -41,13 +41,37 @@ async function readEvents(sessionId) {
|
|
|
41
41
|
return readEventsFromFile(filePath)
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
/** 从指定文件路径读全量事件(测试可注入路径;生产走 readEvents 找 ~/.dsh)。
|
|
44
|
+
/** 从指定文件路径读全量事件(测试可注入路径;生产走 readEvents 找 ~/.dsh)。
|
|
45
|
+
* P0-6(代码 M-5):单次编辑/查询操作会连读多次(span+文件快照事实),每次全量
|
|
46
|
+
* zstd 解压+JSON+foldSurface(1.37M 事件秒级)→ 加**进程级短缓存**(同批次
|
|
47
|
+
* 内共享一次读取;以 mtime+size 校验文件未变,变了即失效,防与写入撕裂)。
|
|
48
|
+
* TTL 很短(300ms)只覆盖单次命令的连续读取,不缓存跨命令的陈旧数据。 */
|
|
49
|
+
const readCache = new Map() // filePath → { sig, events, at }
|
|
50
|
+
const READ_CACHE_TTL_MS = 300
|
|
45
51
|
async function readEventsFromFile(filePath) {
|
|
46
52
|
try {
|
|
47
53
|
if (!filePath) return null
|
|
54
|
+
const { statSync } = await import('node:fs')
|
|
55
|
+
// 文件签名(mtimeMs+size):写入会改 mtime/size → 缓存自动失效
|
|
56
|
+
let sig = null
|
|
57
|
+
try {
|
|
58
|
+
const st = statSync(filePath)
|
|
59
|
+
sig = `${st.mtimeMs}:${st.size}`
|
|
60
|
+
} catch { sig = 'unreadable' }
|
|
61
|
+
const hit = readCache.get(filePath)
|
|
62
|
+
if (hit && hit.sig === sig && Date.now() - hit.at < READ_CACHE_TTL_MS) {
|
|
63
|
+
return hit.events
|
|
64
|
+
}
|
|
48
65
|
const { loadSessionLog } = await import('dsh-log-contract')
|
|
49
66
|
const log = loadSessionLog(filePath)
|
|
50
|
-
|
|
67
|
+
const events = log.events.map((r) => r.event)
|
|
68
|
+
// 只缓存有效读取(失败不缓存,下次重试)
|
|
69
|
+
if (Array.isArray(events) && events.length > 0) {
|
|
70
|
+
readCache.set(filePath, { sig, events, at: Date.now() })
|
|
71
|
+
// 防膨胀:超过 32 个文件清空(单命令最多几个会话文件)
|
|
72
|
+
if (readCache.size > 32) readCache.clear()
|
|
73
|
+
}
|
|
74
|
+
return events
|
|
51
75
|
} catch { return null }
|
|
52
76
|
}
|
|
53
77
|
|
|
@@ -108,6 +132,67 @@ export function computeSpan(events, target, mode = 'round') {
|
|
|
108
132
|
return { start: span[0], end: span[span.length - 1], shadowedSeqs: span }
|
|
109
133
|
}
|
|
110
134
|
|
|
135
|
+
/**
|
|
136
|
+
* 文件侧「目标同一轮的前置 user 原文」(M-1,独立审查 74e580d 后续)——regenerate
|
|
137
|
+
* 重发文本的唯一可靠来源。
|
|
138
|
+
*
|
|
139
|
+
* 为什么必须算在文件侧:host 内存 session.events 是窗口化/稀疏视图(带 undefined 洞),
|
|
140
|
+
* regenerate 在内存里「向前找最近的前置 user」会越过洞(洞里正是该轮 user)或越过被
|
|
141
|
+
* 遮蔽区间,选到**更早轮**的 user → 重发错文本 + marker targetSeq 指向错轮。round span
|
|
142
|
+
* 的起点在文件全量 events + 官方 foldSurface nodes 上就是目标同一轮的轮首 user
|
|
143
|
+
* (computeSpan round 模式向前找 isRoundBoundary 的落点),从该点单点取原文不可能跨轮。
|
|
144
|
+
*
|
|
145
|
+
* 纯函数(零平台 import)。
|
|
146
|
+
* @returns {{seq: number, text: string}|null} span 起点非轮边界 user(孤儿回复:向前
|
|
147
|
+
* 找不到 user;或非 round 模式)→ null(调用方保守处理:绝不重发更早轮的文本)。
|
|
148
|
+
*/
|
|
149
|
+
export function roundPromptOf(events, span, mode = 'round') {
|
|
150
|
+
if (mode !== 'round' || !span || !Array.isArray(events)) return null
|
|
151
|
+
const seq = span.start
|
|
152
|
+
if (typeof seq !== 'number' || !Number.isSafeInteger(seq) || seq < 0) return null
|
|
153
|
+
const event = events[seq]
|
|
154
|
+
if (!isRoundBoundary(event)) return null
|
|
155
|
+
const content = event.data?.content
|
|
156
|
+
const text = Array.isArray(content)
|
|
157
|
+
? content.filter((b) => b && b.type === 'text' && typeof b.text === 'string').map((b) => b.text).join('')
|
|
158
|
+
: ''
|
|
159
|
+
return { seq, text }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* computeSpan + 文件快照事实(comm-200):一次遍历同时给出遮蔽 span 与
|
|
164
|
+
* 「文件快照已知最大 seq / 目标在快照里的 seq」。span 为 null 时,调用方
|
|
165
|
+
* (withFileSpan/http.js)把这些事实注入 args.spanFacts,host-core 据此区分:
|
|
166
|
+
* - 目标 seq > 文件快照最大 seq → 文件尚未 flush 该消息(提交中)→ message-pending;
|
|
167
|
+
* - 快照已覆盖目标位置却不在其 surface(span null)→ 真被遮蔽 → target-shadowed。
|
|
168
|
+
* 纯函数(与 computeSpan 同构,平台无关;零平台 import)。
|
|
169
|
+
*
|
|
170
|
+
* M-1(独立审查 74e580d 后续):同时带回 prompt = round span 起点 user 的原文
|
|
171
|
+
* (roundPromptOf)。span 命中时调用方把它注入 args.regeneratePrompt —— regenerate
|
|
172
|
+
* 的重发文本/ marker targetSeq 一律取自文件侧,不再直扫 host 稀疏 events
|
|
173
|
+
* (直扫会越过洞选中更早轮 user,重发错文本)。
|
|
174
|
+
*
|
|
175
|
+
* @returns {{span: object|null, facts: {fileMaxSeq: number, targetSeq: number},
|
|
176
|
+
* prompt: {seq: number, text: string}|null}}
|
|
177
|
+
*/
|
|
178
|
+
export function computeSpanProbe(events, target, mode = 'round', opts = {}) {
|
|
179
|
+
const span = computeSpan(events, target, mode, opts)
|
|
180
|
+
let targetSeq = typeof target === 'number' ? target : -1
|
|
181
|
+
let fileMaxSeq = -1
|
|
182
|
+
if (Array.isArray(events)) {
|
|
183
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
184
|
+
const ev = events[i]
|
|
185
|
+
if (!ev) continue
|
|
186
|
+
if (typeof ev.seq === 'number' && ev.seq > fileMaxSeq) fileMaxSeq = ev.seq
|
|
187
|
+
if (targetSeq === -1) {
|
|
188
|
+
const id = ev?.type === 'user/message' ? ev.data?.id : ev?.type === 'assistant/message' ? ev.data?.message?.id : undefined
|
|
189
|
+
if (typeof id === 'string' && id === target) targetSeq = ev.seq
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return { span, facts: { fileMaxSeq, targetSeq: targetSeq >= 0 ? targetSeq : -1 }, prompt: roundPromptOf(events, span, mode) }
|
|
194
|
+
}
|
|
195
|
+
|
|
111
196
|
/**
|
|
112
197
|
* DSH 适配器:EventReader 实现。
|
|
113
198
|
* 从文件读全量事件(可靠事实),并提供基于它的遮蔽计算。
|
|
@@ -119,6 +204,17 @@ export const dshAdapter = {
|
|
|
119
204
|
const events = await readEvents(sessionId)
|
|
120
205
|
return computeSpan(events, target, mode)
|
|
121
206
|
},
|
|
207
|
+
/**
|
|
208
|
+
* 便捷:读事件一次,同时给出 span 与文件快照事实(comm-200)。span 为 null 时
|
|
209
|
+
* 调用方(withFileSpan/http.js)注入 facts → host-core 区分「提交中(message-pending)」
|
|
210
|
+
* 与「真被遮蔽(target-shadowed)」——文件 flush 滞后不再误报「已不在活跃对话」。
|
|
211
|
+
* M-1(独立审查 74e580d 后续):probe.prompt = round span 起点 user 的**文件侧原文**
|
|
212
|
+
* ——regenerate 的重发文本/ marker targetSeq 取自这里,不直扫 host 稀疏 events。
|
|
213
|
+
*/
|
|
214
|
+
async spanProbeFromFile(sessionId, target, mode = 'round', opts = {}) {
|
|
215
|
+
const events = await readEvents(sessionId)
|
|
216
|
+
return computeSpanProbe(events, target, mode, opts)
|
|
217
|
+
},
|
|
122
218
|
/**
|
|
123
219
|
* 从文件全量事件算某 turn 内的最大 step 号(情形② marker step 分配用)。
|
|
124
220
|
* 绕开 host 窗口化 session.events(稀疏内存视图可能看不到 turn 内全部 step,
|
package/lib/client.bundle.js
CHANGED
|
@@ -27,8 +27,11 @@ var client_exports = {};
|
|
|
27
27
|
__export(client_exports, {
|
|
28
28
|
__setMessageEditorWire: () => __setMessageEditorWire,
|
|
29
29
|
apply: () => apply,
|
|
30
|
+
en: () => en,
|
|
30
31
|
inject: () => inject,
|
|
31
|
-
name: () => name
|
|
32
|
+
name: () => name,
|
|
33
|
+
opFailureText: () => opFailureText,
|
|
34
|
+
zh: () => zh
|
|
32
35
|
});
|
|
33
36
|
module.exports = __toCommonJS(client_exports);
|
|
34
37
|
var import_react = require("react");
|
|
@@ -69,6 +72,98 @@ function sessionBadge(sessionId) {
|
|
|
69
72
|
return toBase36(fnv1a64(uuid)).padStart(10, "0").slice(0, 10);
|
|
70
73
|
}
|
|
71
74
|
|
|
75
|
+
// lib/close-guard-client.js
|
|
76
|
+
var GUARD_POLL_MS = 5e3;
|
|
77
|
+
var GUARD_TTL_MS = 3e4;
|
|
78
|
+
var GUARD_ARM_TTL_MS = 3e4;
|
|
79
|
+
function localeOf(locale) {
|
|
80
|
+
return locale === "en" ? "en" : "zh";
|
|
81
|
+
}
|
|
82
|
+
function parseReason(reason) {
|
|
83
|
+
const raw = typeof reason === "string" ? reason : "";
|
|
84
|
+
let match;
|
|
85
|
+
if (raw === "agent-running") return { code: "agent-running" };
|
|
86
|
+
if (match = raw.match(/^queued-(\d+)$/)) return { code: "queued", count: Number(match[1]) };
|
|
87
|
+
if (match = raw.match(/^jobs-(\d+)$/)) return { code: "jobs", count: Number(match[1]) };
|
|
88
|
+
if (match = raw.match(/^unclosed-turn-(.+)$/)) {
|
|
89
|
+
const turns = match[1].split(",").map((t) => Number(t)).filter((t) => Number.isInteger(t));
|
|
90
|
+
return { code: "unclosed-turn", turns };
|
|
91
|
+
}
|
|
92
|
+
return { code: "other", raw };
|
|
93
|
+
}
|
|
94
|
+
function describeReason(reason, locale = "zh") {
|
|
95
|
+
const lang = localeOf(locale);
|
|
96
|
+
const r = parseReason(reason);
|
|
97
|
+
switch (r.code) {
|
|
98
|
+
case "agent-running":
|
|
99
|
+
return lang === "zh" ? "\u6B63\u5728\u8FD0\u884C" : "agent running";
|
|
100
|
+
case "queued":
|
|
101
|
+
return lang === "zh" ? `\u6392\u961F\u5F85\u529E ${r.count} \u6761` : `${r.count} queued`;
|
|
102
|
+
case "jobs":
|
|
103
|
+
return lang === "zh" ? `\u540E\u53F0\u4EFB\u52A1 ${r.count} \u4E2A` : `${r.count} background job${r.count === 1 ? "" : "s"}`;
|
|
104
|
+
case "unclosed-turn":
|
|
105
|
+
return lang === "zh" ? `\u672A\u95ED\u5408\u8F6E\u6B21 ${r.turns.join("\u3001")}` : `unclosed turn ${r.turns.join(",")}`;
|
|
106
|
+
default:
|
|
107
|
+
return r.raw || (lang === "zh" ? "\u8FD0\u884C\u4E2D" : "running");
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function sessionLine(item, { locale = "zh", label } = {}) {
|
|
111
|
+
const lang = localeOf(locale);
|
|
112
|
+
const id = label || item?.sessionId || "?";
|
|
113
|
+
const reasons = Array.isArray(item?.reasons) ? item.reasons : [];
|
|
114
|
+
const body = reasons.length > 0 ? reasons.map((r) => describeReason(r, lang)).join("; ") : lang === "zh" ? "\u8FD0\u884C\u4E2D(\u539F\u56E0\u672A\u8BC6\u522B)" : "running (reason unknown)";
|
|
115
|
+
return `- ${lang === "zh" ? "\u4F1A\u8BDD" : "session"} ${id}: ${body}`;
|
|
116
|
+
}
|
|
117
|
+
function runningLines(running, { locale = "zh", labelOf } = {}) {
|
|
118
|
+
const items = Array.isArray(running) ? running : [];
|
|
119
|
+
return items.map((item) => sessionLine(item, {
|
|
120
|
+
locale,
|
|
121
|
+
label: typeof labelOf === "function" ? labelOf(item.sessionId) : void 0
|
|
122
|
+
}));
|
|
123
|
+
}
|
|
124
|
+
function classifySnapshot(snapshot) {
|
|
125
|
+
if (!snapshot || !Array.isArray(snapshot.running)) return "unknown";
|
|
126
|
+
return snapshot.running.length > 0 ? "running" : "idle";
|
|
127
|
+
}
|
|
128
|
+
function isStale(snapshot, { ttlMs = GUARD_TTL_MS, now = Date.now() } = {}) {
|
|
129
|
+
return !snapshot || typeof snapshot.at !== "number" || now - snapshot.at > ttlMs;
|
|
130
|
+
}
|
|
131
|
+
function buildRunningCopy(snapshot, { locale = "zh", labelOf, now = Date.now(), ttlMs = GUARD_TTL_MS } = {}) {
|
|
132
|
+
const lang = localeOf(locale);
|
|
133
|
+
const running = Array.isArray(snapshot?.running) ? snapshot.running : [];
|
|
134
|
+
const count = running.length;
|
|
135
|
+
const head = lang === "zh" ? `\u6709 ${count} \u4E2A\u4F1A\u8BDD\u5B58\u5728\u8FD0\u884C\u4E2D\u4EFB\u52A1,\u5173\u95ED\u5C06\u4E2D\u65AD\u8FDB\u5EA6` : `${count} session${count === 1 ? "" : "s"} with running work \u2014 closing will interrupt progress`;
|
|
136
|
+
const lines = runningLines(running, { locale: lang, labelOf });
|
|
137
|
+
const stale = isStale(snapshot, { ttlMs, now });
|
|
138
|
+
const hint = stale ? lang === "zh" ? "(\u72B6\u6001\u540C\u6B65\u4E8E\u8F83\u65E9\u65F6\u523B,\u53EF\u80FD\u5DF2\u53D8\u5316)" : "(state may be stale)" : void 0;
|
|
139
|
+
return { head, lines, hint, stale };
|
|
140
|
+
}
|
|
141
|
+
function createGuardStore({ now = Date.now, armTtlMs = GUARD_ARM_TTL_MS } = {}) {
|
|
142
|
+
let snapshot = null;
|
|
143
|
+
let armedAt = 0;
|
|
144
|
+
return {
|
|
145
|
+
/** 设置最新 host 快照(runningSessions 形状 { running: [...] })。 */
|
|
146
|
+
set(next) {
|
|
147
|
+
snapshot = next && typeof next === "object" ? { ...next, at: next.at ?? now() } : null;
|
|
148
|
+
},
|
|
149
|
+
get() {
|
|
150
|
+
return snapshot;
|
|
151
|
+
},
|
|
152
|
+
/** 用户确认仍关闭 → 放行标记(二次触发有效)。 */
|
|
153
|
+
arm() {
|
|
154
|
+
armedAt = now();
|
|
155
|
+
},
|
|
156
|
+
/** 放行标记是否仍有效(未过期)。 */
|
|
157
|
+
isArmed() {
|
|
158
|
+
return armedAt > 0 && now() - armedAt <= armTtlMs;
|
|
159
|
+
},
|
|
160
|
+
/** 状态重变(运行中→静止→运行中)或手动重置时清除放行。 */
|
|
161
|
+
disarm() {
|
|
162
|
+
armedAt = 0;
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
72
167
|
// lib/client.js
|
|
73
168
|
var name = "dsh-retrace";
|
|
74
169
|
var inject = ["slots", "locale", "conversationEvents"];
|
|
@@ -163,6 +258,10 @@ var zh = {
|
|
|
163
258
|
"timeline.gitInitConfirm": "\u786E\u5B9A\u8981\u521D\u59CB\u5316 git \u5417\uFF1F",
|
|
164
259
|
"error.generic": "\u64CD\u4F5C\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5",
|
|
165
260
|
"error.busy": "\u8BF7\u5148\u505C\u6B62\u5F53\u524D\u56DE\u590D\u518D\u64CD\u4F5C",
|
|
261
|
+
// comm-199/200:不再原样透传 host 错误(曾为英文 "no longer part of the active
|
|
262
|
+
// conversation"——把「提交中」误报成「已不在活跃对话」)。按 code 本地化:
|
|
263
|
+
"error.targetShadowed": "\u8BE5\u6D88\u606F\u4F4D\u4E8E\u5DF2\u6298\u53E0\u5757(\u5386\u53F2\u53EA\u8BFB):\u5C55\u5F00\u8BE5\u5757\u540E\u7F16\u8F91,\u6216\u8FFD\u52A0\u65B0\u6D88\u606F\u4FEE\u8BA2",
|
|
264
|
+
"error.messagePending": "\u6D88\u606F\u751F\u6210\u4E2D,\u5B8C\u6210\u540E\u53EF\u7F16\u8F91",
|
|
166
265
|
"view.fork": "\u5206\u53C9",
|
|
167
266
|
"fork.title": "\u5206\u53C9\u56FE",
|
|
168
267
|
"fork.empty": "\u8FD8\u6CA1\u6709\u5206\u53C9\u3002\u64A4\u56DE / \u7F16\u8F91 / \u91CD\u65B0\u751F\u6210 / \u6062\u590D\u4F1A\u5728\u6B64\u4EA7\u751F\u5206\u53C9\u3002",
|
|
@@ -181,7 +280,9 @@ var zh = {
|
|
|
181
280
|
"fork.badge": "\u4F1A\u8BDD\u77ED\u7801",
|
|
182
281
|
"fork.badgeHint": "\u77ED\u7801\u7531\u4F1A\u8BDD\u552F\u4E00 id \u786E\u5B9A\u6027\u5BFC\u51FA\uFF0C\u6C38\u4E0D\u53D8\uFF08\u6807\u9898\u4F1A\u53D8\uFF09\u3002",
|
|
183
282
|
"fork.rename": "\u6539\u540D\u79F0",
|
|
184
|
-
"fork.renamePrompt": "\u8F93\u5165\u4F1A\u8BDD\u540D\u79F0\uFF08\u81EA\u52A8\u62FC\u63A5\u77ED\u7801\uFF0C\u8BBE\u7F6E\u540E\u56FA\u5B9A\u4E0D\u518D\u81EA\u52A8\u6539\u540D\uFF09\uFF1A"
|
|
283
|
+
"fork.renamePrompt": "\u8F93\u5165\u4F1A\u8BDD\u540D\u79F0\uFF08\u81EA\u52A8\u62FC\u63A5\u77ED\u7801\uFF0C\u8BBE\u7F6E\u540E\u56FA\u5B9A\u4E0D\u518D\u81EA\u52A8\u6539\u540D\uFF09\uFF1A",
|
|
284
|
+
"options.closeGuard": "\u9000\u51FA\u786E\u8BA4\uFF08\u5173\u95ED\u5B88\u536B\uFF09",
|
|
285
|
+
"options.closeGuardDesc": "\u5F00\uFF08\u9ED8\u8BA4\uFF09\uFF1A\u9875\u9762/\u7A97\u53E3\u5173\u95ED\u524D\u786E\u8BA4\u9000\u51FA\u2014\u2014\u6709\u8FD0\u884C\u4E2D\u4EFB\u52A1\u6216\u672A\u5B8C\u6210\u5BF9\u8BDD\u65F6\u5F3A\u62E6\u5E76\u5217\u51FA\u660E\u7EC6\uFF08\u9632\u8BEF\u5173\u4E22\u8FDB\u5EA6\uFF09\uFF0C\u65E0\u4EFB\u52A1\u65F6\u8F7B\u786E\u8BA4\u4E00\u6B21\u3002\u6B64\u62E6\u622A\u4F5C\u7528\u4E8E\u6D4F\u89C8\u5668/\u9875\u9762\u5173\u95ED\u8DEF\u5F84\uFF08\u7F51\u9875\u7248\u6807\u7B7E\u9875\uFF1B\u684C\u9762\u7248\u5E94\u7528\u9000\u51FA\u4E3A\u5BBF\u4E3B\u539F\u751F\u8DEF\u5F84\uFF0C\u89C1\u8FD0\u884C\u4E2D\u6A2A\u5E45\u63D0\u793A\uFF09\u3002"
|
|
185
286
|
};
|
|
186
287
|
var en = {
|
|
187
288
|
"action.edit": "Edit",
|
|
@@ -261,6 +362,9 @@ var en = {
|
|
|
261
362
|
"timeline.gitInitConfirm": "Initialize git in this workspace?",
|
|
262
363
|
"error.generic": "Operation failed; please try again",
|
|
263
364
|
"error.busy": "Stop the current reply before recalling or editing",
|
|
365
|
+
// comm-199/200:localized by code (see zh). Never show the raw host copy verbatim.
|
|
366
|
+
"error.targetShadowed": "This message is inside a folded (read-only) history block: expand that block to edit it, or append a new message instead",
|
|
367
|
+
"error.messagePending": "Message is still being generated; edit it once it finishes",
|
|
264
368
|
"view.fork": "Fork",
|
|
265
369
|
"fork.title": "Fork map",
|
|
266
370
|
"fork.empty": "No forks yet. Recall / edit / regenerate / restore fork the path here.",
|
|
@@ -279,8 +383,16 @@ var en = {
|
|
|
279
383
|
"fork.badge": "Session badge",
|
|
280
384
|
"fork.badgeHint": "A stable shortcode derived from the session id (titles change, badges never do).",
|
|
281
385
|
"fork.rename": "Rename",
|
|
282
|
-
"fork.renamePrompt": "Enter a session name (badge is prefixed automatically; the title is then pinned, no auto-rename):"
|
|
386
|
+
"fork.renamePrompt": "Enter a session name (badge is prefixed automatically; the title is then pinned, no auto-rename):",
|
|
387
|
+
"options.closeGuard": "Exit confirmation (close guard)",
|
|
388
|
+
"options.closeGuardDesc": "On (default): confirm before the page/window closes \u2014 when sessions have running or unfinished work a strong guard lists them (prevents accidental progress loss); otherwise a light one-time confirm. Applies to browser/page close paths (web tabs; Desktop app exit is a host-native path \u2014 see the running banner)."
|
|
283
389
|
};
|
|
390
|
+
function opFailureText(code, message, t) {
|
|
391
|
+
if (code === "agent-busy") return t("error.busy");
|
|
392
|
+
if (code === "target-shadowed") return t("error.targetShadowed");
|
|
393
|
+
if (code === "message-pending") return t("error.messagePending");
|
|
394
|
+
return message ?? t("error.generic");
|
|
395
|
+
}
|
|
284
396
|
var SURFACE_TYPES = /* @__PURE__ */ new Set(["user/message", "assistant/message", "tool/result"]);
|
|
285
397
|
function isReplacementSurfaceEvent(event) {
|
|
286
398
|
return SURFACE_TYPES.has(event.type) && event.surfaceOp !== void 0 && event.surfaceOp !== "append";
|
|
@@ -305,7 +417,7 @@ function retraceConfigHeaders() {
|
|
|
305
417
|
return { "x-retrace-config": JSON.stringify({ versioning, git, retentionLimit }) };
|
|
306
418
|
}
|
|
307
419
|
var CONFIG_VERSION = 3;
|
|
308
|
-
var CONFIG_DEFAULTS = { version: CONFIG_VERSION, showOriginalInput: true, editFromScratch: false, hideShadowed: true, versioning: true, git: true, retentionLimit: 50, prewrite: true };
|
|
420
|
+
var CONFIG_DEFAULTS = { version: CONFIG_VERSION, showOriginalInput: true, editFromScratch: false, hideShadowed: true, versioning: true, git: true, retentionLimit: 50, prewrite: true, closeGuard: true };
|
|
309
421
|
var configListeners = /* @__PURE__ */ new Set();
|
|
310
422
|
var configCache = readConfig();
|
|
311
423
|
var editReferences = /* @__PURE__ */ new Map();
|
|
@@ -652,7 +764,7 @@ function AssistantActions({ messageId, sessionId, useSession, t }) {
|
|
|
652
764
|
if (!result || result.ok !== true) {
|
|
653
765
|
const message = result?.error?.message || "Operation failed; please try again";
|
|
654
766
|
const code = result?.error?.code;
|
|
655
|
-
setFailure(code
|
|
767
|
+
setFailure(opFailureText(code, message, t));
|
|
656
768
|
}
|
|
657
769
|
},
|
|
658
770
|
(error) => {
|
|
@@ -724,7 +836,7 @@ function UserActionsRow({ node, sessionId, useSession, inputActions, t }) {
|
|
|
724
836
|
setBusy(false);
|
|
725
837
|
if (!result || result.ok !== true) {
|
|
726
838
|
const code = result?.error?.code;
|
|
727
|
-
setFailure(code
|
|
839
|
+
setFailure(opFailureText(code, result?.error?.message ?? null, t));
|
|
728
840
|
return;
|
|
729
841
|
}
|
|
730
842
|
if (result.value?.markerT1Broken === true) {
|
|
@@ -859,6 +971,7 @@ function OptionsRow({ t }) {
|
|
|
859
971
|
optionRow("hideShadowed", "options.hideShadowed", "options.hideShadowedDesc"),
|
|
860
972
|
optionRow("versioning", "options.versioning", "options.versioningDesc"),
|
|
861
973
|
optionRow("git", "options.git", "options.gitDesc"),
|
|
974
|
+
optionRow("closeGuard", "options.closeGuard", "options.closeGuardDesc"),
|
|
862
975
|
(0, import_react.createElement)("div", { key: "retention", className: "dsh-rt-option dsh-rt-option-number" }, [
|
|
863
976
|
(0, import_react.createElement)("span", { className: "dsh-rt-option-text" }, [
|
|
864
977
|
(0, import_react.createElement)("span", { className: "dsh-rt-option-label" }, t("options.retention")),
|
|
@@ -989,6 +1102,24 @@ var CSS = `
|
|
|
989
1102
|
.dsh-rt-fork-badge-tag{font-size:11px;font-weight:500;line-height:16px;color:var(--dsw-alias-label-secondary);flex:none}
|
|
990
1103
|
.dsh-rt-fork-badge-code{font-family:var(--dsw-font-mono);font-size:12px;font-weight:600;line-height:16px;color:var(--dsw-alias-state-info-primary);flex:none}
|
|
991
1104
|
.dsh-rt-fork-badge-id{font-family:var(--dsw-font-mono);font-size:11px;line-height:16px;color:var(--dsw-alias-label-caption);min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
1105
|
+
/* \u5173\u95ED\u5B88\u536B V2(comm-176):\u8FD0\u884C\u4E2D\u6A2A\u5E45 / A \u660E\u7EC6\u6A21\u6001 / \u653E\u884C\u63D0\u793A(\u72EC\u7ACB\u4E8E\u4F1A\u8BDD\u89C6\u56FE) */
|
|
1106
|
+
.dsh-rt-guard-banner{position:fixed;top:10px;right:10px;z-index:2147483000;max-width:min(430px,calc(100vw - 20px));background:var(--dsw-alias-bg-elevated,#262626);border:1px solid #b8860b;border-radius:10px;padding:8px 10px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-primary,#eee);box-shadow:0 4px 20px rgba(0,0,0,.4);display:flex;flex-direction:column;gap:4px;text-align:left}
|
|
1107
|
+
.dsh-rt-guard-banner-head{display:flex;align-items:center;gap:6px;font-weight:600;color:#f0c674}
|
|
1108
|
+
.dsh-rt-guard-banner-note{color:var(--dsw-alias-label-caption,#aaa);font-size:11px;line-height:16px}
|
|
1109
|
+
.dsh-rt-guard-banner-list{margin:2px 0 0;padding:0 0 0 14px;color:var(--dsw-alias-label-primary,#eee);font-family:var(--dsw-font-mono,ui-monospace,monospace);font-size:11px;line-height:17px;white-space:pre-line}
|
|
1110
|
+
.dsh-rt-guard-link{background:none;border:none;padding:0;color:var(--dsw-alias-state-info-primary,#7ab8ff);font-size:11px;cursor:pointer}
|
|
1111
|
+
.dsh-rt-guard-link:hover{text-decoration:underline}
|
|
1112
|
+
.dsh-rt-guard-btn{border:1px solid var(--dsw-alias-border-l2,#555);background:transparent;color:var(--dsw-alias-label-primary,#eee);border-radius:6px;padding:2px 10px;font-size:12px;line-height:20px;cursor:pointer}
|
|
1113
|
+
.dsh-rt-guard-btn:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(255,255,255,.08))}
|
|
1114
|
+
.dsh-rt-guard-btn-primary{border-color:#a33;background:#8b1f1f;color:#fff;font-weight:600}
|
|
1115
|
+
.dsh-rt-guard-btn-primary:hover{background:#a33}
|
|
1116
|
+
.dsh-rt-guard-overlay{position:fixed;inset:0;z-index:2147483001;background:rgba(0,0,0,.45);display:flex;align-items:center;justify-content:center;padding:16px}
|
|
1117
|
+
.dsh-rt-guard-modal{background:var(--dsw-alias-bg-elevated,#202020);border:1px solid #b8860b;border-radius:12px;max-width:min(520px,calc(100vw - 32px));padding:14px 16px;font-size:13px;line-height:20px;color:var(--dsw-alias-label-primary,#eee);box-shadow:0 10px 40px rgba(0,0,0,.5);display:flex;flex-direction:column;gap:10px}
|
|
1118
|
+
.dsh-rt-guard-modal-title{font-weight:700;color:#f0c674;font-size:14px}
|
|
1119
|
+
.dsh-rt-guard-modal-lines{margin:0;padding:0 0 0 16px;font-family:var(--dsw-font-mono,ui-monospace,monospace);font-size:12px;line-height:19px;white-space:pre-line}
|
|
1120
|
+
.dsh-rt-guard-modal-hint{color:var(--dsw-alias-label-caption,#aaa);font-size:11px;line-height:16px}
|
|
1121
|
+
.dsh-rt-guard-modal-actions{display:flex;justify-content:flex-end;gap:8px}
|
|
1122
|
+
.dsh-rt-guard-toast{position:fixed;left:50%;bottom:28px;transform:translateX(-50%);z-index:2147483002;background:var(--dsw-alias-bg-elevated,#262626);border:1px solid var(--dsw-alias-border-l2,#555);border-radius:8px;padding:6px 12px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-primary,#eee);box-shadow:0 4px 16px rgba(0,0,0,.35);max-width:min(560px,calc(100vw - 24px))}
|
|
992
1123
|
`;
|
|
993
1124
|
var JUMP_PAGE_BUDGET = 24;
|
|
994
1125
|
function switchToViewTab(viewId) {
|
|
@@ -1535,11 +1666,255 @@ function ForkHistRow({ boundary, markerText, t, onJump }) {
|
|
|
1535
1666
|
(0, import_react.createElement)("button", { key: "jump", type: "button", className: "dsh-rt-chip", onClick: onJump }, t("timeline.jump"))
|
|
1536
1667
|
]);
|
|
1537
1668
|
}
|
|
1669
|
+
var GUARD_MODAL_ID = "dsh-rt-guard-modal";
|
|
1670
|
+
var GUARD_BANNER_ID = "dsh-rt-guard-banner";
|
|
1671
|
+
var GUARD_TOAST_ID = "dsh-rt-guard-toast";
|
|
1672
|
+
function installCloseGuard(t) {
|
|
1673
|
+
if (typeof window === "undefined" || typeof document === "undefined") return () => {
|
|
1674
|
+
};
|
|
1675
|
+
const locale = () => t("action.cancel") === "\u53D6\u6D88" ? "zh" : "en";
|
|
1676
|
+
const store = createGuardStore();
|
|
1677
|
+
const ui = { banner: null, modal: null, toast: null, toastTimer: 0, dismissed: null, expanded: false, lastSig: "" };
|
|
1678
|
+
let supported = true;
|
|
1679
|
+
let failures = 0;
|
|
1680
|
+
const labelOf = (sessionId) => {
|
|
1681
|
+
try {
|
|
1682
|
+
const badge = sessionBadge(sessionId);
|
|
1683
|
+
return badge || String(sessionId);
|
|
1684
|
+
} catch {
|
|
1685
|
+
return String(sessionId);
|
|
1686
|
+
}
|
|
1687
|
+
};
|
|
1688
|
+
const runningSig = (snapshot) => {
|
|
1689
|
+
const running = snapshot?.running;
|
|
1690
|
+
if (!Array.isArray(running)) return "";
|
|
1691
|
+
return running.map((r) => `${r.sessionId}:${(r.reasons ?? []).join(",")}`).join("|");
|
|
1692
|
+
};
|
|
1693
|
+
function queryRunningState() {
|
|
1694
|
+
if (typeof wire === "function") return wire("runningState", {});
|
|
1695
|
+
return fetch(`${ROUTE_BASE}/runningState`, {
|
|
1696
|
+
method: "GET",
|
|
1697
|
+
headers: retraceConfigHeaders(),
|
|
1698
|
+
cache: "no-store"
|
|
1699
|
+
}).then((res) => {
|
|
1700
|
+
if (res.status < 200 || res.status >= 300) throw new Error(`HTTP ${res.status}`);
|
|
1701
|
+
return res.json();
|
|
1702
|
+
});
|
|
1703
|
+
}
|
|
1704
|
+
function refresh() {
|
|
1705
|
+
if (!supported) return;
|
|
1706
|
+
queryRunningState().then((payload) => {
|
|
1707
|
+
failures = 0;
|
|
1708
|
+
const value = payload && payload.ok ? payload.value : null;
|
|
1709
|
+
if (!value || !Array.isArray(value.running)) return;
|
|
1710
|
+
const prev = store.get();
|
|
1711
|
+
store.set({ running: value.running });
|
|
1712
|
+
if (runningSig(store.get()) !== runningSig(prev)) ui.dismissed = null;
|
|
1713
|
+
renderBanner();
|
|
1714
|
+
}).catch(() => {
|
|
1715
|
+
failures += 1;
|
|
1716
|
+
if (failures >= 3) {
|
|
1717
|
+
supported = false;
|
|
1718
|
+
removeBanner();
|
|
1719
|
+
console.warn("[dsh-retrace] close-guard: running-state \u901A\u9053\u4E0D\u53EF\u7528(3 \u6B21\u5931\u8D25),\u672C\u9875\u5173\u95ED\u5B88\u536B\u505C\u7528");
|
|
1720
|
+
}
|
|
1721
|
+
});
|
|
1722
|
+
}
|
|
1723
|
+
function removeBanner() {
|
|
1724
|
+
if (ui.banner) {
|
|
1725
|
+
ui.banner.remove();
|
|
1726
|
+
ui.banner = null;
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
function bannerNote(lang) {
|
|
1730
|
+
return lang === "zh" ? "\u5173\u95ED\u9875\u9762/\u9000\u51FA\u524D\u8BF7\u5148\u786E\u8BA4\u2014\u2014\u4EFB\u52A1\u4E2D\u65AD\u53EF\u80FD\u4E22\u5931\u8FDB\u5EA6(\u684C\u9762\u7AEF\u5E94\u7528\u9000\u51FA\u5728\u5BBF\u4E3B\u4FA7,\u8BF7\u4EE5\u6B64\u6A2A\u5E45\u4E3A\u51C6)" : "Confirm before closing/exiting \u2014 interrupting work may lose progress (Desktop app exit is host-side; rely on this banner)";
|
|
1731
|
+
}
|
|
1732
|
+
function renderBanner() {
|
|
1733
|
+
const cfg = getConfig();
|
|
1734
|
+
if (!cfg.closeGuard || !supported) return removeBanner();
|
|
1735
|
+
const snap = store.get();
|
|
1736
|
+
if (classifySnapshot(snap) !== "running") return removeBanner();
|
|
1737
|
+
const lang = locale();
|
|
1738
|
+
const lines = runningLines(snap.running, { locale: lang, labelOf });
|
|
1739
|
+
const sig = lines.join("\n");
|
|
1740
|
+
if (ui.dismissed === sig) return;
|
|
1741
|
+
if (ui.banner && ui.banner.isConnected && ui.lastSig === sig) return;
|
|
1742
|
+
if (!ui.banner) {
|
|
1743
|
+
ui.banner = document.createElement("div");
|
|
1744
|
+
ui.banner.id = GUARD_BANNER_ID;
|
|
1745
|
+
ui.banner.className = "dsh-rt-guard-banner";
|
|
1746
|
+
const head2 = document.createElement("div");
|
|
1747
|
+
head2.className = "dsh-rt-guard-banner-head";
|
|
1748
|
+
ui.banner.appendChild(head2);
|
|
1749
|
+
const note = document.createElement("div");
|
|
1750
|
+
note.className = "dsh-rt-guard-banner-note";
|
|
1751
|
+
ui.banner.appendChild(note);
|
|
1752
|
+
const list = document.createElement("div");
|
|
1753
|
+
list.className = "dsh-rt-guard-banner-list";
|
|
1754
|
+
list.style.display = "none";
|
|
1755
|
+
ui.banner.appendChild(list);
|
|
1756
|
+
document.body.appendChild(ui.banner);
|
|
1757
|
+
}
|
|
1758
|
+
const n = snap.running.length;
|
|
1759
|
+
const head = ui.banner.firstChild;
|
|
1760
|
+
head.textContent = "";
|
|
1761
|
+
head.appendChild(document.createTextNode(lang === "zh" ? `\u26A0\uFE0F ${n} \u4E2A\u4F1A\u8BDD\u8FD0\u884C\u4E2D` : `\u26A0\uFE0F ${n} session${n === 1 ? "" : "s"} running`));
|
|
1762
|
+
const toggle = document.createElement("button");
|
|
1763
|
+
toggle.type = "button";
|
|
1764
|
+
toggle.className = "dsh-rt-guard-link";
|
|
1765
|
+
toggle.textContent = ui.expanded ? lang === "zh" ? "\u6536\u8D77" : "Hide" : lang === "zh" ? "\u660E\u7EC6" : "Details";
|
|
1766
|
+
toggle.onclick = () => {
|
|
1767
|
+
ui.expanded = !ui.expanded;
|
|
1768
|
+
const list = ui.banner.querySelector(".dsh-rt-guard-banner-list");
|
|
1769
|
+
if (list) list.style.display = ui.expanded ? "block" : "none";
|
|
1770
|
+
toggle.textContent = ui.expanded ? lang === "zh" ? "\u6536\u8D77" : "Hide" : lang === "zh" ? "\u660E\u7EC6" : "Details";
|
|
1771
|
+
};
|
|
1772
|
+
const dismiss = document.createElement("button");
|
|
1773
|
+
dismiss.type = "button";
|
|
1774
|
+
dismiss.className = "dsh-rt-guard-link";
|
|
1775
|
+
dismiss.textContent = "\xD7";
|
|
1776
|
+
dismiss.title = lang === "zh" ? "\u5FFD\u7565(\u4EFB\u52A1\u7ED3\u675F\u6216\u53D8\u5316\u540E\u518D\u63D0\u793A)" : "Dismiss (re-shows when the running set changes)";
|
|
1777
|
+
dismiss.onclick = () => {
|
|
1778
|
+
ui.dismissed = sig;
|
|
1779
|
+
removeBanner();
|
|
1780
|
+
};
|
|
1781
|
+
head.appendChild(toggle);
|
|
1782
|
+
head.appendChild(dismiss);
|
|
1783
|
+
ui.banner.querySelector(".dsh-rt-guard-banner-note").textContent = bannerNote(lang);
|
|
1784
|
+
const listEl = ui.banner.querySelector(".dsh-rt-guard-banner-list");
|
|
1785
|
+
listEl.textContent = lines.join("\n");
|
|
1786
|
+
listEl.style.display = ui.expanded ? "block" : "none";
|
|
1787
|
+
ui.lastSig = sig;
|
|
1788
|
+
}
|
|
1789
|
+
function hideModal() {
|
|
1790
|
+
if (ui.modal) {
|
|
1791
|
+
ui.modal.remove();
|
|
1792
|
+
ui.modal = null;
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
function showModal() {
|
|
1796
|
+
const snap = store.get();
|
|
1797
|
+
if (classifySnapshot(snap) !== "running") return;
|
|
1798
|
+
const lang = locale();
|
|
1799
|
+
const copy = buildRunningCopy(snap, { locale: lang, labelOf });
|
|
1800
|
+
hideModal();
|
|
1801
|
+
const overlay = document.createElement("div");
|
|
1802
|
+
overlay.className = "dsh-rt-guard-overlay";
|
|
1803
|
+
overlay.id = GUARD_MODAL_ID;
|
|
1804
|
+
overlay.onclick = (event) => {
|
|
1805
|
+
if (event.target === overlay) hideModal();
|
|
1806
|
+
};
|
|
1807
|
+
const modal = document.createElement("div");
|
|
1808
|
+
modal.className = "dsh-rt-guard-modal";
|
|
1809
|
+
modal.onclick = (event) => event.stopPropagation();
|
|
1810
|
+
const title = document.createElement("div");
|
|
1811
|
+
title.className = "dsh-rt-guard-modal-title";
|
|
1812
|
+
title.textContent = copy.head;
|
|
1813
|
+
modal.appendChild(title);
|
|
1814
|
+
const lines = document.createElement("div");
|
|
1815
|
+
lines.className = "dsh-rt-guard-modal-lines";
|
|
1816
|
+
lines.textContent = copy.lines.join("\n");
|
|
1817
|
+
modal.appendChild(lines);
|
|
1818
|
+
if (copy.hint) {
|
|
1819
|
+
const hint = document.createElement("div");
|
|
1820
|
+
hint.className = "dsh-rt-guard-modal-hint";
|
|
1821
|
+
hint.textContent = copy.hint;
|
|
1822
|
+
modal.appendChild(hint);
|
|
1823
|
+
}
|
|
1824
|
+
const actions = document.createElement("div");
|
|
1825
|
+
actions.className = "dsh-rt-guard-modal-actions";
|
|
1826
|
+
const cancel = document.createElement("button");
|
|
1827
|
+
cancel.type = "button";
|
|
1828
|
+
cancel.className = "dsh-rt-guard-btn";
|
|
1829
|
+
cancel.textContent = lang === "zh" ? "\u53D6\u6D88" : "Cancel";
|
|
1830
|
+
cancel.onclick = () => hideModal();
|
|
1831
|
+
const proceed = document.createElement("button");
|
|
1832
|
+
proceed.type = "button";
|
|
1833
|
+
proceed.className = "dsh-rt-guard-btn dsh-rt-guard-btn-primary";
|
|
1834
|
+
proceed.textContent = lang === "zh" ? "\u4ECD\u5173\u95ED" : "Close anyway";
|
|
1835
|
+
proceed.onclick = () => {
|
|
1836
|
+
store.arm();
|
|
1837
|
+
hideModal();
|
|
1838
|
+
try {
|
|
1839
|
+
window.close();
|
|
1840
|
+
} catch {
|
|
1841
|
+
}
|
|
1842
|
+
showToast(lang === "zh" ? "\u5DF2\u653E\u884C\u2014\u2014\u82E5\u672A\u81EA\u52A8\u9000\u51FA,\u8BF7\u518D\u6B21\u70B9\u51FB\u5173\u95ED\u7A97\u53E3/\u6807\u7B7E\u9875(30 \u79D2\u5185\u6709\u6548,\u8FC7\u671F\u9700\u91CD\u65B0\u786E\u8BA4)" : "Armed \u2014 if the window did not close, click close again (valid 30s, then re-confirm)");
|
|
1843
|
+
};
|
|
1844
|
+
actions.appendChild(cancel);
|
|
1845
|
+
actions.appendChild(proceed);
|
|
1846
|
+
modal.appendChild(actions);
|
|
1847
|
+
overlay.appendChild(modal);
|
|
1848
|
+
document.body.appendChild(overlay);
|
|
1849
|
+
ui.modal = overlay;
|
|
1850
|
+
}
|
|
1851
|
+
function showToast(text) {
|
|
1852
|
+
if (!ui.toast) {
|
|
1853
|
+
ui.toast = document.createElement("div");
|
|
1854
|
+
ui.toast.id = GUARD_TOAST_ID;
|
|
1855
|
+
ui.toast.className = "dsh-rt-guard-toast";
|
|
1856
|
+
document.body.appendChild(ui.toast);
|
|
1857
|
+
}
|
|
1858
|
+
ui.toast.textContent = text;
|
|
1859
|
+
clearTimeout(ui.toastTimer);
|
|
1860
|
+
ui.toastTimer = setTimeout(() => {
|
|
1861
|
+
if (ui.toast) {
|
|
1862
|
+
ui.toast.remove();
|
|
1863
|
+
ui.toast = null;
|
|
1864
|
+
}
|
|
1865
|
+
}, 8e3);
|
|
1866
|
+
}
|
|
1867
|
+
function onBeforeUnload(event) {
|
|
1868
|
+
if (!getConfig().closeGuard || !supported) return;
|
|
1869
|
+
if (store.isArmed()) return;
|
|
1870
|
+
const kind = classifySnapshot(store.get());
|
|
1871
|
+
if (kind === "unknown") return;
|
|
1872
|
+
try {
|
|
1873
|
+
event.preventDefault?.();
|
|
1874
|
+
} catch {
|
|
1875
|
+
}
|
|
1876
|
+
try {
|
|
1877
|
+
event.returnValue = "";
|
|
1878
|
+
} catch {
|
|
1879
|
+
}
|
|
1880
|
+
if (kind === "running") {
|
|
1881
|
+
setTimeout(() => {
|
|
1882
|
+
if (classifySnapshot(store.get()) === "running" && !store.isArmed()) showModal();
|
|
1883
|
+
}, 0);
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
function onVisibility() {
|
|
1887
|
+
if (document.visibilityState === "visible") refresh();
|
|
1888
|
+
}
|
|
1889
|
+
window.addEventListener("beforeunload", onBeforeUnload);
|
|
1890
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
1891
|
+
const timer = window.setInterval(refresh, GUARD_POLL_MS);
|
|
1892
|
+
refresh();
|
|
1893
|
+
const unsubscribe = subscribeConfig(() => {
|
|
1894
|
+
renderBanner();
|
|
1895
|
+
if (getConfig().closeGuard) refresh();
|
|
1896
|
+
});
|
|
1897
|
+
return () => {
|
|
1898
|
+
window.removeEventListener("beforeunload", onBeforeUnload);
|
|
1899
|
+
document.removeEventListener("visibilitychange", onVisibility);
|
|
1900
|
+
window.clearInterval(timer);
|
|
1901
|
+
unsubscribe();
|
|
1902
|
+
removeBanner();
|
|
1903
|
+
hideModal();
|
|
1904
|
+
if (ui.toast) {
|
|
1905
|
+
ui.toast.remove();
|
|
1906
|
+
ui.toast = null;
|
|
1907
|
+
}
|
|
1908
|
+
clearTimeout(ui.toastTimer);
|
|
1909
|
+
};
|
|
1910
|
+
}
|
|
1538
1911
|
function apply(ctx) {
|
|
1539
1912
|
const disposeStyle = ensureStyle();
|
|
1540
1913
|
ctx.effect(() => () => disposeStyle(), "dsh-retrace: styles");
|
|
1541
1914
|
ctx.effect(() => ctx.locale.register(NS, { zh, en }), "dsh-retrace: dictionaries");
|
|
1542
1915
|
const t = ctx.locale.bind(NS);
|
|
1916
|
+
const disposeCloseGuard = installCloseGuard(t);
|
|
1917
|
+
ctx.effect(() => () => disposeCloseGuard(), "dsh-retrace: close guard");
|
|
1543
1918
|
const conversationEvents = ctx.get("conversationEvents");
|
|
1544
1919
|
if (conversationEvents) {
|
|
1545
1920
|
const disposeDefinitions = [
|