dsh-retrace 0.4.30 → 0.4.31

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.
@@ -37,42 +37,6 @@ __export(client_exports, {
37
37
  module.exports = __toCommonJS(client_exports);
38
38
  var import_react = require("react");
39
39
 
40
- // lib/badge.js
41
- function uuidOf(id) {
42
- const text = String(id ?? "");
43
- const m = text.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i);
44
- if (m) return m[0].replace(/-/g, "").toLowerCase();
45
- const cleaned = text.replace(/[^0-9a-f]/gi, "").toLowerCase();
46
- return cleaned.length >= 8 ? cleaned.slice(0, 32) : "";
47
- }
48
- function fnv1a64(text) {
49
- let hash = 0xcbf29ce484222325n;
50
- const prime = 0x100000001b3n;
51
- const mask = 0xffffffffffffffffn;
52
- for (let i = 0; i < text.length; i++) {
53
- hash ^= BigInt(text.charCodeAt(i));
54
- hash = hash * prime & mask;
55
- }
56
- return hash;
57
- }
58
- var BASE36 = "0123456789abcdefghijklmnopqrstuvwxyz";
59
- function toBase36(value) {
60
- if (value < 0n) value = -value;
61
- if (value === 0n) return "0";
62
- let out = "";
63
- let v = value;
64
- while (v > 0n) {
65
- out = BASE36[Number(v % 36n)] + out;
66
- v /= 36n;
67
- }
68
- return out;
69
- }
70
- function sessionBadge(sessionId) {
71
- const uuid = uuidOf(sessionId);
72
- if (uuid.length === 0) return "";
73
- return toBase36(fnv1a64(uuid)).padStart(10, "0").slice(0, 10);
74
- }
75
-
76
40
  // lib/marker-carrier.js
77
41
  var MARKER_ID_PREFIXES = ["retrace", "message-editor"];
78
42
  var AUDIT_EVENT_TYPE = "compaction/prune";
@@ -192,6 +156,42 @@ function carrierShadowedSeqs(event, eventAt) {
192
156
  return Array.isArray(inner) ? inner.filter((s) => Number.isSafeInteger(s)) : [];
193
157
  }
194
158
 
159
+ // lib/badge.js
160
+ function uuidOf(id) {
161
+ const text = String(id ?? "");
162
+ const m = text.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i);
163
+ if (m) return m[0].replace(/-/g, "").toLowerCase();
164
+ const cleaned = text.replace(/[^0-9a-f]/gi, "").toLowerCase();
165
+ return cleaned.length >= 8 ? cleaned.slice(0, 32) : "";
166
+ }
167
+ function fnv1a64(text) {
168
+ let hash = 0xcbf29ce484222325n;
169
+ const prime = 0x100000001b3n;
170
+ const mask = 0xffffffffffffffffn;
171
+ for (let i = 0; i < text.length; i++) {
172
+ hash ^= BigInt(text.charCodeAt(i));
173
+ hash = hash * prime & mask;
174
+ }
175
+ return hash;
176
+ }
177
+ var BASE36 = "0123456789abcdefghijklmnopqrstuvwxyz";
178
+ function toBase36(value) {
179
+ if (value < 0n) value = -value;
180
+ if (value === 0n) return "0";
181
+ let out = "";
182
+ let v = value;
183
+ while (v > 0n) {
184
+ out = BASE36[Number(v % 36n)] + out;
185
+ v /= 36n;
186
+ }
187
+ return out;
188
+ }
189
+ function sessionBadge(sessionId) {
190
+ const uuid = uuidOf(sessionId);
191
+ if (uuid.length === 0) return "";
192
+ return toBase36(fnv1a64(uuid)).padStart(10, "0").slice(0, 10);
193
+ }
194
+
195
195
  // lib/close-guard-client.js
196
196
  var GUARD_POLL_MS = 5e3;
197
197
  var GUARD_TTL_MS = 3e4;
@@ -287,7 +287,7 @@ function shouldArmNativeGate(snapshot, page = pageEnvironment()) {
287
287
  function createGuardStore({ now = Date.now, armTtlMs = GUARD_ARM_TTL_MS } = {}) {
288
288
  let snapshot = null;
289
289
  let armedAt = 0;
290
- return {
290
+ const store = {
291
291
  /** 设置最新 host 快照(runningSessions 形状 { running: [...] })。 */
292
292
  set(next) {
293
293
  snapshot = next && typeof next === "object" ? { ...next, at: next.at ?? now() } : null;
@@ -308,6 +308,343 @@ function createGuardStore({ now = Date.now, armTtlMs = GUARD_ARM_TTL_MS } = {})
308
308
  armedAt = 0;
309
309
  }
310
310
  };
311
+ try {
312
+ const win = globalThis?.window;
313
+ const doc = globalThis?.document;
314
+ if (win && doc) {
315
+ queueMicrotask(() => {
316
+ try {
317
+ installDesktopGate({ win, doc, store });
318
+ } catch {
319
+ }
320
+ });
321
+ }
322
+ } catch {
323
+ }
324
+ return store;
325
+ }
326
+ var GATE_WATCHDOG_MS = 1500;
327
+ var GATE_MODAL_ID = "dsh-rt-guard-modal";
328
+ var GATE_HINT_ID = "dsh-rt-guard-toast";
329
+ var GATE_CONFIG_KEY = "dsh-retrace:config";
330
+ var GATE_ROUTE = "/api/plugins/retrace/runningState";
331
+ var GATE_QUERY = "closeGuardEvent";
332
+ function runningCountOf(snapshot) {
333
+ return Array.isArray(snapshot?.running) ? snapshot.running.length : 0;
334
+ }
335
+ function gateEnabled(storage = globalThis?.localStorage) {
336
+ try {
337
+ const raw = storage?.getItem?.(GATE_CONFIG_KEY);
338
+ if (raw === null || raw === void 0 || raw === "") return true;
339
+ const parsed = JSON.parse(raw);
340
+ return parsed?.closeGuard !== false;
341
+ } catch {
342
+ return true;
343
+ }
344
+ }
345
+ function planBeforeUnload(snapshot, { armed = false, enabled = true, desktop = false, visible = true } = {}) {
346
+ if (!enabled) return { action: "allow", reason: "disabled" };
347
+ if (armed) return { action: "allow", reason: "armed" };
348
+ const kind = classifySnapshot(snapshot);
349
+ if (kind === "unknown") return { action: "allow", reason: "state-unknown" };
350
+ if (kind === "idle") return { action: "allow", reason: "no-running" };
351
+ if (visible === false) return { action: "allow", reason: "hidden" };
352
+ if (!desktop) return { action: "allow", reason: "browser-native-gate" };
353
+ return { action: "gate", reason: "running-desktop" };
354
+ }
355
+ function reportGateEvent(event, extra = {}, fetchImpl = globalThis?.fetch) {
356
+ try {
357
+ if (typeof fetchImpl !== "function") return;
358
+ const params = new URLSearchParams({ [GATE_QUERY]: String(event) });
359
+ for (const [key, value] of Object.entries(extra)) {
360
+ if (value !== void 0 && value !== null && value !== "") params.set(key, String(value));
361
+ }
362
+ const done = fetchImpl(`${GATE_ROUTE}?${params.toString()}`, { method: "GET", cache: "no-store", keepalive: true });
363
+ if (done && typeof done.catch === "function") done.catch(() => {
364
+ });
365
+ } catch {
366
+ }
367
+ }
368
+ function installDesktopGate({
369
+ win,
370
+ doc,
371
+ store,
372
+ page = pageEnvironment,
373
+ enabled = gateEnabled,
374
+ report = reportGateEvent,
375
+ timers = {}
376
+ } = {}) {
377
+ if (!win || !doc || !store || win.__dshRetraceCloseGate) return () => {
378
+ };
379
+ win.__dshRetraceCloseGate = true;
380
+ const setT = timers.setTimeout ?? win.setTimeout?.bind(win) ?? setTimeout;
381
+ const clearT = timers.clearTimeout ?? win.clearTimeout?.bind(win) ?? clearTimeout;
382
+ let modal = null;
383
+ let attempt = 0;
384
+ let secondary = null;
385
+ const labelOf = (sessionId) => {
386
+ try {
387
+ return sessionBadge(sessionId) || String(sessionId);
388
+ } catch {
389
+ return String(sessionId);
390
+ }
391
+ };
392
+ const langOf = () => String(win.navigator?.language ?? "").toLowerCase().startsWith("en") ? "en" : "zh";
393
+ const el = (tag, css, text) => {
394
+ const node = doc.createElement(tag);
395
+ if (css) node.style.cssText = css;
396
+ if (text !== void 0) node.textContent = text;
397
+ return node;
398
+ };
399
+ const removeModal = () => {
400
+ try {
401
+ modal?.remove?.();
402
+ } catch {
403
+ }
404
+ modal = null;
405
+ };
406
+ const modalVisible = () => {
407
+ try {
408
+ if (!modal || modal.isConnected === false) return false;
409
+ const height = modal.offsetHeight;
410
+ if (typeof height === "number") return height > 0;
411
+ const rect = modal.getBoundingClientRect?.();
412
+ return rect === void 0 ? false : rect.width > 0;
413
+ } catch {
414
+ return false;
415
+ }
416
+ };
417
+ const eventExtra = (extra = {}) => ({ surface: "desktop-renderer", ...extra });
418
+ function createTimer() {
419
+ try {
420
+ const g = win;
421
+ const canWorker = typeof g.Worker === "function" && typeof g.Blob === "function" && typeof g.URL?.createObjectURL === "function";
422
+ if (canWorker) {
423
+ const url = g.URL.createObjectURL(new g.Blob(["onmessage=function(e){setTimeout(function(){postMessage(1)},e.data)}"], { type: "text/javascript" }));
424
+ const worker = new g.Worker(url);
425
+ let done = false;
426
+ const stop = () => {
427
+ if (done) return;
428
+ done = true;
429
+ try {
430
+ worker.terminate();
431
+ } catch {
432
+ }
433
+ try {
434
+ g.URL.revokeObjectURL(url);
435
+ } catch {
436
+ }
437
+ };
438
+ return {
439
+ kind: "worker",
440
+ arm(ms, cb) {
441
+ worker.onmessage = () => {
442
+ if (done) return;
443
+ stop();
444
+ cb();
445
+ };
446
+ worker.postMessage(ms);
447
+ },
448
+ dispose: stop
449
+ };
450
+ }
451
+ } catch {
452
+ }
453
+ let handle;
454
+ return {
455
+ kind: "timeout",
456
+ arm(ms, cb) {
457
+ handle = setT(cb, ms);
458
+ },
459
+ dispose() {
460
+ try {
461
+ clearT(handle);
462
+ } catch {
463
+ }
464
+ }
465
+ };
466
+ }
467
+ function clearSecondary() {
468
+ const current = secondary;
469
+ secondary = null;
470
+ if (!current) return;
471
+ try {
472
+ current.timer?.dispose?.();
473
+ } catch {
474
+ }
475
+ try {
476
+ doc.removeEventListener?.("visibilitychange", current.onHidden);
477
+ } catch {
478
+ }
479
+ try {
480
+ doc.removeEventListener?.("keydown", current.onKeydown);
481
+ } catch {
482
+ }
483
+ }
484
+ function showHint(text) {
485
+ try {
486
+ const tip = el("div", "position:fixed;left:50%;bottom:28px;transform:translateX(-50%);z-index:2147483002;background:#262626;color:#eee;border:1px solid #555;border-radius:8px;padding:6px 12px;font:12px/18px -apple-system,system-ui,sans-serif;max-width:90vw;", text);
487
+ tip.id = GATE_HINT_ID;
488
+ tip.className = "dsh-rt-guard-toast";
489
+ doc.body.appendChild(tip);
490
+ setT(() => {
491
+ try {
492
+ tip.remove?.();
493
+ } catch {
494
+ }
495
+ }, 8e3);
496
+ } catch {
497
+ }
498
+ }
499
+ function release(reason, id) {
500
+ if (id !== void 0 && id !== attempt) return;
501
+ attempt += 1;
502
+ clearSecondary();
503
+ report(reason, eventExtra({ running: runningCountOf(store.get()) }));
504
+ try {
505
+ store.arm();
506
+ } catch {
507
+ }
508
+ removeModal();
509
+ try {
510
+ win.close?.();
511
+ } catch {
512
+ }
513
+ setT(() => showHint(reason.startsWith("fail-soft") ? "\u786E\u8BA4\u6846\u672A\u80FD\u663E\u793A\uFF1A\u5DF2\u6309\u5B98\u65B9\u884C\u4E3A\u653E\u884C\uFF0C\u8BF7\u518D\u70B9\u4E00\u6B21\u5173\u95ED" : "\u5DF2\u653E\u884C\uFF1A\u82E5\u7A97\u53E3\u672A\u5173\u95ED\uFF0C\u8BF7\u518D\u70B9\u4E00\u6B21\u5173\u95ED"), 300);
514
+ }
515
+ function showModal(copy, lang, id) {
516
+ const overlay = el("div", "position:fixed;inset:0;z-index:2147483001;background:rgba(0,0,0,.45);display:flex;align-items:center;justify-content:center;padding:16px;");
517
+ overlay.id = GATE_MODAL_ID;
518
+ overlay.className = "dsh-rt-guard-overlay";
519
+ overlay.setAttribute?.("role", "dialog");
520
+ const box = el("div", "background:#202020;color:#eee;border:1px solid #b8860b;border-radius:12px;max-width:min(520px,92vw);padding:14px 16px;font:13px/20px -apple-system,system-ui,sans-serif;display:flex;flex-direction:column;gap:10px;box-sizing:border-box;");
521
+ box.className = "dsh-rt-guard-modal";
522
+ const title = el("div", "font-weight:700;color:#f0c674;font-size:14px;", copy.head);
523
+ title.className = "dsh-rt-guard-modal-title";
524
+ const body = el("div", "white-space:pre-line;font:12px/19px ui-monospace,SFMono-Regular,monospace;", copy.lines.join("\n"));
525
+ body.className = "dsh-rt-guard-modal-lines";
526
+ box.appendChild(title);
527
+ box.appendChild(body);
528
+ if (copy.hint) {
529
+ const note = el("div", "color:#aaa;font-size:11px;", copy.hint);
530
+ note.className = "dsh-rt-guard-modal-hint";
531
+ box.appendChild(note);
532
+ }
533
+ const waiting = el("div", "color:#aaa;font-size:11px;", lang === "zh" ? "\u7B49\u5F85\u4F60\u7684\u9009\u62E9\uFF08\u4E0D\u81EA\u52A8\u5173\u95ED\uFF1BEsc \u7B49\u540C\u53D6\u6D88\uFF09" : "Waiting for your choice (no auto-close; Esc cancels)");
534
+ waiting.className = "dsh-rt-guard-modal-wait";
535
+ box.appendChild(waiting);
536
+ const actions = el("div", "display:flex;justify-content:flex-end;gap:8px;");
537
+ actions.className = "dsh-rt-guard-modal-actions";
538
+ const cancel = el("button", "border:1px solid #555;background:transparent;color:#eee;border-radius:6px;padding:4px 12px;font-size:12px;line-height:20px;cursor:pointer;", lang === "zh" ? "\u53D6\u6D88" : "Cancel");
539
+ cancel.type = "button";
540
+ cancel.className = "dsh-rt-guard-btn";
541
+ cancel.onclick = () => {
542
+ if (id !== attempt) return;
543
+ attempt += 1;
544
+ clearSecondary();
545
+ report("cancel", eventExtra());
546
+ removeModal();
547
+ };
548
+ const proceed = el("button", "border:1px solid #a33;background:#8b1f1f;color:#fff;font-weight:600;border-radius:6px;padding:4px 12px;font-size:12px;line-height:20px;cursor:pointer;", lang === "zh" ? "\u4ECD\u8981\u5173\u95ED" : "Close anyway");
549
+ proceed.type = "button";
550
+ proceed.className = "dsh-rt-guard-btn dsh-rt-guard-btn-primary";
551
+ proceed.onclick = () => release("release", id);
552
+ actions.appendChild(cancel);
553
+ actions.appendChild(proceed);
554
+ box.appendChild(actions);
555
+ overlay.appendChild(box);
556
+ try {
557
+ doc.body.appendChild(overlay);
558
+ modal = overlay;
559
+ } catch {
560
+ modal = null;
561
+ return false;
562
+ }
563
+ try {
564
+ (cancel.focus ?? proceed.focus)?.call(cancel);
565
+ } catch {
566
+ }
567
+ return modalVisible();
568
+ }
569
+ function armSecondary(id) {
570
+ clearSecondary();
571
+ const onHidden = () => {
572
+ if (doc.visibilityState === "hidden") release("fail-soft-hidden", id);
573
+ };
574
+ const onKeydown = (event) => {
575
+ if (event?.key !== "Escape" || id !== attempt) return;
576
+ attempt += 1;
577
+ clearSecondary();
578
+ report("cancel", eventExtra());
579
+ removeModal();
580
+ };
581
+ const timer = createTimer();
582
+ const state = { id, timer, onHidden, onKeydown };
583
+ secondary = state;
584
+ try {
585
+ doc.addEventListener?.("visibilitychange", onHidden);
586
+ } catch {
587
+ }
588
+ try {
589
+ doc.addEventListener?.("keydown", onKeydown);
590
+ } catch {
591
+ }
592
+ timer.arm(GATE_WATCHDOG_MS, () => {
593
+ if (id !== attempt || secondary !== state) return;
594
+ if (modalVisible()) report("waiting", eventExtra({ running: runningCountOf(store.get()) }));
595
+ else release("fail-soft-ui-gone", id);
596
+ });
597
+ }
598
+ function onBeforeUnload(event) {
599
+ const snapshot = store.get();
600
+ const plan = planBeforeUnload(snapshot, {
601
+ armed: store.isArmed?.() === true,
602
+ enabled: enabled() === true,
603
+ desktop: clientDesktopEvidence(page()).desktop === true,
604
+ visible: doc.visibilityState !== "hidden"
605
+ });
606
+ if (plan.action !== "gate") {
607
+ if (plan.reason === "no-running" || plan.reason === "hidden") report(`allow-${plan.reason}`, eventExtra({ running: runningCountOf(snapshot) }));
608
+ return;
609
+ }
610
+ const id = attempt + 1;
611
+ attempt = id;
612
+ let shown = false;
613
+ try {
614
+ shown = showModal(buildRunningCopy(snapshot, { locale: langOf(), labelOf }), langOf(), id) === true;
615
+ } catch {
616
+ shown = false;
617
+ }
618
+ if (!shown) {
619
+ removeModal();
620
+ release("fail-soft-ui-not-rendered", id);
621
+ return;
622
+ }
623
+ try {
624
+ event?.preventDefault?.();
625
+ } catch {
626
+ }
627
+ try {
628
+ if (event) event.returnValue = "";
629
+ } catch {
630
+ }
631
+ report("intercept", eventExtra({ running: runningCountOf(snapshot) }));
632
+ armSecondary(id);
633
+ }
634
+ win.addEventListener("beforeunload", onBeforeUnload);
635
+ if (clientDesktopEvidence(page()).desktop === true) report("gate-ready", eventExtra({ running: runningCountOf(store.get()) }));
636
+ return () => {
637
+ clearSecondary();
638
+ try {
639
+ win.removeEventListener("beforeunload", onBeforeUnload);
640
+ } catch {
641
+ }
642
+ removeModal();
643
+ try {
644
+ delete win.__dshRetraceCloseGate;
645
+ } catch {
646
+ }
647
+ };
311
648
  }
312
649
 
313
650
  // lib/client.js
@@ -495,7 +832,7 @@ var zh = {
495
832
  "path.round": "\u7B2C {n} \u8F6E",
496
833
  "path.empty": "\uFF08\u5F53\u524D\u6CA1\u6709\u5BF9\u8BDD\u8F6E\u6B21\uFF09",
497
834
  "options.closeGuard": "\u9000\u51FA\u786E\u8BA4\uFF08\u5173\u95ED\u5B88\u536B\uFF09",
498
- "options.closeGuardDesc": "\u5F00\uFF08\u9ED8\u8BA4\uFF09\uFF1A\u9875\u9762\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\u53EA\u5728\u5BBF\u4E3B\u786E\u8BA4\u4F1A\u663E\u793A\u539F\u751F\u786E\u8BA4\u6846\u7684\u9875\u9762\u4E0A\u6B66\u88C5\uFF08\u666E\u901A\u6D4F\u89C8\u5668\u6807\u7B7E\u9875\uFF1B\u5173\u6389\u672C\u9879\u540C\u65F6\u505C\u7528\u8FD0\u884C\u4E2D\u6A2A\u5E45\uFF09\u3002\u684C\u9762\u7AEF\uFF08DSH Desktop\uFF09\u4E00\u5F8B\u4E0D\u6B66\u88C5\uFF1A\u9000\u51FA\u5165\u53E3\u968F\u7248\u672C/\u5E73\u53F0\u800C\u53D8\u2014\u2014\u5916\u90E8\u62A5\u544A\u7684 0.9.0 \u4E00\u7C7B\u5165\u53E3\u4F1A\u89E6\u53D1\u9875\u9762 beforeunload\uFF0C\u800C\u684C\u9762\u58F3\u672A\u5904\u7406 will-prevent-unload \u21D2 \u5426\u51B3\u88AB\u9759\u9ED8\u541E\u6389\u3001\u9000\u51FA\u5361\u4F4F\uFF1B2.0.9 \u7684\u6258\u76D8\u9879\u8D70 requestQuit\u2192window.destroy\u2192app.exit\uFF0C\u6839\u672C\u4E0D\u7ECF\u8FC7\u5B83\u3002\u5224\u636E\u662F\u4E24\u9053\uFF1A\u5BBF\u4E3B\u8BF4\u4F1A\u5F39\u539F\u751F\u6846\uFF08quitVeto\uFF09\uFF0C\u4E14\u672C\u9875\u81EA\u5DF1\u770B\u4E0D\u51FA\u684C\u9762\u75D5\u8FF9\uFF08UA \u65E0 Electron\u3001URL \u65E0 dsh-desktop-\uFF09\u2014\u2014\u4EFB\u4E00\u6761\u4E0D\u6210\u7ACB\u5C31\u4E0D\u6B66\u88C5\uFF1B\u56E0\u6B64\u9875\u9762\u91CC\u5206\u4E0D\u51FA\u662F\u54EA\u4E00\u7C7B\u58F3\u4E5F\u6CA1\u5173\u7CFB\uFF0C\u684C\u9762\u4FDD\u62A4\u7531\u8FD0\u884C\u4E2D\u6A2A\u5E45\u627F\u62C5\u3002\u684C\u9762\u7AEF\u9000\u4E0D\u6389\u65F6\u5148\u5173\u8FD9\u4E00\u9879\uFF08\u7ACB\u5373\u6062\u590D\uFF0C\u4E0D\u7528\u91CD\u542F\uFF09\u3002"
835
+ "options.closeGuardDesc": "\u5F00\uFF08\u9ED8\u8BA4\uFF09\uFF1A\u9875\u9762\u5173\u95ED\u524D\u786E\u8BA4\u9000\u51FA\u2014\u2014\u6709\u8FD0\u884C\u4E2D\u4EFB\u52A1\u6216\u672A\u5B8C\u6210\u5BF9\u8BDD\u65F6\uFF0C\u5F39**\u672C\u63D2\u4EF6\u81EA\u7ED8**\u7684\u4E2D\u6587\u786E\u8BA4\u6846\uFF08[\u53D6\u6D88] / [\u4ECD\u8981\u5173\u95ED]\uFF09\uFF0C\u70B9[\u4ECD\u8981\u5173\u95ED]\u771F\u7684\u9000\u51FA\uFF08\u786E\u8BA4\u6846\u4E00\u76F4\u7B49\u4F60\u7684\u9009\u62E9\uFF0CEsc \u7B49\u540C\u53D6\u6D88\uFF09\uFF1B\u65E0\u4EFB\u52A1\u76F4\u63A5\u5173\uFF0C\u4E0D\u6253\u6270\u3002\u684C\u9762\u7AEF\uFF08DSH Desktop\uFF09\u4E0D\u6B66\u88C5**\u5BBF\u4E3B\u539F\u751F\u786E\u8BA4\u6846**\u90A3\u6761\u8DEF\uFF08\u684C\u9762\u58F3\u672A\u5904\u7406 will-prevent-unload\uFF0C\u5BBF\u4E3B\u6846\u4F1A\u88AB\u9759\u9ED8\u541E\u6389\uFF09\uFF0C\u6539\u7528**\u9875\u9762\u81EA\u7ED8\u786E\u8BA4\u95E8**\uFF1A\u5148\u753B\u6846\u5E76\u786E\u8BA4\u770B\u5F97\u89C1\uFF0C\u624D\u62E6\u4E0B\u5173\u95ED\uFF1B\u6846\u753B\u4E0D\u51FA\u6765\u3001\u4E0D\u53EF\u89C1\u6216\u9875\u9762\u4E0D\u53EF\u89C1 \u21D2 \u5F53\u573A\u653E\u884C\uFF0C\u770B\u95E8\u72D7\uFF08Web Worker \u8BA1\u65F6\u5668\uFF0C\u4E0D\u53D7\u540E\u53F0\u8282\u6D41\uFF09\u53EA\u4F5C\u6700\u540E\u4E00\u9053\u3002\u6B66\u88C5**\u539F\u751F\u95E8**\u7684\u5224\u636E\u662F\u4E24\u9053\uFF1A\u5BBF\u4E3B\u8BF4\u4F1A\u5F39\u539F\u751F\u6846\uFF08quitVeto\uFF09\uFF0C\u4E14\u672C\u9875\u81EA\u5DF1\u770B\u4E0D\u51FA\u684C\u9762\u75D5\u8FF9\uFF08UA \u65E0 Electron\u3001URL \u65E0 dsh-desktop-\uFF09\u2014\u2014\u4EFB\u4E00\u6761\u4E0D\u6210\u7ACB\u5C31\u4E0D\u6B66\u88C5\uFF08\u4E00\u7968\u5426\u51B3\uFF09\u3002\u9000\u51FA\u5165\u53E3\u968F\u7248\u672C/\u5E73\u53F0\u800C\u53D8\u2014\u2014\u5916\u90E8\u62A5\u544A\u7684 0.9.0 \u4E00\u7C7B\u5165\u53E3\u4F1A\u8D70\u5230\u9875\u9762 beforeunload\uFF0C2.0.9 \u7684\u6258\u76D8/\u2318Q \u8D70 requestQuit\u2192window.destroy\u2192app.exit \u4E0D\u7ECF\u8FC7\u672C\u95E8\u3002\u684C\u9762\u7AEF\u9000\u4E0D\u6389\u65F6\u5148\u5173\u8FD9\u4E00\u9879\uFF08\u7ACB\u5373\u6062\u590D\uFF0C\u4E0D\u7528\u91CD\u542F\uFF09\u3002"
499
836
  };
500
837
  var en = {
501
838
  "action.edit": "Edit",
@@ -642,7 +979,7 @@ var en = {
642
979
  "path.round": "Round {n}",
643
980
  "path.empty": "(no conversation rounds yet)",
644
981
  "options.closeGuard": "Exit confirmation (close guard)",
645
- "options.closeGuardDesc": "On (default): confirm before the page closes \u2014 when sessions have running or unfinished work a strong guard lists them (prevents accidental progress loss); otherwise a light one-time confirm. Armed only where the host reports a native confirm dialog (ordinary browser tabs; turning this off also disables the running banner). Desktop (DSH Desktop) never arms it: quit entries differ by version/platform \u2014 the 0.9.0-class entry in the external report does reach page beforeunload, and the shell has no will-prevent-unload handler, so the veto is swallowed silently and the exit hangs; the 2.0.9 tray item goes requestQuit\u2192window.destroy\u2192app.exit and never reaches it. Two criteria must both hold: the host says a native dialog appears (quitVeto) AND this page shows no desktop evidence itself (no Electron in the UA, no dsh-desktop- in the URL); if either fails it stays unarmed, so it does not matter that the page cannot tell which shell it is \u2014 the running banner carries that protection instead. If Desktop cannot quit, turn this off first (recovers immediately, no restart)."
982
+ "options.closeGuardDesc": "On (default): confirm before the page closes \u2014 when sessions have running or unfinished work this plugin shows its own Chinese confirm dialog ([Cancel] / [Close anyway]), and [Close anyway] really exits (the dialog waits for your choice; Esc cancels); with no running work the page closes directly, without interruption. Desktop (DSH Desktop) never arms the host native dialog path (the desktop shell has no will-prevent-unload handler, so that dialog is swallowed silently); it uses the page-drawn confirm gate instead: draw the dialog and verify it is visible first, only then block the close; if it cannot be drawn or shown, or the page is not visible, the close passes immediately, with a watchdog (Web Worker timer, immune to background throttling) as the last resort. Arming the native gate takes both criteria: the host says a native dialog appears (quitVeto) AND this page shows no desktop evidence itself (no Electron in the UA, no dsh-desktop- in the URL) \u2014 if either fails it stays unarmed (a client-side veto). Quit entries differ by version/platform \u2014 the 0.9.0-class entry in the external report does reach page beforeunload, while the 2.0.9 tray/\u2318Q goes requestQuit\u2192window.destroy\u2192app.exit and never reaches this gate. If Desktop cannot quit, turn this off first (recovers immediately, no restart)."
646
983
  };
647
984
  function opFailureText(code, message, t) {
648
985
  if (code === "agent-busy") return t("error.busy");
@@ -658,6 +995,26 @@ function isReplacementSurfaceEvent(event) {
658
995
  return SURFACE_TYPES.has(event.type) && event.surfaceOp !== void 0 && event.surfaceOp !== "append";
659
996
  }
660
997
  var wire = null;
998
+ var badgeBySession = /* @__PURE__ */ new Map();
999
+ var normalizeBadgeKey = (id) => String(id ?? "").replace(/^session-/, "").toLowerCase();
1000
+ function rememberBadge(sessionId, badge) {
1001
+ if (typeof badge !== "string" || badge === "") return;
1002
+ badgeBySession.set(normalizeBadgeKey(sessionId), badge);
1003
+ }
1004
+ function canonicalBadgeOf(sessionId) {
1005
+ const hit = badgeBySession.get(normalizeBadgeKey(sessionId));
1006
+ return typeof hit === "string" ? hit : "";
1007
+ }
1008
+ function rememberBadgeMap(badges) {
1009
+ if (!badges || typeof badges !== "object") return 0;
1010
+ let added = 0;
1011
+ for (const [id, code] of Object.entries(badges)) {
1012
+ const before = canonicalBadgeOf(id);
1013
+ rememberBadge(id, code);
1014
+ if (before === "" && canonicalBadgeOf(id) !== "") added += 1;
1015
+ }
1016
+ return added;
1017
+ }
661
1018
  function __setMessageEditorWire(fn) {
662
1019
  wire = fn;
663
1020
  }
@@ -1613,6 +1970,93 @@ function reportJumpUnavailable(reason, detail = {}) {
1613
1970
  } catch {
1614
1971
  }
1615
1972
  }
1973
+ function reportBadgeFailure(op, detail) {
1974
+ const message = detail instanceof Error ? detail.message : String(detail ?? "unknown");
1975
+ console.warn(`[dsh-retrace] ${op} failed: ${message}`);
1976
+ try {
1977
+ const sent = callOp("clientReport", { id: name, source: `${op}-failed: ${message}`.slice(0, 200) });
1978
+ if (sent && typeof sent.catch === "function") sent.catch(() => {
1979
+ });
1980
+ } catch {
1981
+ }
1982
+ }
1983
+ var badgeInfoSent = /* @__PURE__ */ new Set();
1984
+ function reportBadgeInfo(op, detail) {
1985
+ const key = `${op}:${detail}`;
1986
+ if (badgeInfoSent.has(key)) return;
1987
+ badgeInfoSent.add(key);
1988
+ console.warn(`[dsh-retrace] ${op}: ${detail}`);
1989
+ try {
1990
+ const sent = callOp("clientReport", { id: name, source: `${op}: ${String(detail).slice(0, 180)}` });
1991
+ if (sent && typeof sent.catch === "function") sent.catch(() => {
1992
+ });
1993
+ } catch {
1994
+ }
1995
+ }
1996
+ function safeSchedule(fn, delayMs) {
1997
+ try {
1998
+ const schedule = typeof window === "undefined" ? void 0 : window.setTimeout;
1999
+ if (typeof schedule === "function") {
2000
+ const handle = schedule(fn, delayMs);
2001
+ return () => {
2002
+ try {
2003
+ window.clearTimeout(handle);
2004
+ } catch {
2005
+ }
2006
+ };
2007
+ }
2008
+ if (typeof queueMicrotask === "function") {
2009
+ queueMicrotask(fn);
2010
+ return () => {
2011
+ };
2012
+ }
2013
+ } catch {
2014
+ }
2015
+ return () => {
2016
+ };
2017
+ }
2018
+ function bootstrapBadgeTitles({ attempts = 5, delayMs = 3e3 } = {}) {
2019
+ if (globalThis.__DSH_RETRACE_BADGE_BOOTSTRAP !== true) return () => {
2020
+ };
2021
+ let mapPending;
2022
+ try {
2023
+ mapPending = callOp("badgeMap", {});
2024
+ } catch (cause) {
2025
+ mapPending = Promise.reject(cause);
2026
+ }
2027
+ Promise.resolve(mapPending).then((result) => {
2028
+ if (result && result.ok === true) reportBadgeInfo("badgeMap", `loaded ${rememberBadgeMap(result.value?.badges)}`);
2029
+ else reportBadgeFailure("badgeMap", result?.error?.message ?? "host returned ok:false");
2030
+ }).catch((cause) => reportBadgeFailure("badgeMap", cause));
2031
+ let cancelTimer = () => {
2032
+ };
2033
+ let cancelled = false;
2034
+ let left = attempts;
2035
+ const attempt = () => {
2036
+ if (cancelled) return;
2037
+ let pending;
2038
+ try {
2039
+ pending = callOp("initBadgeTitles", {});
2040
+ } catch (cause) {
2041
+ pending = Promise.reject(cause);
2042
+ }
2043
+ Promise.resolve(pending).then((result) => {
2044
+ if (result && result.ok === true) return;
2045
+ throw new Error(result?.error?.message ?? "host returned ok:false");
2046
+ }).catch((cause) => {
2047
+ if (cancelled) return;
2048
+ left -= 1;
2049
+ const suffix = left > 0 ? `(retry, ${left} left)` : "(retries exhausted)";
2050
+ reportBadgeFailure("initBadgeTitles", `${cause?.message ?? cause} ${suffix}`);
2051
+ if (left > 0) cancelTimer = safeSchedule(attempt, delayMs);
2052
+ });
2053
+ };
2054
+ cancelTimer = safeSchedule(attempt, 0);
2055
+ return () => {
2056
+ cancelled = true;
2057
+ if (cancelTimer) cancelTimer();
2058
+ };
2059
+ }
1616
2060
  async function jumpToAnchor(store, anchorSeq, readNodes) {
1617
2061
  const { key, reason, pages } = await resolveAnchorKey({
1618
2062
  anchorSeq,
@@ -2044,14 +2488,21 @@ function RetraceView({ sessionId, useChat, useProjection, t, actions, store }) {
2044
2488
  refreshGit();
2045
2489
  refreshDigests();
2046
2490
  timelineGet(`/doctor?sessionId=${encodeURIComponent(sessionId)}`).then((result) => setDoctor(result?.ok === true ? result.value : null)).catch(() => setDoctor(null));
2047
- callOp("setBadgeTitle", { sessionId }).catch(() => {
2048
- });
2491
+ callOp("setBadgeTitle", { sessionId }).then((result) => {
2492
+ if (!result || result.ok !== true) {
2493
+ reportBadgeFailure("setBadgeTitle", result?.error?.message ?? "host returned ok:false");
2494
+ return;
2495
+ }
2496
+ reportBadgeInfo("setBadgeTitle", `${sessionId} \u2192 ${result.value?.title ?? ""}${result.value?.alreadyTagged === true ? "(alreadyTagged)" : ""}`);
2497
+ }).catch((cause) => reportBadgeFailure("setBadgeTitle", cause));
2049
2498
  callOp("sessionBadge", { sessionId }).then((result) => {
2050
2499
  if (result && result.ok === true && typeof result.value?.badge === "string") {
2051
2500
  setBadge(result.value.badge);
2501
+ rememberBadge(sessionId, result.value.badge);
2502
+ } else {
2503
+ reportBadgeFailure("sessionBadge", result?.error?.message ?? "response has no badge");
2052
2504
  }
2053
- }).catch(() => {
2054
- });
2505
+ }).catch((cause) => reportBadgeFailure("sessionBadge", cause));
2055
2506
  }, []);
2056
2507
  const jump = (boundarySeq) => jumpToAnchor(store, boundarySeq, () => chatNodesRef.current);
2057
2508
  const jumpRow = (row) => jump(jumpTargetOf(row));
@@ -2721,14 +3172,7 @@ function installCloseGuard(t) {
2721
3172
  const ui = { banner: null, modal: null, toast: null, toastTimer: 0, dismissed: null, expanded: false, lastSig: "" };
2722
3173
  let supported = true;
2723
3174
  let failures = 0;
2724
- const labelOf = (sessionId) => {
2725
- try {
2726
- const badge = sessionBadge(sessionId);
2727
- return badge || String(sessionId);
2728
- } catch {
2729
- return String(sessionId);
2730
- }
2731
- };
3175
+ const labelOf = (sessionId) => canonicalBadgeOf(sessionId) || String(sessionId);
2732
3176
  const runningSig = (snapshot) => {
2733
3177
  const running = snapshot?.running;
2734
3178
  if (!Array.isArray(running)) return "";
@@ -2771,7 +3215,7 @@ function installCloseGuard(t) {
2771
3215
  }
2772
3216
  }
2773
3217
  function bannerNote(lang) {
2774
- 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\u4E0D\u6B66\u88C5\u539F\u751F\u9000\u51FA\u786E\u8BA4,\u8BF7\u4EE5\u6B64\u6A2A\u5E45\u4E3A\u51C6)" : "Confirm before closing/exiting \u2014 interrupting work may lose progress (no native exit prompt on Desktop; rely on this banner)";
3218
+ 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\u7528\u9875\u9762\u81EA\u7ED8\u786E\u8BA4\u6846;\u82E5\u672A\u5F39\u51FA,\u4EE5\u6B64\u6A2A\u5E45\u4E3A\u51C6)" : "Confirm before closing/exiting \u2014 interrupting work may lose progress (Desktop uses a page-drawn confirm dialog; if it does not appear, rely on this banner)";
2775
3219
  }
2776
3220
  function renderBanner() {
2777
3221
  const cfg = getConfig();
@@ -2968,6 +3412,15 @@ function apply(ctx) {
2968
3412
  const disposeStyle = ensureStyle();
2969
3413
  ctx.effect(() => () => disposeStyle(), "dsh-retrace: styles");
2970
3414
  ctx.effect(() => ctx.locale.register(NS, { zh, en }), "dsh-retrace: dictionaries");
3415
+ ctx.effect(() => {
3416
+ try {
3417
+ return bootstrapBadgeTitles();
3418
+ } catch (error) {
3419
+ reportBadgeFailure("initBadgeTitles", error);
3420
+ return () => {
3421
+ };
3422
+ }
3423
+ }, "dsh-retrace: init badge titles");
2971
3424
  const t = ctx.locale.bind(NS);
2972
3425
  let disposeCloseGuard = () => {
2973
3426
  };