dsh-mobile 0.6.3 → 0.6.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -598,7 +598,12 @@ window.__ModuleLoader__.load({
598
598
  cpolarDownloadTimeout: "The cpolar download timed out. Check this computer’s network or proxy, then retry.",
599
599
  cpolarDownloadHttp: "The official cpolar download returned HTTP {status}. Retry later or check the official download page.",
600
600
  cpolarIntegrityFailed: "The cpolar download did not match the pinned build, so it was not installed. Retry later or update DSH Mobile; do not bypass verification.",
601
- cpolarExtractFailed: "The cpolar download completed, but extraction failed. Check system extraction tools and security software, then retry.",
601
+ cpolarExtractFailed: "The cpolar download was verified, but extraction failed. Check the extraction stage in the DSH log; the message alone does not establish a security-software block.",
602
+ cpolarExtractToolsFailed: "A required Windows extraction tool is unavailable. Check system PowerShell, tar and expand; no administrator startup or service installation is required.",
603
+ cpolarExtractArchiveFailed: "Could not extract the verified cpolar ZIP or locate its MSI. Check the ZIP extraction stage in the DSH log.",
604
+ cpolarExtractDatabaseFailed: "Could not read the cpolar MSI database or its embedded cabinet. Check the MSI read stage in the DSH log; do not install the MSI as a workaround.",
605
+ cpolarExtractCabinetFailed: "Could not extract the cpolar executable from its cabinet. Check the cabinet extraction stage and private-directory permissions.",
606
+ cpolarExtractFileFailed: "Could not read or copy the extracted cpolar executable. Check private-directory permissions and disk space; your existing component is retained.",
602
607
  cpolarStorageFailed: "Could not write the cpolar component in DSH Mobile’s private directory. Check disk space, permissions, and security software, then retry."
603
608
  });
604
609
  Object.assign(MOBILE_CONTROL_MESSAGES.it, {
@@ -623,7 +628,12 @@ window.__ModuleLoader__.load({
623
628
  cpolarDownloadTimeout: "Download di cpolar scaduto. Controlla rete o proxy del computer e riprova.",
624
629
  cpolarDownloadHttp: "Il download ufficiale di cpolar ha restituito HTTP {status}. Riprova più tardi o controlla la pagina ufficiale.",
625
630
  cpolarIntegrityFailed: "Il pacchetto cpolar non corrisponde alla versione verificata e non è stato installato. Riprova più tardi o aggiorna DSH Mobile; non aggirare la verifica.",
626
- cpolarExtractFailed: "Download di cpolar completato, ma estrazione non riuscita. Controlla gli strumenti di estrazione e il software di sicurezza, poi riprova.",
631
+ cpolarExtractFailed: "Download di cpolar verificato, ma estrazione non riuscita. Controlla la fase nel log DSH; il messaggio non dimostra un blocco del software di sicurezza.",
632
+ cpolarExtractToolsFailed: "Uno strumento di estrazione Windows richiesto non è disponibile. Controlla PowerShell di sistema, tar ed expand; non serve avviare come amministratore o installare un servizio.",
633
+ cpolarExtractArchiveFailed: "Impossibile estrarre lo ZIP cpolar verificato o trovare il suo MSI. Controlla la fase ZIP nel log DSH.",
634
+ cpolarExtractDatabaseFailed: "Impossibile leggere il database MSI cpolar o il cabinet incorporato. Controlla la fase di lettura MSI nel log DSH; non installare il MSI per aggirare il problema.",
635
+ cpolarExtractCabinetFailed: "Impossibile estrarre il programma cpolar dal cabinet. Controlla la fase cabinet e i permessi della directory privata.",
636
+ cpolarExtractFileFailed: "Impossibile leggere o copiare il programma cpolar estratto. Controlla permessi e spazio nella directory privata; il componente esistente è conservato.",
627
637
  cpolarStorageFailed: "Impossibile scrivere cpolar nella directory privata di DSH Mobile. Controlla spazio, permessi e software di sicurezza, poi riprova."
628
638
  });
629
639
  Object.assign(MOBILE_CONTROL_MESSAGES.zh, {
@@ -648,7 +658,12 @@ window.__ModuleLoader__.load({
648
658
  cpolarDownloadTimeout: "下载 cpolar 组件超时。请检查这台电脑的网络或代理,然后重试。",
649
659
  cpolarDownloadHttp: "cpolar 官网下载返回 HTTP {status}。请稍后重试,或检查官网下载页。",
650
660
  cpolarIntegrityFailed: "下载的 cpolar 组件未通过固定版本校验,因此没有安装。请稍后重试或更新 DSH Mobile,不要跳过校验。",
651
- cpolarExtractFailed: "cpolar 下载完成,但解包失败。请检查系统解压组件和安全软件,然后重试。",
661
+ cpolarExtractFailed: "cpolar 下载已校验,但提取失败。请查看 DSH 日志中的具体阶段,不能仅凭此提示认定安全软件拦截。",
662
+ cpolarExtractToolsFailed: "所需的 Windows 提取工具不可用。请检查系统 PowerShell、tar 和 expand;无需以管理员启动 DSH 或安装系统服务。",
663
+ cpolarExtractArchiveFailed: "无法展开已校验的 cpolar ZIP 或找到其中的 MSI。请查看 DSH 日志中的 ZIP 提取阶段。",
664
+ cpolarExtractDatabaseFailed: "无法读取 cpolar MSI 数据库或内嵌 CAB。请查看 DSH 日志中的 MSI 读取阶段,不要用运行 MSI 安装程序绕过此问题。",
665
+ cpolarExtractCabinetFailed: "无法从 CAB 中提取 cpolar 程序。请查看 CAB 提取阶段及私有目录权限。",
666
+ cpolarExtractFileFailed: "无法读取或复制已提取的 cpolar 程序。请检查私有目录权限和磁盘空间;已有组件不会被替换。",
652
667
  cpolarStorageFailed: "无法把 cpolar 写入 DSH Mobile 私有目录。请检查磁盘空间、目录权限和安全软件,然后重试。"
653
668
  });
654
669
  Object.assign(MOBILE_CONTROL_MESSAGES.en, {
@@ -2478,19 +2493,396 @@ window.__ModuleLoader__.load({
2478
2493
  function createFrpAttachTemplate(settings, options = {}) {
2479
2494
  return createFrpAttachTemplateParts(settings, options).text;
2480
2495
  }
2481
- /** Wire bounds mirrored by the Android notification policy. */
2482
- const TASK_NOTIFY_LIMITS = Object.freeze({
2496
+ //#endregion
2497
+ //#region node_modules/@noble/hashes/esm/utils.js
2498
+ /*! noble-hashes - MIT License (c) 2022 Paul Miller (paulmillr.com) */
2499
+ /** Checks if something is Uint8Array. Be careful: nodejs Buffer will return true. */
2500
+ function isBytes(a) {
2501
+ return a instanceof Uint8Array || ArrayBuffer.isView(a) && a.constructor.name === "Uint8Array";
2502
+ }
2503
+ /** Asserts something is Uint8Array. */
2504
+ function abytes(b, ...lengths) {
2505
+ if (!isBytes(b)) throw new Error("Uint8Array expected");
2506
+ if (lengths.length > 0 && !lengths.includes(b.length)) throw new Error("Uint8Array expected of length " + lengths + ", got length=" + b.length);
2507
+ }
2508
+ /** Asserts a hash instance has not been destroyed / finished */
2509
+ function aexists(instance, checkFinished = true) {
2510
+ if (instance.destroyed) throw new Error("Hash instance has been destroyed");
2511
+ if (checkFinished && instance.finished) throw new Error("Hash#digest() has already been called");
2512
+ }
2513
+ /** Asserts output is properly-sized byte array */
2514
+ function aoutput(out, instance) {
2515
+ abytes(out);
2516
+ const min = instance.outputLen;
2517
+ if (out.length < min) throw new Error("digestInto() expects output buffer of length at least " + min);
2518
+ }
2519
+ /** Zeroize a byte array. Warning: JS provides no guarantees. */
2520
+ function clean(...arrays) {
2521
+ for (let i = 0; i < arrays.length; i++) arrays[i].fill(0);
2522
+ }
2523
+ /** Create DataView of an array for easy byte-level manipulation. */
2524
+ function createView(arr) {
2525
+ return new DataView(arr.buffer, arr.byteOffset, arr.byteLength);
2526
+ }
2527
+ /** The rotate right (circular right shift) operation for uint32 */
2528
+ function rotr(word, shift) {
2529
+ return word << 32 - shift | word >>> shift;
2530
+ }
2531
+ const hasHexBuiltin = /* @__PURE__ */ (() => typeof Uint8Array.from([]).toHex === "function" && typeof Uint8Array.fromHex === "function")();
2532
+ const hexes = /* @__PURE__ */ Array.from({ length: 256 }, (_, i) => i.toString(16).padStart(2, "0"));
2533
+ /**
2534
+ * Convert byte array to hex string. Uses built-in function, when available.
2535
+ * @example bytesToHex(Uint8Array.from([0xca, 0xfe, 0x01, 0x23])) // 'cafe0123'
2536
+ */
2537
+ function bytesToHex(bytes) {
2538
+ abytes(bytes);
2539
+ if (hasHexBuiltin) return bytes.toHex();
2540
+ let hex = "";
2541
+ for (let i = 0; i < bytes.length; i++) hex += hexes[bytes[i]];
2542
+ return hex;
2543
+ }
2544
+ /**
2545
+ * Converts string to bytes using UTF8 encoding.
2546
+ * @example utf8ToBytes('abc') // Uint8Array.from([97, 98, 99])
2547
+ */
2548
+ function utf8ToBytes(str) {
2549
+ if (typeof str !== "string") throw new Error("string expected");
2550
+ return new Uint8Array(new TextEncoder().encode(str));
2551
+ }
2552
+ /**
2553
+ * Normalizes (non-hex) string or Uint8Array to Uint8Array.
2554
+ * Warning: when Uint8Array is passed, it would NOT get copied.
2555
+ * Keep in mind for future mutable operations.
2556
+ */
2557
+ function toBytes(data) {
2558
+ if (typeof data === "string") data = utf8ToBytes(data);
2559
+ abytes(data);
2560
+ return data;
2561
+ }
2562
+ /** For runtime check if class implements interface */
2563
+ var Hash = class {};
2564
+ /** Wraps hash function, creating an interface on top of it */
2565
+ function createHasher(hashCons) {
2566
+ const hashC = (msg) => hashCons().update(toBytes(msg)).digest();
2567
+ const tmp = hashCons();
2568
+ hashC.outputLen = tmp.outputLen;
2569
+ hashC.blockLen = tmp.blockLen;
2570
+ hashC.create = () => hashCons();
2571
+ return hashC;
2572
+ }
2573
+ //#endregion
2574
+ //#region node_modules/@noble/hashes/esm/_md.js
2575
+ /**
2576
+ * Internal Merkle-Damgard hash utils.
2577
+ * @module
2578
+ */
2579
+ /** Polyfill for Safari 14. https://caniuse.com/mdn-javascript_builtins_dataview_setbiguint64 */
2580
+ function setBigUint64(view, byteOffset, value, isLE) {
2581
+ if (typeof view.setBigUint64 === "function") return view.setBigUint64(byteOffset, value, isLE);
2582
+ const _32n = BigInt(32);
2583
+ const _u32_max = BigInt(4294967295);
2584
+ const wh = Number(value >> _32n & _u32_max);
2585
+ const wl = Number(value & _u32_max);
2586
+ const h = isLE ? 4 : 0;
2587
+ const l = isLE ? 0 : 4;
2588
+ view.setUint32(byteOffset + h, wh, isLE);
2589
+ view.setUint32(byteOffset + l, wl, isLE);
2590
+ }
2591
+ /** Choice: a ? b : c */
2592
+ function Chi(a, b, c) {
2593
+ return a & b ^ ~a & c;
2594
+ }
2595
+ /** Majority function, true if any two inputs is true. */
2596
+ function Maj(a, b, c) {
2597
+ return a & b ^ a & c ^ b & c;
2598
+ }
2599
+ /**
2600
+ * Merkle-Damgard hash construction base class.
2601
+ * Could be used to create MD5, RIPEMD, SHA1, SHA2.
2602
+ */
2603
+ var HashMD = class extends Hash {
2604
+ constructor(blockLen, outputLen, padOffset, isLE) {
2605
+ super();
2606
+ this.finished = false;
2607
+ this.length = 0;
2608
+ this.pos = 0;
2609
+ this.destroyed = false;
2610
+ this.blockLen = blockLen;
2611
+ this.outputLen = outputLen;
2612
+ this.padOffset = padOffset;
2613
+ this.isLE = isLE;
2614
+ this.buffer = new Uint8Array(blockLen);
2615
+ this.view = createView(this.buffer);
2616
+ }
2617
+ update(data) {
2618
+ aexists(this);
2619
+ data = toBytes(data);
2620
+ abytes(data);
2621
+ const { view, buffer, blockLen } = this;
2622
+ const len = data.length;
2623
+ for (let pos = 0; pos < len;) {
2624
+ const take = Math.min(blockLen - this.pos, len - pos);
2625
+ if (take === blockLen) {
2626
+ const dataView = createView(data);
2627
+ for (; blockLen <= len - pos; pos += blockLen) this.process(dataView, pos);
2628
+ continue;
2629
+ }
2630
+ buffer.set(data.subarray(pos, pos + take), this.pos);
2631
+ this.pos += take;
2632
+ pos += take;
2633
+ if (this.pos === blockLen) {
2634
+ this.process(view, 0);
2635
+ this.pos = 0;
2636
+ }
2637
+ }
2638
+ this.length += data.length;
2639
+ this.roundClean();
2640
+ return this;
2641
+ }
2642
+ digestInto(out) {
2643
+ aexists(this);
2644
+ aoutput(out, this);
2645
+ this.finished = true;
2646
+ const { buffer, view, blockLen, isLE } = this;
2647
+ let { pos } = this;
2648
+ buffer[pos++] = 128;
2649
+ clean(this.buffer.subarray(pos));
2650
+ if (this.padOffset > blockLen - pos) {
2651
+ this.process(view, 0);
2652
+ pos = 0;
2653
+ }
2654
+ for (let i = pos; i < blockLen; i++) buffer[i] = 0;
2655
+ setBigUint64(view, blockLen - 8, BigInt(this.length * 8), isLE);
2656
+ this.process(view, 0);
2657
+ const oview = createView(out);
2658
+ const len = this.outputLen;
2659
+ if (len % 4) throw new Error("_sha2: outputLen should be aligned to 32bit");
2660
+ const outLen = len / 4;
2661
+ const state = this.get();
2662
+ if (outLen > state.length) throw new Error("_sha2: outputLen bigger than state");
2663
+ for (let i = 0; i < outLen; i++) oview.setUint32(4 * i, state[i], isLE);
2664
+ }
2665
+ digest() {
2666
+ const { buffer, outputLen } = this;
2667
+ this.digestInto(buffer);
2668
+ const res = buffer.slice(0, outputLen);
2669
+ this.destroy();
2670
+ return res;
2671
+ }
2672
+ _cloneInto(to) {
2673
+ to || (to = new this.constructor());
2674
+ to.set(...this.get());
2675
+ const { blockLen, buffer, length, finished, destroyed, pos } = this;
2676
+ to.destroyed = destroyed;
2677
+ to.finished = finished;
2678
+ to.length = length;
2679
+ to.pos = pos;
2680
+ if (length % blockLen) to.buffer.set(buffer);
2681
+ return to;
2682
+ }
2683
+ clone() {
2684
+ return this._cloneInto();
2685
+ }
2686
+ };
2687
+ /**
2688
+ * Initial SHA-2 state: fractional parts of square roots of first 16 primes 2..53.
2689
+ * Check out `test/misc/sha2-gen-iv.js` for recomputation guide.
2690
+ */
2691
+ /** Initial SHA256 state. Bits 0..32 of frac part of sqrt of primes 2..19 */
2692
+ const SHA256_IV = /* @__PURE__ */ Uint32Array.from([
2693
+ 1779033703,
2694
+ 3144134277,
2695
+ 1013904242,
2696
+ 2773480762,
2697
+ 1359893119,
2698
+ 2600822924,
2699
+ 528734635,
2700
+ 1541459225
2701
+ ]);
2702
+ //#endregion
2703
+ //#region node_modules/@noble/hashes/esm/sha2.js
2704
+ /**
2705
+ * SHA2 hash function. A.k.a. sha256, sha384, sha512, sha512_224, sha512_256.
2706
+ * SHA256 is the fastest hash implementable in JS, even faster than Blake3.
2707
+ * Check out [RFC 4634](https://datatracker.ietf.org/doc/html/rfc4634) and
2708
+ * [FIPS 180-4](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.180-4.pdf).
2709
+ * @module
2710
+ */
2711
+ /**
2712
+ * Round constants:
2713
+ * First 32 bits of fractional parts of the cube roots of the first 64 primes 2..311)
2714
+ */
2715
+ const SHA256_K = /* @__PURE__ */ Uint32Array.from([
2716
+ 1116352408,
2717
+ 1899447441,
2718
+ 3049323471,
2719
+ 3921009573,
2720
+ 961987163,
2721
+ 1508970993,
2722
+ 2453635748,
2723
+ 2870763221,
2724
+ 3624381080,
2725
+ 310598401,
2726
+ 607225278,
2727
+ 1426881987,
2728
+ 1925078388,
2729
+ 2162078206,
2730
+ 2614888103,
2731
+ 3248222580,
2732
+ 3835390401,
2733
+ 4022224774,
2734
+ 264347078,
2735
+ 604807628,
2736
+ 770255983,
2737
+ 1249150122,
2738
+ 1555081692,
2739
+ 1996064986,
2740
+ 2554220882,
2741
+ 2821834349,
2742
+ 2952996808,
2743
+ 3210313671,
2744
+ 3336571891,
2745
+ 3584528711,
2746
+ 113926993,
2747
+ 338241895,
2748
+ 666307205,
2749
+ 773529912,
2750
+ 1294757372,
2751
+ 1396182291,
2752
+ 1695183700,
2753
+ 1986661051,
2754
+ 2177026350,
2755
+ 2456956037,
2756
+ 2730485921,
2757
+ 2820302411,
2758
+ 3259730800,
2759
+ 3345764771,
2760
+ 3516065817,
2761
+ 3600352804,
2762
+ 4094571909,
2763
+ 275423344,
2764
+ 430227734,
2765
+ 506948616,
2766
+ 659060556,
2767
+ 883997877,
2768
+ 958139571,
2769
+ 1322822218,
2770
+ 1537002063,
2771
+ 1747873779,
2772
+ 1955562222,
2773
+ 2024104815,
2774
+ 2227730452,
2775
+ 2361852424,
2776
+ 2428436474,
2777
+ 2756734187,
2778
+ 3204031479,
2779
+ 3329325298
2780
+ ]);
2781
+ /** Reusable temporary buffer. "W" comes straight from spec. */
2782
+ const SHA256_W = /* @__PURE__ */ new Uint32Array(64);
2783
+ var SHA256 = class extends HashMD {
2784
+ constructor(outputLen = 32) {
2785
+ super(64, outputLen, 8, false);
2786
+ this.A = SHA256_IV[0] | 0;
2787
+ this.B = SHA256_IV[1] | 0;
2788
+ this.C = SHA256_IV[2] | 0;
2789
+ this.D = SHA256_IV[3] | 0;
2790
+ this.E = SHA256_IV[4] | 0;
2791
+ this.F = SHA256_IV[5] | 0;
2792
+ this.G = SHA256_IV[6] | 0;
2793
+ this.H = SHA256_IV[7] | 0;
2794
+ }
2795
+ get() {
2796
+ const { A, B, C, D, E, F, G, H } = this;
2797
+ return [
2798
+ A,
2799
+ B,
2800
+ C,
2801
+ D,
2802
+ E,
2803
+ F,
2804
+ G,
2805
+ H
2806
+ ];
2807
+ }
2808
+ set(A, B, C, D, E, F, G, H) {
2809
+ this.A = A | 0;
2810
+ this.B = B | 0;
2811
+ this.C = C | 0;
2812
+ this.D = D | 0;
2813
+ this.E = E | 0;
2814
+ this.F = F | 0;
2815
+ this.G = G | 0;
2816
+ this.H = H | 0;
2817
+ }
2818
+ process(view, offset) {
2819
+ for (let i = 0; i < 16; i++, offset += 4) SHA256_W[i] = view.getUint32(offset, false);
2820
+ for (let i = 16; i < 64; i++) {
2821
+ const W15 = SHA256_W[i - 15];
2822
+ const W2 = SHA256_W[i - 2];
2823
+ const s0 = rotr(W15, 7) ^ rotr(W15, 18) ^ W15 >>> 3;
2824
+ const s1 = rotr(W2, 17) ^ rotr(W2, 19) ^ W2 >>> 10;
2825
+ SHA256_W[i] = s1 + SHA256_W[i - 7] + s0 + SHA256_W[i - 16] | 0;
2826
+ }
2827
+ let { A, B, C, D, E, F, G, H } = this;
2828
+ for (let i = 0; i < 64; i++) {
2829
+ const sigma1 = rotr(E, 6) ^ rotr(E, 11) ^ rotr(E, 25);
2830
+ const T1 = H + sigma1 + Chi(E, F, G) + SHA256_K[i] + SHA256_W[i] | 0;
2831
+ const T2 = (rotr(A, 2) ^ rotr(A, 13) ^ rotr(A, 22)) + Maj(A, B, C) | 0;
2832
+ H = G;
2833
+ G = F;
2834
+ F = E;
2835
+ E = D + T1 | 0;
2836
+ D = C;
2837
+ C = B;
2838
+ B = A;
2839
+ A = T1 + T2 | 0;
2840
+ }
2841
+ A = A + this.A | 0;
2842
+ B = B + this.B | 0;
2843
+ C = C + this.C | 0;
2844
+ D = D + this.D | 0;
2845
+ E = E + this.E | 0;
2846
+ F = F + this.F | 0;
2847
+ G = G + this.G | 0;
2848
+ H = H + this.H | 0;
2849
+ this.set(A, B, C, D, E, F, G, H);
2850
+ }
2851
+ roundClean() {
2852
+ clean(SHA256_W);
2853
+ }
2854
+ destroy() {
2855
+ this.set(0, 0, 0, 0, 0, 0, 0, 0);
2856
+ clean(this.buffer);
2857
+ }
2858
+ };
2859
+ //#endregion
2860
+ //#region node_modules/@noble/hashes/esm/sha256.js
2861
+ /**
2862
+ * SHA2-256 a.k.a. sha256. In JS, it is the fastest hash, even faster than Blake3.
2863
+ *
2864
+ * To break sha256 using birthday attack, attackers need to try 2^128 hashes.
2865
+ * BTC network is doing 2^70 hashes/sec (2^95 hashes/year) as per 2025.
2866
+ *
2867
+ * Check out [FIPS 180-4](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.180-4.pdf).
2868
+ * @module
2869
+ * @deprecated
2870
+ */
2871
+ /** @deprecated Use import from `noble/hashes/sha2` module */
2872
+ const sha256 = /* @__PURE__ */ createHasher(() => new SHA256());
2873
+ Object.freeze({
2483
2874
  title: 80,
2484
2875
  body: 200,
2485
2876
  tag: 64
2486
2877
  });
2487
2878
  const MAX_SEEN_QUESTION_KEYS = 20;
2488
- function sanitizeTagFragment(value) {
2489
- return value.toLowerCase().replaceAll(/[^a-z0-9-]+/gu, "-").replaceAll(/^-+|-+$/gu, "").slice(0, TASK_NOTIFY_LIMITS.tag);
2879
+ /** A bounded native identifier derived from the complete, case-sensitive wire ID. */
2880
+ function notificationIdentity(value) {
2881
+ return bytesToHex(sha256(JSON.stringify(value)).subarray(0, 16));
2490
2882
  }
2491
2883
  /** Stable per-turn tag so concurrent sessions keep separate notifications. */
2492
2884
  function taskCompletionTag(sessionId, turn) {
2493
- return `dsh-task-done-${sanitizeTagFragment(`${sessionId}-${String(turn)}`) || "task"}`.slice(0, TASK_NOTIFY_LIMITS.tag);
2885
+ return `dsh-task-done-${notificationIdentity(sessionId)}-${String(turn)}`;
2494
2886
  }
2495
2887
  /** Decide whether the current snapshot deserves a system notification. */
2496
2888
  var TaskNotifyTracker = class {
@@ -2581,7 +2973,7 @@ window.__ModuleLoader__.load({
2581
2973
  }
2582
2974
  event(kind, sessionLabel, questionKey) {
2583
2975
  const text = this.format(kind, sessionLabel);
2584
- const tag = kind === "question" && questionKey !== void 0 ? `dsh-task-question-${sanitizeTagFragment(questionKey) || "pending"}` : "dsh-task-done";
2976
+ const tag = kind === "question" && questionKey !== void 0 ? `dsh-task-question-${notificationIdentity(questionKey)}` : "dsh-task-done";
2585
2977
  return Object.freeze({
2586
2978
  kind,
2587
2979
  title: text.title,
@@ -2666,7 +3058,7 @@ window.__ModuleLoader__.load({
2666
3058
  if (typeof record.sessionId !== "string" || record.sessionId.trim() === "") return void 0;
2667
3059
  const turn = typeof record.turn === "number" && Number.isSafeInteger(record.turn) && record.turn >= 0 ? record.turn : 0;
2668
3060
  return Object.freeze({
2669
- sessionId: record.sessionId.slice(0, 128),
3061
+ sessionId: record.sessionId,
2670
3062
  turn
2671
3063
  });
2672
3064
  }
@@ -2796,6 +3188,10 @@ window.__ModuleLoader__.load({
2796
3188
  [data-dsh-mobile-composer-controls] button:not(:where(dialog *,[popover] *,[role="dialog"] *,[role="menu"] *,[role="listbox"] *,[data-trigger-menu] *)) { white-space:normal !important; overflow-wrap:anywhere !important; }
2797
3189
  @media (max-width:720px) {
2798
3190
  [data-dsh-mobile-composer-row] button:not([role="switch"]):not(:where(dialog *,[popover] *,[role="dialog"] *,[role="menu"] *,[role="listbox"] *,[data-trigger-menu] *)) { min-width:44px !important; min-height:44px !important; touch-action:manipulation; }
3191
+ /* Public slot icon wrappers must reserve the same space as their enlarged
3192
+ child targets. Keep wrapper and tooltip ownership intact; pure switches,
3193
+ dialogs and wrappers without direct non-switch buttons are not selected. */
3194
+ :is([data-dsh-mobile-composer-tools],[data-dsh-mobile-composer-controls]) > :is([data-slot="conversation.input.left"],[data-slot="conversation.input.right"]) > :where(span,div):not(:where(dialog,[popover],[role="dialog"],[role="menu"],[role="listbox"],[data-trigger-menu])):has(> button:not([role="switch"])) { min-width:44px !important; min-height:44px !important; flex-shrink:0 !important; }
2799
3195
  [data-dsh-mobile-composer-trailing] > button[class*="_primary"] { min-width:44px !important; flex-shrink:0 !important; }
2800
3196
  [data-dsh-mobile-composer-row] { display:flex !important; flex-wrap:wrap !important; align-items:center !important; gap:4px 8px !important; min-width:0 !important; max-width:100% !important; }
2801
3197
  [data-dsh-mobile-composer-tools]:not([hidden]) { display:flex !important; flex:0 1 auto !important; flex-wrap:wrap !important; width:auto !important; min-width:0 !important; max-width:100% !important; gap:6px !important; }
@@ -2855,6 +3251,8 @@ window.__ModuleLoader__.load({
2855
3251
  .dsh-mobile-settings_fontFamily > span { flex:1 1 100px; }
2856
3252
  .dsh-mobile-settings_fontFamily input { box-sizing:border-box; flex:1 1 180px; width:100%; min-width:0; min-height:48px; padding:8px 12px; border:0.5px solid var(--dsw-alias-border-l2); border-radius:10px; background:var(--dsw-alias-bg-module-platform); color:var(--dsw-alias-label-primary); font:inherit; }
2857
3253
  .dsh-mobile-settings_fontFamily input:focus-visible,.dsh-mobile-settings_fontDetails > summary:focus-visible { outline:2px solid var(--dsw-alias-label-primary-bluish,#2563eb); outline-offset:2px; }
3254
+ html.dsh-native-mobile-active [data-dsh-mobile-settings-header] { box-sizing:border-box !important; height:56px !important; min-height:56px !important; padding:4px 12px !important; }
3255
+ html.dsh-native-mobile-active [data-dsh-mobile-settings-header] [class*="_close"] { box-sizing:border-box !important; flex:none !important; width:48px !important; min-width:48px !important; height:48px !important; min-height:48px !important; }
2858
3256
  /* The mobile row replaces the Host-backed font control, not the theme selector. */
2859
3257
  html.dsh-native-mobile-active [data-slot="settings.general.item"]:has([data-mobile-font-setting]) > :not([data-mobile-font-setting])[class*="_row"]:has([class*="_control"] > [class*="_stepper"] > [class*="_arrows"]) { display:none !important; }
2860
3258
  @media ${NATIVE_MOBILE_OVERLAY_QUERY} {
@@ -2924,8 +3322,6 @@ window.__ModuleLoader__.load({
2924
3322
  [data-dsh-mobile-settings-list] [class*="_navCell"] { flex:0 0 auto !important; min-width:max-content !important; height:44px !important; padding:10px 12px !important; }
2925
3323
  [data-dsh-mobile-settings-list] [aria-current="true"] { border-color:transparent !important; outline:0 !important; box-shadow:none !important; }
2926
3324
  [data-dsh-mobile-settings-content] { flex:1 1 auto !important; width:100% !important; min-height:0 !important; }
2927
- [data-dsh-mobile-settings-header] { height:48px !important; min-height:48px !important; padding:10px 12px 6px !important; }
2928
- [data-dsh-mobile-settings-header] [class*="_close"] { width:36px !important; height:36px !important; }
2929
3325
  [data-dsh-mobile-settings-options] { box-sizing:border-box !important; width:100% !important; padding:4px 16px max(24px,env(safe-area-inset-bottom)) !important; overflow-x:hidden !important; }
2930
3326
  [data-dsh-mobile-settings-options] > * { width:100% !important; min-width:0 !important; }
2931
3327
  [data-dsh-mobile-settings-options] [data-slot="settings.general.item"] > [class*="_row"] { flex-direction:column !important; align-items:stretch !important; gap:12px !important; }
@@ -4397,6 +4793,63 @@ window.__ModuleLoader__.load({
4397
4793
  };
4398
4794
  }
4399
4795
  //#endregion
4796
+ //#region src/mobile-font-capabilities.ts
4797
+ const NO_ADVANCED_MOBILE_TYPOGRAPHY = Object.freeze({
4798
+ codeSize: false,
4799
+ textFamily: false,
4800
+ codeFamily: false
4801
+ });
4802
+ /**
4803
+ * Probe computed fonts derived from DSH's public Markdown tokens. Input values are
4804
+ * restored synchronously before painting; merely defining the input variable is not support.
4805
+ * @param document - Live page document in the current browsing context.
4806
+ * @returns Independently observed advanced typography support.
4807
+ */
4808
+ function readMobileTypographyCapabilities(document) {
4809
+ const style = document.body.style;
4810
+ const text = document.createElement("span");
4811
+ const code = document.createElement("span");
4812
+ for (const probe of [text, code]) {
4813
+ probe.setAttribute("aria-hidden", "true");
4814
+ probe.style.cssText = "position:fixed;visibility:hidden;pointer-events:none;contain:strict;width:0;height:0;overflow:hidden";
4815
+ }
4816
+ text.style.font = "var(--dsw-font-markdown-base, 14px sans-serif)";
4817
+ code.style.font = "var(--dsw-font-markdown-code-block, 11px monospace)";
4818
+ document.body.append(text, code);
4819
+ const previous = /* @__PURE__ */ new Map();
4820
+ const sample = (property, value) => {
4821
+ if (!previous.has(property)) previous.set(property, {
4822
+ value: style.getPropertyValue(property),
4823
+ priority: style.getPropertyPriority(property)
4824
+ });
4825
+ style.setProperty(property, value);
4826
+ };
4827
+ try {
4828
+ sample("--dsh-code-font-size", "23px");
4829
+ const firstSize = getComputedStyle(code).fontSize;
4830
+ sample("--dsh-code-font-size", "24px");
4831
+ const secondSize = getComputedStyle(code).fontSize;
4832
+ sample("--dsh-font-family-text", "\"dsh-mobile-text-capability-a\"");
4833
+ const firstTextFamily = getComputedStyle(text).fontFamily;
4834
+ sample("--dsh-font-family-text", "\"dsh-mobile-text-capability-b\"");
4835
+ const secondTextFamily = getComputedStyle(text).fontFamily;
4836
+ sample("--dsh-font-family-code", "\"dsh-mobile-code-capability-a\"");
4837
+ const firstCodeFamily = getComputedStyle(code).fontFamily;
4838
+ sample("--dsh-font-family-code", "\"dsh-mobile-code-capability-b\"");
4839
+ const secondCodeFamily = getComputedStyle(code).fontFamily;
4840
+ return Object.freeze({
4841
+ codeSize: firstSize === "23px" && secondSize === "24px",
4842
+ textFamily: firstTextFamily.includes("dsh-mobile-text-capability-a") && secondTextFamily.includes("dsh-mobile-text-capability-b"),
4843
+ codeFamily: firstCodeFamily.includes("dsh-mobile-code-capability-a") && secondCodeFamily.includes("dsh-mobile-code-capability-b")
4844
+ });
4845
+ } finally {
4846
+ for (const [property, value] of previous) if (value.value === "") style.removeProperty(property);
4847
+ else style.setProperty(property, value.value, value.priority);
4848
+ text.remove();
4849
+ code.remove();
4850
+ }
4851
+ }
4852
+ //#endregion
4400
4853
  //#region src/mobile-font.ts
4401
4854
  /** Device-local typography for authenticated mobile pages, without Host settings writes. */
4402
4855
  const MOBILE_FONT_STORAGE_KEY = "dsh-mobile.content-font-size.v1";
@@ -4497,6 +4950,8 @@ window.__ModuleLoader__.load({
4497
4950
  storage;
4498
4951
  typography = defaultTypography();
4499
4952
  listeners = /* @__PURE__ */ new Set();
4953
+ capabilities = NO_ADVANCED_MOBILE_TYPOGRAPHY;
4954
+ capabilityListeners = /* @__PURE__ */ new Set();
4500
4955
  constructor(storage) {
4501
4956
  this.storage = storage;
4502
4957
  let legacySize = 16;
@@ -4516,6 +4971,19 @@ window.__ModuleLoader__.load({
4516
4971
  this.listeners.delete(listener);
4517
4972
  };
4518
4973
  };
4974
+ getCapabilitiesSnapshot = () => this.capabilities;
4975
+ subscribeCapabilities = (listener) => {
4976
+ this.capabilityListeners.add(listener);
4977
+ return () => {
4978
+ this.capabilityListeners.delete(listener);
4979
+ };
4980
+ };
4981
+ /** Update observed theme support without changing stored values or Host settings. */
4982
+ updateCapabilities(capabilities) {
4983
+ if (this.capabilities.codeSize === capabilities.codeSize && this.capabilities.textFamily === capabilities.textFamily && this.capabilities.codeFamily === capabilities.codeFamily) return;
4984
+ this.capabilities = Object.freeze({ ...capabilities });
4985
+ for (const listener of this.capabilityListeners) listener();
4986
+ }
4519
4987
  /** Apply a user-selected size locally; never send it to DSH's settings RPC. */
4520
4988
  set(size) {
4521
4989
  this.setRoleSize("text", size);
@@ -4550,6 +5018,23 @@ window.__ModuleLoader__.load({
4550
5018
  if (MOBILE_FONT_ROLES.every((role) => this.typography.sizes[role] === next.sizes[role] && this.typography.families[role] === "")) return;
4551
5019
  this.persist(next);
4552
5020
  }
5021
+ /** Reset visible supported controls while retaining preferences an older theme cannot consume. */
5022
+ resetSupported() {
5023
+ const defaults = defaultTypography();
5024
+ const sizes = {
5025
+ text: defaults.sizes.text,
5026
+ code: this.capabilities.codeSize ? defaults.sizes.code : this.typography.sizes.code
5027
+ };
5028
+ const families = {
5029
+ text: this.capabilities.textFamily ? "" : this.typography.families.text,
5030
+ code: this.capabilities.codeFamily ? "" : this.typography.families.code
5031
+ };
5032
+ if (MOBILE_FONT_ROLES.every((role) => this.typography.sizes[role] === sizes[role] && this.typography.families[role] === families[role])) return;
5033
+ this.persist({
5034
+ sizes: Object.freeze(sizes),
5035
+ families: Object.freeze(families)
5036
+ });
5037
+ }
4553
5038
  persist(typography) {
4554
5039
  this.typography = Object.freeze(typography);
4555
5040
  try {
@@ -4561,7 +5046,11 @@ window.__ModuleLoader__.load({
4561
5046
  for (const listener of this.listeners) listener();
4562
5047
  }
4563
5048
  };
4564
- /** Keep mobile typography independent from Host theme changes, including body-level portals. */
5049
+ /**
5050
+ * Keep mobile typography independent from Host theme changes, including body-level portals.
5051
+ * Recheck head-owned stylesheets and body presentation attributes, not conversation DOM updates.
5052
+ * Arbitrary stylesheets inserted into body are not observed.
5053
+ */
4565
5054
  function installMobileFontPreference() {
4566
5055
  let storage = null;
4567
5056
  try {
@@ -4577,11 +5066,26 @@ window.__ModuleLoader__.load({
4577
5066
  const owned = /* @__PURE__ */ new Map();
4578
5067
  const apply = () => {
4579
5068
  const typography = preference.getTypographySnapshot();
5069
+ const capabilities = preference.getCapabilitiesSnapshot();
4580
5070
  for (const role of MOBILE_FONT_ROLES) for (const [property, value] of [[roleSpecs[role].variable, `${typography.sizes[role]}px`], [`--dsh-font-family-${role}`, typography.families[role]]]) {
4581
5071
  const current = style.getPropertyValue(property);
4582
5072
  const priority = style.getPropertyPriority(property);
4583
5073
  const last = owned.get(property);
4584
- if (last !== void 0 && (current !== last.value || priority !== last.priority)) previous.set(property, {
5074
+ if (!(property === roleSpecs[role].variable ? role === "text" || capabilities.codeSize : role === "text" ? capabilities.textFamily : capabilities.codeFamily)) {
5075
+ if (last !== void 0) {
5076
+ if (current === last.value && priority === last.priority) {
5077
+ const foreign = previous.get(property);
5078
+ if (foreign.value === "") style.removeProperty(property);
5079
+ else style.setProperty(property, foreign.value, foreign.priority);
5080
+ } else previous.set(property, {
5081
+ value: current,
5082
+ priority
5083
+ });
5084
+ owned.delete(property);
5085
+ }
5086
+ continue;
5087
+ }
5088
+ if (last === void 0 || current !== last.value || priority !== last.priority) previous.set(property, {
4585
5089
  value: current,
4586
5090
  priority
4587
5091
  });
@@ -4594,17 +5098,55 @@ window.__ModuleLoader__.load({
4594
5098
  else style.setProperty(property, value);
4595
5099
  }
4596
5100
  };
4597
- apply();
4598
- const unsubscribe = preference.subscribe(apply);
4599
- const observer = new MutationObserver(apply);
5101
+ let lastBodyState = "";
5102
+ const bodyState = () => `${style.cssText}|${document.body.className}|${document.body.getAttribute("data-ds-dark-theme")}`;
5103
+ const applyAndRemember = () => {
5104
+ apply();
5105
+ lastBodyState = bodyState();
5106
+ };
5107
+ const refresh = () => {
5108
+ preference.updateCapabilities(readMobileTypographyCapabilities(document));
5109
+ applyAndRemember();
5110
+ };
5111
+ refresh();
5112
+ const unsubscribe = preference.subscribe(applyAndRemember);
5113
+ const observer = new MutationObserver(() => {
5114
+ if (bodyState() !== lastBodyState) refresh();
5115
+ });
4600
5116
  observer.observe(document.body, {
4601
5117
  attributes: true,
4602
- attributeFilter: ["style"]
5118
+ attributeFilter: [
5119
+ "style",
5120
+ "class",
5121
+ "data-ds-dark-theme"
5122
+ ]
5123
+ });
5124
+ const themeObserver = new MutationObserver((records) => {
5125
+ const isSheet = (node) => node instanceof Element && (node.matches("style,link[rel=\"stylesheet\"]") || node.querySelector("style,link[rel=\"stylesheet\"]") !== null);
5126
+ if (records.some((record) => record.target instanceof Element && record.target.closest("style,link[rel=\"stylesheet\"]") !== null || [...record.addedNodes, ...record.removedNodes].some(isSheet))) refresh();
4603
5127
  });
5128
+ themeObserver.observe(document.head, {
5129
+ subtree: true,
5130
+ childList: true,
5131
+ characterData: true,
5132
+ attributes: true,
5133
+ attributeFilter: [
5134
+ "href",
5135
+ "media",
5136
+ "disabled",
5137
+ "rel"
5138
+ ]
5139
+ });
5140
+ const sheetLoaded = (event) => {
5141
+ if (event.target instanceof Element && event.target.matches("link[rel=\"stylesheet\"]")) refresh();
5142
+ };
5143
+ document.head.addEventListener("load", sheetLoaded, true);
4604
5144
  return {
4605
5145
  preference,
4606
5146
  dispose: () => {
4607
5147
  observer.disconnect();
5148
+ themeObserver.disconnect();
5149
+ document.head.removeEventListener("load", sheetLoaded, true);
4608
5150
  unsubscribe();
4609
5151
  for (const [property, value] of previous) {
4610
5152
  const last = owned.get(property);
@@ -4627,7 +5169,8 @@ window.__ModuleLoader__.load({
4627
5169
  familyHint: "填写设备已有的字体,多个名称用逗号分隔;留空使用默认字体。",
4628
5170
  codeSize: "代码字号",
4629
5171
  textFamily: "正文字体",
4630
- codeFamily: "代码字体"
5172
+ codeFamily: "代码字体",
5173
+ limited: "只显示当前 DSH 主题支持的字体选项;其他已保存的设置会保留。"
4631
5174
  },
4632
5175
  en: {
4633
5176
  title: "Mobile font size",
@@ -4640,7 +5183,8 @@ window.__ModuleLoader__.load({
4640
5183
  familyHint: "Use fonts already on your device, separated by commas. Leave empty for the default.",
4641
5184
  codeSize: "Code font size",
4642
5185
  textFamily: "Text font",
4643
- codeFamily: "Code font"
5186
+ codeFamily: "Code font",
5187
+ limited: "Only font options supported by the current DSH theme are shown. Other saved preferences are retained."
4644
5188
  },
4645
5189
  it: {
4646
5190
  title: "Dimensione testo mobile",
@@ -4653,7 +5197,8 @@ window.__ModuleLoader__.load({
4653
5197
  familyHint: "Usa caratteri presenti sul dispositivo, separati da virgole. Lascia vuoto per il predefinito.",
4654
5198
  codeSize: "Dimensione codice",
4655
5199
  textFamily: "Carattere testo",
4656
- codeFamily: "Carattere codice"
5200
+ codeFamily: "Carattere codice",
5201
+ limited: "Sono mostrate solo le opzioni supportate dal tema DSH corrente. Le altre preferenze salvate vengono mantenute."
4657
5202
  }
4658
5203
  };
4659
5204
  function FontFamilyInput({ preference, role, value, label, placeholder }) {
@@ -4681,6 +5226,7 @@ window.__ModuleLoader__.load({
4681
5226
  /** General settings row matching the existing Mobile controls and DSH theme tokens. */
4682
5227
  function MobileFontSizeRow({ preference, locale }) {
4683
5228
  const typography = (0, react.useSyncExternalStore)(preference.subscribe, preference.getTypographySnapshot);
5229
+ const capabilities = (0, react.useSyncExternalStore)(preference.subscribeCapabilities, preference.getCapabilitiesSnapshot);
4684
5230
  const copy = messages$1[locale];
4685
5231
  const controls = (role, title) => {
4686
5232
  const size = typography.sizes[role];
@@ -4711,18 +5257,22 @@ window.__ModuleLoader__.load({
4711
5257
  return (0, react.createElement)("div", {
4712
5258
  lang: locale,
4713
5259
  "data-mobile-font-setting": true
4714
- }, (0, react.createElement)("div", { className: "dsh-mobile-settings_row" }, (0, react.createElement)("div", { className: "dsh-mobile-settings_rowText" }, (0, react.createElement)("div", { className: "dsh-mobile-settings_title" }, copy.title), (0, react.createElement)("div", { className: "dsh-mobile-settings_desc" }, copy.description)), controls("text", copy.title)), (0, react.createElement)("details", { className: "dsh-mobile-settings_fontDetails" }, (0, react.createElement)("summary", null, copy.more), (0, react.createElement)("p", { className: "dsh-mobile-settings_desc" }, copy.familyHint), (0, react.createElement)("div", { className: "dsh-mobile-settings_row" }, (0, react.createElement)("span", { className: "dsh-mobile-settings_rowText dsh-mobile-settings_title" }, copy.codeSize), controls("code", copy.codeSize)), ...MOBILE_FONT_ROLES.map((role) => (0, react.createElement)(FontFamilyInput, {
5260
+ }, (0, react.createElement)("div", { className: "dsh-mobile-settings_row" }, (0, react.createElement)("div", { className: "dsh-mobile-settings_rowText" }, (0, react.createElement)("div", { className: "dsh-mobile-settings_title" }, copy.title), (0, react.createElement)("div", { className: "dsh-mobile-settings_desc" }, copy.description)), controls("text", copy.title)), !Object.values(capabilities).every(Boolean) && (0, react.createElement)("p", {
5261
+ className: "dsh-mobile-settings_desc",
5262
+ "data-mobile-font-limited": true
5263
+ }, copy.limited), Object.values(capabilities).some(Boolean) && (0, react.createElement)("details", { className: "dsh-mobile-settings_fontDetails" }, (0, react.createElement)("summary", null, copy.more), (capabilities.textFamily || capabilities.codeFamily) && (0, react.createElement)("p", { className: "dsh-mobile-settings_desc" }, copy.familyHint), capabilities.codeSize && (0, react.createElement)("div", { className: "dsh-mobile-settings_row" }, (0, react.createElement)("span", { className: "dsh-mobile-settings_rowText dsh-mobile-settings_title" }, copy.codeSize), controls("code", copy.codeSize)), ...MOBILE_FONT_ROLES.filter((role) => role === "text" ? capabilities.textFamily : capabilities.codeFamily).map((role) => (0, react.createElement)(FontFamilyInput, {
4715
5264
  key: role,
4716
5265
  preference,
4717
5266
  role,
4718
5267
  value: typography.families[role],
4719
5268
  label: role === "text" ? copy.textFamily : copy.codeFamily,
4720
5269
  placeholder: copy.defaultFamily
4721
- })), (0, react.createElement)("button", {
5270
+ }))), (0, react.createElement)("div", { className: "dsh-mobile-settings_row" }, (0, react.createElement)("button", {
4722
5271
  type: "button",
4723
5272
  className: "dsh-mobile-settings_selector",
5273
+ "data-mobile-font-reset": true,
4724
5274
  onClick: () => {
4725
- preference.reset();
5275
+ preference.resetSupported();
4726
5276
  }
4727
5277
  }, copy.reset)));
4728
5278
  }
@@ -4945,6 +5495,116 @@ window.__ModuleLoader__.load({
4945
5495
  }, copy.reset)));
4946
5496
  }
4947
5497
  //#endregion
5498
+ //#region src/mobile-native-settings-action.ts
5499
+ /** Own capability discovery and explicit calls; native actions retain the App's own timeout. */
5500
+ var MobileNativeSettingsActionController = class {
5501
+ action;
5502
+ getBridge;
5503
+ snapshot = Object.freeze({
5504
+ supported: false,
5505
+ state: "idle"
5506
+ });
5507
+ listeners = /* @__PURE__ */ new Set();
5508
+ bridge;
5509
+ active = false;
5510
+ generation = 0;
5511
+ probe = 0;
5512
+ attempt = 0;
5513
+ constructor(action, getBridge) {
5514
+ this.action = action;
5515
+ this.getBridge = getBridge;
5516
+ }
5517
+ getSnapshot = () => this.snapshot;
5518
+ subscribe = (listener) => {
5519
+ this.listeners.add(listener);
5520
+ return () => {
5521
+ this.listeners.delete(listener);
5522
+ };
5523
+ };
5524
+ start() {
5525
+ this.active = true;
5526
+ this.publish({
5527
+ supported: false,
5528
+ state: "idle"
5529
+ });
5530
+ this.refresh();
5531
+ }
5532
+ stop() {
5533
+ this.active = false;
5534
+ this.generation += 1;
5535
+ this.probe += 1;
5536
+ this.attempt += 1;
5537
+ this.bridge = void 0;
5538
+ }
5539
+ /** Recheck support without replaying or invalidating an in-flight call to the same adapter. */
5540
+ refresh = () => {
5541
+ if (!this.active) return;
5542
+ const bridge = this.getBridge();
5543
+ const probe = ++this.probe;
5544
+ if (bridge !== this.bridge) {
5545
+ this.bridge = bridge;
5546
+ this.generation += 1;
5547
+ this.attempt += 1;
5548
+ this.publish({
5549
+ supported: false,
5550
+ state: "idle"
5551
+ });
5552
+ }
5553
+ if (bridge === void 0) return;
5554
+ const generation = this.generation;
5555
+ const settle = (supported) => {
5556
+ if (!this.current(bridge, generation) || probe !== this.probe) return;
5557
+ if (!supported) this.attempt += 1;
5558
+ this.publish({
5559
+ supported,
5560
+ state: supported ? this.snapshot.state : "idle"
5561
+ });
5562
+ };
5563
+ Promise.resolve().then(() => {
5564
+ if (!this.current(bridge, generation) || probe !== this.probe) return void 0;
5565
+ return bridge.capabilities();
5566
+ }).then((capabilities) => {
5567
+ settle(capabilities?.includes(this.action) === true);
5568
+ }, () => {
5569
+ settle(false);
5570
+ });
5571
+ };
5572
+ /** Send at most one explicit action; late replies cannot update a replaced or unmounted row. */
5573
+ invoke = () => {
5574
+ const bridge = this.bridge;
5575
+ if (bridge === void 0 || !this.snapshot.supported || !this.current(bridge, this.generation) || this.snapshot.state === "busy" || this.action === "mobile.switch-computer" && this.snapshot.state === "success") return;
5576
+ const generation = this.generation;
5577
+ const attempt = ++this.attempt;
5578
+ this.publish({
5579
+ supported: true,
5580
+ state: "busy"
5581
+ });
5582
+ const current = () => this.current(bridge, generation) && attempt === this.attempt && this.snapshot.supported;
5583
+ Promise.resolve().then(() => {
5584
+ if (!current()) return void 0;
5585
+ return bridge.invoke(this.action, {});
5586
+ }).then(() => {
5587
+ if (current()) this.publish({
5588
+ supported: true,
5589
+ state: "success"
5590
+ });
5591
+ }, () => {
5592
+ if (current()) this.publish({
5593
+ supported: true,
5594
+ state: "error"
5595
+ });
5596
+ });
5597
+ };
5598
+ current(bridge, generation) {
5599
+ return this.active && generation === this.generation && this.bridge === bridge && this.getBridge() === bridge;
5600
+ }
5601
+ publish(snapshot) {
5602
+ if (snapshot.supported === this.snapshot.supported && snapshot.state === this.snapshot.state) return;
5603
+ this.snapshot = Object.freeze(snapshot);
5604
+ for (const listener of this.listeners) listener();
5605
+ }
5606
+ };
5607
+ //#endregion
4948
5608
  //#region src/local-admin-host.ts
4949
5609
  /** Browser-safe desktop-admin Host checks. Do not import Node APIs here. */
4950
5610
  const DESKTOP_ADMIN_HEADER = "x-dsh-mobile-desktop-admin";
@@ -5005,65 +5665,282 @@ window.__ModuleLoader__.load({
5005
5665
  return (protocol === "dsh-app:" ? hostname === "app" : (protocol === "" || protocol === "http:" || protocol === "https:") && isLocalAdminHostname(hostname)) && frontend !== "dedicated" && !new URLSearchParams(query).has("dsh-mobile-preview");
5006
5666
  }
5007
5667
  //#endregion
5668
+ //#region src/client-module-presentation.ts
5669
+ /** Group boot-protected entries before considering their package namespace.
5670
+ * @param entry - Catalog entry with server-owned required and dependency fields.
5671
+ * @returns Display group, never an authorization or compatibility decision.
5672
+ */
5673
+ function clientModuleGroup(entry) {
5674
+ if (entry.required) return "required";
5675
+ return entry.id.startsWith("@deepseek-ai/") ? "advanced" : "extensions";
5676
+ }
5677
+ /** Format the package leaf mechanically; the catalog has no product titles.
5678
+ * @param id - Exact module ID from the server catalog.
5679
+ * @returns Unscoped package words without guessed function or plugin ownership.
5680
+ */
5681
+ function clientModuleDisplayName(id) {
5682
+ const leaf = id.startsWith("@") && id.includes("/") ? id.slice(id.indexOf("/") + 1) : id;
5683
+ return (leaf.replace(/^dsh-(?:client-(?:ui-)?)?/u, "") || leaf || id).replace(/[-_]+/gu, " ");
5684
+ }
5685
+ function matchesQuery(id, query) {
5686
+ const needle = query.trim().toLowerCase();
5687
+ return needle === "" || `${id}\n${clientModuleDisplayName(id)}`.toLowerCase().includes(needle);
5688
+ }
5689
+ /** Match the visible title or exact ID and the current draft loading choice.
5690
+ * @param entry - Catalog entry to consider.
5691
+ * @param excluded - Current draft exclusions, not a runtime loading observation.
5692
+ * @param query - User-entered search text.
5693
+ * @param filter - All, selected for loading, or selected for omission.
5694
+ * @returns Whether the row belongs in the current filtered list.
5695
+ */
5696
+ function clientModuleMatches(entry, excluded, query, filter) {
5697
+ const omitted = !entry.required && excluded.has(entry.id);
5698
+ return matchesQuery(entry.id, query) && (filter === "all" || (filter === "excluded" ? omitted : !omitted));
5699
+ }
5700
+ /** Editable selections retain unknown server exclusions and never cascade dependencies. */
5701
+ var ClientModuleDraft = class {
5702
+ entries;
5703
+ initial;
5704
+ excluded;
5705
+ entriesById;
5706
+ constructor(entries, excludedIds) {
5707
+ this.entries = entries;
5708
+ this.initial = new Set(excludedIds);
5709
+ this.excluded = new Set(excludedIds);
5710
+ this.entriesById = new Map(entries.map((entry) => [entry.id, entry]));
5711
+ }
5712
+ /** @returns Number of changed loading choices, including hidden filtered rows. */
5713
+ get changeCount() {
5714
+ let count = 0;
5715
+ for (const id of this.initial) if (!this.excluded.has(id)) count += 1;
5716
+ for (const id of this.excluded) if (!this.initial.has(id)) count += 1;
5717
+ return count;
5718
+ }
5719
+ /** @returns A snapshot of exclusions suitable for authenticated saving. */
5720
+ get excludedIds() {
5721
+ return [...this.excluded];
5722
+ }
5723
+ /** @returns Retained exclusions that cannot be saved against the current catalog. */
5724
+ get invalidExclusions() {
5725
+ return [...this.excluded].flatMap((id) => {
5726
+ const entry = this.entriesById.get(id);
5727
+ return entry === void 0 ? [{
5728
+ id,
5729
+ reason: "missing"
5730
+ }] : entry.required ? [{
5731
+ id,
5732
+ reason: "required"
5733
+ }] : [];
5734
+ });
5735
+ }
5736
+ /** Explicitly remove an old exclusion; ordinary optional choices use setLoaded.
5737
+ * @param id - Exact ID of a retained missing or now-required exclusion.
5738
+ * @returns Whether this draft changed; no server request is made.
5739
+ */
5740
+ clearInvalidExclusion(id) {
5741
+ const entry = this.entriesById.get(id);
5742
+ if (!this.initial.has(id) || entry !== void 0 && !entry.required) return false;
5743
+ return this.excluded.delete(id);
5744
+ }
5745
+ /** @returns Whether a catalog entry is selected for loading, not already loaded. */
5746
+ isLoaded(id) {
5747
+ return this.entriesById.get(id)?.required === true || !this.excluded.has(id);
5748
+ }
5749
+ /** Change one optional choice; required and unknown entries cannot be edited.
5750
+ * @param id - Exact catalog ID.
5751
+ * @param loaded - Whether to load the entry on the next open.
5752
+ */
5753
+ setLoaded(id, loaded) {
5754
+ const entry = this.entriesById.get(id);
5755
+ if (entry === void 0 || entry.required) return;
5756
+ if (loaded) this.excluded.delete(id);
5757
+ else this.excluded.add(id);
5758
+ }
5759
+ /** Filter a draft without changing hidden choices.
5760
+ * @param query - Search input.
5761
+ * @param filter - Requested loading selection.
5762
+ * @returns Catalog entries in their original stable order.
5763
+ */
5764
+ matchingEntries(query, filter) {
5765
+ return this.entries.filter((entry) => clientModuleMatches(entry, this.excluded, query, filter));
5766
+ }
5767
+ /** Find invalid old exclusions by name or ID, independently from the protected catalog row.
5768
+ * @param query - Search input.
5769
+ * @param filter - Old exclusions appear in all and excluded selections, not loaded.
5770
+ * @returns Invalid draft exclusions in their retained order.
5771
+ */
5772
+ matchingInvalidExclusions(query, filter) {
5773
+ return filter === "loaded" ? [] : this.invalidExclusions.filter((entry) => matchesQuery(entry.id, query));
5774
+ }
5775
+ };
5776
+ //#endregion
5008
5777
  //#region src/client-module-ui.ts
5009
5778
  /** Device-local module selection and the computer's defaults share a read-only catalog. */
5010
5779
  const CLIENT_MODULE_COPY = {
5011
5780
  zh: {
5012
5781
  title: "移动页面模块",
5013
- computer: "为移动访问设置默认加载模块,不卸载电脑上的插件。",
5014
- device: "只为本设备选择加载模块,不改变其他手机或电脑。",
5782
+ computer: "设置此电脑的移动访问默认项;电脑插件不变。",
5783
+ device: "仅本设备,下次打开生效;电脑插件不变。",
5015
5784
  open: "管理",
5785
+ help: "加载规则",
5016
5786
  loading: "正在读取模块…",
5017
- hint: "勾选表示加载。启动必需的模块不能取消;依赖冲突会在保存时说明。",
5018
- save: "保存,下次打开生效",
5787
+ hint: "勾选表示下次打开时加载。启动必需项受保护;依赖冲突会在保存时说明,不会自动取消其他模块。",
5788
+ save: "保存选择",
5019
5789
  reset: "恢复默认",
5020
5790
  close: "关闭",
5021
5791
  required: "启动必需",
5022
5792
  dependencies: "依赖:",
5023
- saved: "已保存,不会自动刷新当前页面。重新打开 DSH 后生效。",
5024
- failed: "暂时无法完成。请确认移动访问已启用,或检查下面的依赖冲突。",
5793
+ noDependencies: "无声明的前端依赖",
5794
+ details: "模块详情",
5795
+ moduleId: "模块 ID",
5796
+ saved: "已保存,下次打开 DSH 生效。不会自动刷新当前页面或丢弃草稿。",
5797
+ failed: "暂时无法读取模块。请确认移动访问已启用后重试。",
5798
+ saveFailed: "未确认保存成功,已保留你的选择。可重新保存,或重新读取已保存设置。",
5025
5799
  conflict: "无法保存:",
5026
- computerSource: "使用电脑默认设置",
5027
- deviceSource: "使用本设备设置",
5028
- resetConfirm: "恢复默认模块选择?不会重新加载当前会话。"
5800
+ computerSource: "当前来源:电脑默认设置",
5801
+ deviceSource: "当前来源:本设备设置",
5802
+ pluginSource: "当前来源:插件默认设置",
5803
+ resetConfirm: "恢复默认模块选择?仅下次打开生效,不会重新加载当前会话。",
5804
+ discardConfirm: "放弃未保存的模块选择并关闭?",
5805
+ reloadConfirm: "重新读取会替换未保存的选择。继续吗?",
5806
+ savingCloseConfirm: "保存结果尚未确认。现在关闭不会撤销可能已完成的保存,确定关闭吗?",
5807
+ search: "搜索模块",
5808
+ searchPlaceholder: "搜索名称或模块 ID",
5809
+ filter: "加载选择",
5810
+ all: "全部",
5811
+ loaded: "加载",
5812
+ excluded: "不加载",
5813
+ empty: "没有符合条件的模块。",
5814
+ clearFilters: "清除筛选",
5815
+ extensions: "可选扩展",
5816
+ extensionsHint: "可选择不加载的前端模块;一项模块不一定等于一个完整插件。",
5817
+ advanced: "DSH 可选组件(高级)",
5818
+ advancedHint: "DSH 的可选前端组件。只修改你了解的项目。",
5819
+ requiredGroup: "启动必需项",
5820
+ requiredHint: "这些模块保证页面启动,不能关闭。",
5821
+ changes: "未保存:{count} 项改动",
5822
+ unchanged: "没有未保存的改动",
5823
+ results: "匹配 {visible} / {total} 项",
5824
+ retry: "重新读取",
5825
+ saving: "正在保存…",
5826
+ readTimeout: "读取超时。当前页面和已保存设置未变,请重试。",
5827
+ writeTimeout: "保存超时,结果尚未确认。保留了你的选择;请重新读取确认,不会自动重试保存。",
5828
+ nextOpen: "仅下次打开生效",
5829
+ invalidSelections: "失效的旧选择",
5830
+ invalidHint: "这些旧排除已与当前模块目录不符。移除后仍需保存选择。",
5831
+ invalidMissing: "当前模块目录中不存在。",
5832
+ invalidRequired: "当前已是启动必需项,不能排除。",
5833
+ clearInvalid: "移除旧排除"
5029
5834
  },
5030
5835
  en: {
5031
5836
  title: "Mobile page modules",
5032
- computer: "Choose default modules for mobile access without uninstalling computer plugins.",
5033
- device: "Choose modules for this device only; other phones and computers are unchanged.",
5837
+ computer: "Mobile defaults for this computer. Desktop plugins are unchanged.",
5838
+ device: "For this device’s next open only. Computer plugins are unchanged.",
5034
5839
  open: "Manage",
5840
+ help: "Loading rules",
5035
5841
  loading: "Reading modules…",
5036
- hint: "Checked means loaded. Required boot modules cannot be unchecked; saving explains dependency conflicts.",
5037
- save: "Save for next open",
5842
+ hint: "Checked means load on the next open. Required entries are protected; saving explains dependency conflicts without unchecking other modules.",
5843
+ save: "Save selection",
5038
5844
  reset: "Restore defaults",
5039
5845
  close: "Close",
5040
5846
  required: "Required for boot",
5041
5847
  dependencies: "Depends on: ",
5042
- saved: "Saved. This page is not reloaded automatically. Reopen DSH to apply.",
5043
- failed: "Could not complete this operation. Enable mobile access or check the dependency conflict below.",
5848
+ noDependencies: "No declared frontend dependencies",
5849
+ details: "Module details",
5850
+ moduleId: "Module ID",
5851
+ saved: "Saved for the next DSH open. This page is not refreshed and its draft is kept.",
5852
+ failed: "Could not read modules. Enable mobile access and try again.",
5853
+ saveFailed: "Saving was not confirmed. Your choices are kept. Save again or reread the saved settings.",
5044
5854
  conflict: "Could not save: ",
5045
- computerSource: "Using computer defaults",
5046
- deviceSource: "Using this device’s settings",
5047
- resetConfirm: "Restore default module selection? The current conversation will not reload."
5855
+ computerSource: "Source: computer defaults",
5856
+ deviceSource: "Source: this device’s settings",
5857
+ pluginSource: "Source: plugin defaults",
5858
+ resetConfirm: "Restore default selections for the next open? The current conversation will not reload.",
5859
+ discardConfirm: "Discard unsaved module choices and close?",
5860
+ reloadConfirm: "Rereading replaces unsaved choices. Continue?",
5861
+ savingCloseConfirm: "Saving is not confirmed. Closing will not undo a save that may have completed. Close anyway?",
5862
+ search: "Search modules",
5863
+ searchPlaceholder: "Search name or module ID",
5864
+ filter: "Loading selection",
5865
+ all: "All",
5866
+ loaded: "Load",
5867
+ excluded: "Do not load",
5868
+ empty: "No modules match these filters.",
5869
+ clearFilters: "Clear filters",
5870
+ extensions: "Optional extensions",
5871
+ extensionsHint: "Optional frontend modules; one module is not necessarily a complete plugin.",
5872
+ advanced: "DSH optional components (advanced)",
5873
+ advancedHint: "Optional DSH frontend components. Change only entries you understand.",
5874
+ requiredGroup: "Required for boot",
5875
+ requiredHint: "These modules keep the page bootable and cannot be disabled.",
5876
+ changes: "Unsaved: {count} changes",
5877
+ unchanged: "No unsaved changes",
5878
+ results: "Matching {visible} / {total}",
5879
+ retry: "Reread settings",
5880
+ saving: "Saving…",
5881
+ readTimeout: "Reading timed out. This page and saved settings are unchanged; try again.",
5882
+ writeTimeout: "Saving timed out and its result is unconfirmed. Your choices are kept. Reread to confirm; saving is not retried automatically.",
5883
+ nextOpen: "Applies on next open only",
5884
+ invalidSelections: "Outdated selections",
5885
+ invalidHint: "These old exclusions no longer match the module catalog. Save your choices after removing them.",
5886
+ invalidMissing: "Not present in the current module catalog.",
5887
+ invalidRequired: "Now required for boot and cannot be excluded.",
5888
+ clearInvalid: "Remove old exclusion"
5048
5889
  },
5049
5890
  it: {
5050
5891
  title: "Moduli della pagina mobile",
5051
- computer: "Scegli i moduli predefiniti senza disinstallare i plugin del computer.",
5052
- device: "Scegli i moduli solo per questo dispositivo, senza cambiare gli altri.",
5892
+ computer: "Predefiniti mobile del computer; i plugin non cambiano.",
5893
+ device: "Solo alla prossima apertura su questo dispositivo; i plugin del computer non cambiano.",
5053
5894
  open: "Gestisci",
5895
+ help: "Regole di caricamento",
5054
5896
  loading: "Lettura dei moduli…",
5055
- hint: "Selezionato significa caricato. I moduli obbligatori non possono essere rimossi; il salvataggio mostra i conflitti.",
5056
- save: "Salva per la prossima apertura",
5897
+ hint: "Selezionato significa caricare alla prossima apertura. I moduli obbligatori sono protetti; il salvataggio mostra i conflitti senza deselezionare altri moduli.",
5898
+ save: "Salva selezione",
5057
5899
  reset: "Ripristina predefiniti",
5058
5900
  close: "Chiudi",
5059
5901
  required: "Obbligatorio",
5060
5902
  dependencies: "Dipende da: ",
5061
- saved: "Salvato. La pagina non viene ricaricata automaticamente. Riapri DSH per applicare.",
5062
- failed: "Operazione non completata. Abilita accesso mobile o controlla il conflitto sotto.",
5903
+ noDependencies: "Nessuna dipendenza frontend dichiarata",
5904
+ details: "Dettagli modulo",
5905
+ moduleId: "ID modulo",
5906
+ saved: "Salvato per la prossima apertura di DSH. La pagina non si aggiorna e la bozza resta.",
5907
+ failed: "Impossibile leggere i moduli. Abilita accesso mobile e riprova.",
5908
+ saveFailed: "Salvataggio non confermato. Le scelte restano; salva di nuovo o rileggi le impostazioni salvate.",
5063
5909
  conflict: "Impossibile salvare: ",
5064
- computerSource: "Predefiniti del computer",
5065
- deviceSource: "Impostazioni del dispositivo",
5066
- resetConfirm: "Ripristinare i moduli predefiniti? La conversazione attuale non verrà ricaricata."
5910
+ computerSource: "Origine: predefiniti del computer",
5911
+ deviceSource: "Origine: impostazioni del dispositivo",
5912
+ pluginSource: "Origine: predefiniti del plugin",
5913
+ resetConfirm: "Ripristinare i predefiniti per la prossima apertura? La conversazione attuale non verrà ricaricata.",
5914
+ discardConfirm: "Scartare le scelte non salvate e chiudere?",
5915
+ reloadConfirm: "Rileggere sostituisce le scelte non salvate. Continuare?",
5916
+ savingCloseConfirm: "Salvataggio non confermato. Chiudere non annulla un salvataggio già completato. Chiudere comunque?",
5917
+ search: "Cerca moduli",
5918
+ searchPlaceholder: "Cerca nome o ID modulo",
5919
+ filter: "Selezione caricamento",
5920
+ all: "Tutti",
5921
+ loaded: "Carica",
5922
+ excluded: "Non caricare",
5923
+ empty: "Nessun modulo corrisponde ai filtri.",
5924
+ clearFilters: "Cancella filtri",
5925
+ extensions: "Estensioni facoltative",
5926
+ extensionsHint: "Moduli frontend facoltativi; un modulo non è necessariamente un plugin completo.",
5927
+ advanced: "Componenti DSH facoltativi (avanzate)",
5928
+ advancedHint: "Componenti frontend DSH facoltativi. Modifica solo quelli che conosci.",
5929
+ requiredGroup: "Moduli obbligatori",
5930
+ requiredHint: "Questi moduli permettono l’avvio e non possono essere disattivati.",
5931
+ changes: "Non salvate: {count} modifiche",
5932
+ unchanged: "Nessuna modifica non salvata",
5933
+ results: "Corrispondenti {visible} / {total}",
5934
+ retry: "Rileggi impostazioni",
5935
+ saving: "Salvataggio…",
5936
+ readTimeout: "Lettura scaduta. Pagina e impostazioni salvate non cambiano; riprova.",
5937
+ writeTimeout: "Salvataggio scaduto e non confermato. Le scelte restano. Rileggi per confermare; nessun tentativo automatico.",
5938
+ nextOpen: "Solo alla prossima apertura",
5939
+ invalidSelections: "Scelte obsolete",
5940
+ invalidHint: "Queste esclusioni precedenti non corrispondono più al catalogo. Salva le scelte dopo averle rimosse.",
5941
+ invalidMissing: "Assente dal catalogo attuale dei moduli.",
5942
+ invalidRequired: "Ora obbligatorio per l’avvio e non può essere escluso.",
5943
+ clearInvalid: "Rimuovi esclusione precedente"
5067
5944
  }
5068
5945
  };
5069
5946
  var ClientModuleUiConflict = class extends Error {
@@ -5073,29 +5950,40 @@ window.__ModuleLoader__.load({
5073
5950
  this.detail = detail;
5074
5951
  }
5075
5952
  };
5953
+ /** A deadline does not imply that a POST was rolled back by the server. */
5954
+ var ClientModuleRequestTimeout = class extends Error {
5955
+ mutation;
5956
+ constructor(mutation) {
5957
+ super("client_module_request_timeout");
5958
+ this.mutation = mutation;
5959
+ }
5960
+ };
5961
+ const CLIENT_MODULE_REQUEST_TIMEOUT_MS = {
5962
+ read: 15e3,
5963
+ write: 2e4
5964
+ };
5076
5965
  /** Validate catalog response fields before creating interactive controls.
5077
5966
  * @param value - Gateway or computer-admin JSON response.
5078
5967
  * @returns The fields used by the module-selection dialog.
5079
5968
  */
5080
5969
  function parseClientModuleView(value) {
5081
- if (typeof value !== "object" || value === null || !("entries" in value) || !Array.isArray(value.entries) || !("excludedClientModules" in value) || !Array.isArray(value.excludedClientModules) || !("source" in value) || ![
5082
- "device",
5083
- "computer",
5084
- "plugin"
5085
- ].includes(String(value.source))) throw new Error("invalid_module_catalog");
5970
+ if (typeof value !== "object" || value === null || !("entries" in value) || !Array.isArray(value.entries) || !("excludedClientModules" in value) || !Array.isArray(value.excludedClientModules) || !("source" in value) || value.source !== "device" && value.source !== "computer" && value.source !== "plugin") throw new Error("invalid_module_catalog");
5971
+ const entries = value.entries.map((entry) => {
5972
+ if (typeof entry !== "object" || entry === null || !("id" in entry) || typeof entry.id !== "string" || !("required" in entry) || typeof entry.required !== "boolean" || !("dependencies" in entry) || !Array.isArray(entry.dependencies)) throw new Error("invalid_module_catalog");
5973
+ const dependencies = entry.dependencies.map((dependency) => {
5974
+ if (typeof dependency !== "string") throw new Error("invalid_module_catalog");
5975
+ return dependency;
5976
+ });
5977
+ if (entry.id.trim() === "") throw new Error("invalid_module_catalog");
5978
+ return {
5979
+ id: entry.id,
5980
+ required: entry.required,
5981
+ dependencies
5982
+ };
5983
+ });
5984
+ if (new Set(entries.map((entry) => entry.id)).size !== entries.length) throw new Error("invalid_module_catalog");
5086
5985
  return {
5087
- entries: value.entries.map((entry) => {
5088
- if (typeof entry !== "object" || entry === null || !("id" in entry) || typeof entry.id !== "string" || !("required" in entry) || typeof entry.required !== "boolean" || !("dependencies" in entry) || !Array.isArray(entry.dependencies)) throw new Error("invalid_module_catalog");
5089
- const dependencies = entry.dependencies.map((dependency) => {
5090
- if (typeof dependency !== "string") throw new Error("invalid_module_catalog");
5091
- return dependency;
5092
- });
5093
- return {
5094
- id: entry.id,
5095
- required: entry.required,
5096
- dependencies
5097
- };
5098
- }),
5986
+ entries,
5099
5987
  excludedClientModules: value.excludedClientModules.map((id) => {
5100
5988
  if (typeof id !== "string") throw new Error("invalid_module_catalog");
5101
5989
  return id;
@@ -5103,32 +5991,103 @@ window.__ModuleLoader__.load({
5103
5991
  source: value.source === "device" ? "device" : value.source === "computer" ? "computer" : "plugin"
5104
5992
  };
5105
5993
  }
5994
+ /** Read or explicitly write module choices with a bounded, cancellable operation.
5995
+ * @param scope - Computer-admin endpoint or authenticated device endpoint.
5996
+ * @param signal - Owning dialog lifetime; cancellation cannot undo a completed write.
5997
+ * @param body - Omit for a read; supplied fields are sent once without automatic replay.
5998
+ * @returns Validated catalog fields returned by the selected endpoint.
5999
+ */
6000
+ async function requestClientModuleView(scope, signal, body) {
6001
+ if (signal.aborted) throw new DOMException("Dialog closed", "AbortError");
6002
+ const controller = new AbortController();
6003
+ let rejectCancellation = () => {};
6004
+ const cancellation = new Promise((_, reject) => {
6005
+ rejectCancellation = reject;
6006
+ });
6007
+ const abort = () => {
6008
+ controller.abort();
6009
+ rejectCancellation(new DOMException("Dialog closed", "AbortError"));
6010
+ };
6011
+ signal.addEventListener("abort", abort, { once: true });
6012
+ const timer = setTimeout(() => {
6013
+ rejectCancellation(new ClientModuleRequestTimeout(body !== void 0));
6014
+ controller.abort();
6015
+ }, body === void 0 ? CLIENT_MODULE_REQUEST_TIMEOUT_MS.read : CLIENT_MODULE_REQUEST_TIMEOUT_MS.write);
6016
+ const operation = async () => {
6017
+ const init = {
6018
+ method: body === void 0 ? "GET" : "POST",
6019
+ signal: controller.signal,
6020
+ ...body === void 0 ? {} : { body: JSON.stringify(body) }
6021
+ };
6022
+ init.headers = scope === "computer" ? localAdminRequestHeaders(init, location) : { "content-type": "application/json" };
6023
+ if (scope === "device" && body !== void 0) {
6024
+ const cookies = document.cookie.split(";").map((value) => value.trim());
6025
+ const csrfCookie = cookies.find((value) => value.startsWith("__Host-dsh_ma_csrf=")) ?? cookies.find((value) => value.startsWith("dsh_ma_csrf="));
6026
+ if (csrfCookie !== void 0) {
6027
+ const headers = new Headers(init.headers);
6028
+ headers.set("x-dsh-mobile-csrf", csrfCookie.slice(csrfCookie.indexOf("=") + 1));
6029
+ init.headers = headers;
6030
+ }
6031
+ }
6032
+ const response = await fetch(scope === "computer" ? "/api/mobile-access/client-modules" : "/mobile-access/client-modules", init);
6033
+ const value = await response.json();
6034
+ if (!response.ok) {
6035
+ const detail = typeof value === "object" && value !== null && "detail" in value && typeof value.detail === "string" ? value.detail.slice(0, 512) : "";
6036
+ if (response.status === 409 && detail !== "" && typeof value === "object" && value !== null && "error" in value && value.error === "excluded_client_modules_invalid") throw new ClientModuleUiConflict(detail);
6037
+ throw new Error("client_module_request_failed");
6038
+ }
6039
+ return parseClientModuleView(value);
6040
+ };
6041
+ try {
6042
+ return await Promise.race([operation(), cancellation]);
6043
+ } finally {
6044
+ clearTimeout(timer);
6045
+ signal.removeEventListener("abort", abort);
6046
+ }
6047
+ }
5106
6048
  const CLIENT_MODULE_STYLES = `
5107
6049
  .dsh-module-row{display:flex;align-items:center;gap:8px;padding:16px 0;border-bottom:.5px solid var(--dsw-alias-border-l2)}
5108
6050
  .dsh-module-rowText{flex:1;min-width:0}.dsh-module-title{font-size:14px;line-height:22px;color:var(--dsw-alias-label-primary)}
5109
6051
  .dsh-module-description{margin-top:4px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}
5110
6052
  .dsh-module-button{min-height:48px;padding:0 14px;border:0;border-radius:24px;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-primary);font:inherit;font-size:14px;cursor:pointer}
5111
6053
  .dsh-module-button:focus-visible{outline:2px solid var(--dsw-alias-label-primary-bluish,#2563eb);outline-offset:2px}
5112
- .dsh-module-dialog{box-sizing:border-box;width:min(640px,calc(100vw - 24px));max-height:calc(100dvh - 32px);padding:20px;border:1px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-panel,20px);background:var(--dsw-alias-bg-layer-1,#fff);color:var(--dsw-alias-label-primary,#171a21);font:16px/1.5 var(--dsw-font-family,sans-serif);overflow:auto}
5113
- .dsh-module-dialog::backdrop{background:rgb(15 23 42 / 40%)}.dsh-module-dialog h2{font-size:20px;margin:0 0 12px}
5114
- .dsh-module-dialog p{overflow-wrap:anywhere}.dsh-module-list{display:flex;flex-direction:column;gap:4px;margin:12px 0}
5115
- .dsh-module-choice{display:flex;align-items:flex-start;gap:12px;min-height:48px;padding:8px 0;border-bottom:1px solid var(--dsw-alias-border-l2)}
5116
- .dsh-module-choice input{width:24px;height:24px;flex:none;margin:2px 0}.dsh-module-choice span{min-width:0;overflow-wrap:anywhere}
5117
- .dsh-module-choice small{display:block;font-size:12px;color:var(--dsw-alias-label-tertiary);line-height:18px}.dsh-module-actions{display:flex;flex-wrap:wrap;gap:8px}
6054
+ .dsh-module-button:not(:disabled):active{background:var(--dsw-alias-interactive-bg-active)}
6055
+ .dsh-module-dialog{box-sizing:border-box;width:min(640px,calc(100vw - 24px));max-height:calc(100dvh - 32px);padding:0;border:1px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-panel,20px);background:var(--dsw-alias-bg-layer-1,#fff);color:var(--dsw-alias-label-primary,#171a21);font:16px/1.5 var(--dsw-font-family,sans-serif);overflow:hidden}
6056
+ .dsh-module-dialog[open]{display:flex;flex-direction:column}.dsh-module-dialog::backdrop{background:rgb(15 23 42 / 40%)}
6057
+ .dsh-module-header{display:flex;align-items:center;gap:12px;padding:12px 16px;border-bottom:1px solid var(--dsw-alias-border-l2);flex:none}
6058
+ .dsh-module-header h2{flex:1;min-width:0;font-size:20px;line-height:1.4;margin:0;overflow-wrap:anywhere}.dsh-module-header .dsh-module-button{flex:none}
6059
+ .dsh-module-body{min-height:0;overflow:auto;overscroll-behavior:contain;padding:16px;scroll-padding:12px}.dsh-module-dialog p{margin:0 0 12px;overflow-wrap:anywhere}
6060
+ .dsh-module-hint,.dsh-module-source,.dsh-module-choice small,.dsh-module-groupHint,.dsh-module-results{font-size:13px;line-height:20px;color:var(--dsw-alias-label-secondary,var(--dsw-alias-label-tertiary))}
6061
+ .dsh-module-tools{display:grid;grid-template-columns:minmax(0,1fr) minmax(100px,.45fr);gap:12px;margin:16px 0 8px}.dsh-module-field{display:flex;flex-direction:column;gap:4px;min-width:0;font-size:13px}
6062
+ .dsh-module-help>summary{box-sizing:border-box;min-height:48px;padding:12px 0;cursor:pointer;font-size:13px;color:var(--dsw-alias-label-secondary)}
6063
+ .dsh-module-field input,.dsh-module-field select{box-sizing:border-box;width:100%;min-width:0;min-height:48px;padding:8px 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-bg-base,var(--dsw-alias-bg-layer-1));color:var(--dsw-alias-label-primary);font:inherit;font-size:16px}
6064
+ .dsh-module-dialog :is(input,select,summary):focus-visible{outline:2px solid var(--dsw-alias-label-primary-bluish,#2563eb);outline-offset:2px}
6065
+ .dsh-module-list{display:flex;flex-direction:column;gap:16px;margin:12px 0}.dsh-module-group h3{margin:0 0 8px;font-size:16px;line-height:24px}
6066
+ .dsh-module-group>summary{min-height:48px;cursor:pointer;font-weight:600;overflow-wrap:anywhere;align-content:center}.dsh-module-group>summary+div{margin-top:8px}
6067
+ .dsh-module-entry{border-bottom:1px solid var(--dsw-alias-border-l2);padding:8px 0}.dsh-module-choice{display:flex;align-items:center;gap:12px;min-height:48px;cursor:pointer}
6068
+ .dsh-module-choice input{width:24px;height:24px;flex:none;margin:0}.dsh-module-choice span{min-width:0;overflow-wrap:anywhere}.dsh-module-choiceName{display:block;font-weight:500}
6069
+ .dsh-module-choice small{display:block}.dsh-module-entryDetails{margin-left:36px;min-width:0}.dsh-module-entryDetails summary{min-height:48px;align-content:center;cursor:pointer;font-size:13px;color:var(--dsw-alias-label-secondary,var(--dsw-alias-label-tertiary))}
6070
+ .dsh-module-invalid-entry code{display:block;min-width:0;overflow-wrap:anywhere;white-space:normal;font-size:13px}.dsh-module-invalid-entry .dsh-module-button{max-width:100%;white-space:normal;line-height:20px;padding:8px 14px}
6071
+ .dsh-module-entryDetails p,.dsh-module-entryDetails code{overflow-wrap:anywhere;font-size:13px}.dsh-module-entryDetails code{white-space:normal}
6072
+ .dsh-module-footer{flex:none;padding:12px 16px max(12px,env(safe-area-inset-bottom));border-top:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1,#fff)}
6073
+ .dsh-module-footer p{font-size:13px;line-height:20px;margin:0 0 8px}.dsh-module-actions{display:flex;flex-wrap:wrap;gap:8px}.dsh-module-actions .dsh-module-button{flex:1;min-width:0;white-space:normal;line-height:20px;padding:8px 12px}
6074
+ .dsh-module-button:disabled{opacity:.5;cursor:default}.dsh-module-save{background:var(--dsw-alias-bg-module-platform);font-weight:600}
6075
+ .dsh-module-notice:empty{display:none}.dsh-module-notice{max-height:min(96px,24dvh);overflow:auto;font-size:14px;line-height:22px;overscroll-behavior:contain}.dsh-module-empty{padding:16px 0}
6076
+ @media(max-width:420px){.dsh-module-tools{grid-template-columns:minmax(0,1fr)}.dsh-module-body{padding:12px}.dsh-module-header,.dsh-module-footer{padding-left:12px;padding-right:12px}}
5118
6077
  .dsh-module-error{color:var(--dsw-alias-state-error-primary,#b42318)}
5119
6078
  `;
5120
6079
  const openDialogs = /* @__PURE__ */ new Set();
5121
6080
  /** Cancel pending dialogs when the owning plugin generation is disposed. */
5122
6081
  function disposeClientModuleDialogs() {
5123
- for (const dispose of [...openDialogs]) dispose();
6082
+ for (const dialog of [...openDialogs]) dialog.dispose();
5124
6083
  }
5125
6084
  /** Close the foreground module modal before navigating its underlying DSH settings.
5126
6085
  * @returns Whether an open module modal consumed the return action.
5127
6086
  */
5128
6087
  function closeClientModuleDialog() {
5129
- const dispose = [...openDialogs].at(-1);
5130
- if (dispose === void 0) return false;
5131
- dispose();
6088
+ const dialog = [...openDialogs].at(-1);
6089
+ if (dialog === void 0) return false;
6090
+ dialog.requestClose();
5132
6091
  return true;
5133
6092
  }
5134
6093
  /** Open one modal; closing cancels reads and restores focus without reloading the page.
@@ -5138,143 +6097,338 @@ window.__ModuleLoader__.load({
5138
6097
  */
5139
6098
  function openClientModuleSettings(scope, locale, trigger) {
5140
6099
  const copy = CLIENT_MODULE_COPY[locale];
5141
- const endpoint = scope === "computer" ? "/api/mobile-access/client-modules" : "/mobile-access/client-modules";
5142
6100
  const controller = new AbortController();
5143
6101
  const dialog = document.createElement("dialog");
5144
6102
  dialog.className = "dsh-module-dialog";
5145
6103
  dialog.setAttribute("aria-label", copy.title);
6104
+ dialog.lang = locale;
6105
+ const button = (text) => {
6106
+ const result = document.createElement("button");
6107
+ result.type = "button";
6108
+ result.className = "dsh-module-button";
6109
+ result.textContent = text;
6110
+ return result;
6111
+ };
6112
+ const paragraph = (text, className = "") => {
6113
+ const result = document.createElement("p");
6114
+ result.textContent = text;
6115
+ result.className = className;
6116
+ return result;
6117
+ };
6118
+ const header = document.createElement("div");
6119
+ header.className = "dsh-module-header";
5146
6120
  const title = document.createElement("h2");
5147
6121
  title.textContent = copy.title;
5148
- const intro = document.createElement("p");
5149
- intro.textContent = `${copy[scope]} ${copy.hint}`;
5150
- const status = document.createElement("p");
6122
+ const close = button(copy.close);
6123
+ header.append(title, close);
6124
+ const body = document.createElement("div");
6125
+ body.className = "dsh-module-body";
6126
+ const intro = paragraph(copy[scope]);
6127
+ const hint = paragraph(copy.hint, "dsh-module-hint");
6128
+ const help = document.createElement("details");
6129
+ help.className = "dsh-module-help";
6130
+ const helpTitle = document.createElement("summary");
6131
+ helpTitle.textContent = copy.help;
6132
+ help.append(helpTitle, hint);
6133
+ const source = paragraph("", "dsh-module-source");
6134
+ const status = paragraph("", "dsh-module-notice");
5151
6135
  status.setAttribute("role", "status");
5152
6136
  status.setAttribute("aria-live", "polite");
5153
6137
  status.tabIndex = -1;
6138
+ const tools = document.createElement("div");
6139
+ tools.className = "dsh-module-tools";
6140
+ const searchLabel = document.createElement("label");
6141
+ searchLabel.className = "dsh-module-field";
6142
+ searchLabel.textContent = copy.search;
6143
+ const search = document.createElement("input");
6144
+ search.type = "search";
6145
+ search.placeholder = copy.searchPlaceholder;
6146
+ search.setAttribute("aria-label", copy.search);
6147
+ searchLabel.append(search);
6148
+ const filterLabel = document.createElement("label");
6149
+ filterLabel.className = "dsh-module-field";
6150
+ filterLabel.textContent = copy.filter;
6151
+ const filter = document.createElement("select");
6152
+ filter.dataset.clientModuleFilter = "true";
6153
+ filter.setAttribute("aria-label", copy.filter);
6154
+ for (const value of [
6155
+ "all",
6156
+ "loaded",
6157
+ "excluded"
6158
+ ]) {
6159
+ const option = document.createElement("option");
6160
+ option.value = value;
6161
+ option.textContent = copy[value];
6162
+ filter.append(option);
6163
+ }
6164
+ filterLabel.append(filter);
6165
+ tools.append(searchLabel, filterLabel);
6166
+ const results = paragraph("", "dsh-module-results");
6167
+ results.setAttribute("aria-live", "polite");
5154
6168
  const list = document.createElement("div");
5155
6169
  list.className = "dsh-module-list";
6170
+ const footer = document.createElement("div");
6171
+ footer.className = "dsh-module-footer";
6172
+ const changes = paragraph(copy.unchanged);
6173
+ changes.setAttribute("role", "status");
6174
+ changes.setAttribute("aria-live", "polite");
5156
6175
  const actions = document.createElement("div");
5157
6176
  actions.className = "dsh-module-actions";
5158
- const button = (text) => {
5159
- const result = document.createElement("button");
5160
- result.type = "button";
5161
- result.className = "dsh-module-button";
5162
- result.textContent = text;
5163
- return result;
5164
- };
5165
6177
  const save = button(copy.save);
6178
+ save.classList.add("dsh-module-save");
5166
6179
  const reset = button(copy.reset);
5167
- const close = button(copy.close);
5168
- actions.append(save, reset, close);
5169
- dialog.append(title, intro, status, list, actions);
6180
+ const reread = button(copy.retry);
6181
+ actions.append(save, reset, reread);
6182
+ footer.append(changes, status, actions);
6183
+ body.append(intro, source, tools, results, list, help);
6184
+ dialog.append(header, body, footer);
5170
6185
  let disposed = false;
5171
- let busy = false;
5172
- let loaded = false;
6186
+ let busy;
6187
+ let generation = 0;
6188
+ let draft;
6189
+ let unconfirmedWrite = false;
6190
+ const expansion = /* @__PURE__ */ new Map([
6191
+ ["extensions", true],
6192
+ ["advanced", false],
6193
+ ["required", false]
6194
+ ]);
5173
6195
  const choices = /* @__PURE__ */ new Map();
5174
- const setBusy = (value) => {
5175
- busy = value;
5176
- save.disabled = value || !loaded;
5177
- reset.disabled = value || !loaded;
5178
- for (const input of choices.values()) if (!input.dataset.required) input.disabled = value;
5179
- };
5180
- const request = async (body) => {
5181
- const init = {
5182
- method: body === void 0 ? "GET" : "POST",
5183
- signal: controller.signal,
5184
- ...body === void 0 ? {} : { body: JSON.stringify(body) }
5185
- };
5186
- init.headers = scope === "computer" ? localAdminRequestHeaders(init, location) : { "content-type": "application/json" };
5187
- if (scope === "device" && body !== void 0) {
5188
- const csrfCookie = document.cookie.split(";").map((value) => value.trim()).find((value) => value.startsWith("__Host-dsh_ma_csrf=")) ?? document.cookie.split(";").map((value) => value.trim()).find((value) => value.startsWith("dsh_ma_csrf="));
5189
- if (csrfCookie !== void 0) {
5190
- const headers = new Headers(init.headers);
5191
- headers.set("x-dsh-mobile-csrf", csrfCookie.slice(csrfCookie.indexOf("=") + 1));
5192
- init.headers = headers;
6196
+ const invalidActions = /* @__PURE__ */ new Map();
6197
+ const filterValue = () => filter.value === "loaded" ? "loaded" : filter.value === "excluded" ? "excluded" : "all";
6198
+ const updateControls = () => {
6199
+ const changed = draft?.changeCount ?? 0;
6200
+ save.disabled = busy !== void 0 || changed === 0;
6201
+ reset.disabled = busy !== void 0 || draft === void 0;
6202
+ reread.disabled = busy !== void 0;
6203
+ search.disabled = busy !== void 0 || draft === void 0;
6204
+ filter.disabled = search.disabled;
6205
+ save.textContent = busy === "write" ? copy.saving : copy.save;
6206
+ changes.textContent = `${changed === 0 ? copy.unchanged : copy.changes.replace("{count}", String(changed))} · ${copy.nextOpen}`;
6207
+ list.setAttribute("aria-busy", String(busy !== void 0));
6208
+ for (const input of choices.values()) input.disabled = busy !== void 0 || input.dataset.required === "true";
6209
+ for (const action of invalidActions.values()) action.disabled = busy !== void 0;
6210
+ };
6211
+ const renderList = () => {
6212
+ choices.clear();
6213
+ invalidActions.clear();
6214
+ list.replaceChildren();
6215
+ if (draft === void 0) {
6216
+ results.textContent = "";
6217
+ return;
6218
+ }
6219
+ const visible = draft.matchingEntries(search.value, filterValue());
6220
+ const invalid = draft.matchingInvalidExclusions(search.value, filterValue());
6221
+ results.textContent = copy.results.replace("{visible}", String(visible.length + invalid.length)).replace("{total}", String(draft.entries.length + draft.invalidExclusions.length));
6222
+ const filtered = search.value.trim() !== "" || filterValue() !== "all";
6223
+ if (visible.length === 0 && invalid.length === 0) {
6224
+ const empty = document.createElement("div");
6225
+ empty.className = "dsh-module-empty";
6226
+ empty.append(paragraph(copy.empty));
6227
+ const clear = button(copy.clearFilters);
6228
+ clear.addEventListener("click", () => {
6229
+ search.value = "";
6230
+ filter.value = "all";
6231
+ renderList();
6232
+ search.focus({ preventScroll: true });
6233
+ });
6234
+ empty.append(clear);
6235
+ list.append(empty);
6236
+ }
6237
+ if (invalid.length > 0) {
6238
+ const section = document.createElement("section");
6239
+ section.className = "dsh-module-group";
6240
+ section.dataset.clientModuleGroup = "invalid";
6241
+ const heading = document.createElement("h3");
6242
+ heading.textContent = `${copy.invalidSelections} (${invalid.length})`;
6243
+ section.append(heading, paragraph(copy.invalidHint, "dsh-module-groupHint"));
6244
+ for (const entry of invalid) {
6245
+ const row = document.createElement("div");
6246
+ row.className = "dsh-module-entry dsh-module-invalid-entry";
6247
+ row.dataset.invalidClientModuleId = entry.id;
6248
+ const id = document.createElement("code");
6249
+ id.textContent = entry.id;
6250
+ const remove = button(copy.clearInvalid);
6251
+ remove.setAttribute("aria-label", `${copy.clearInvalid}: ${entry.id}`);
6252
+ remove.addEventListener("click", () => {
6253
+ if (disposed || busy !== void 0 || draft?.clearInvalidExclusion(entry.id) !== true) return;
6254
+ status.textContent = "";
6255
+ status.classList.remove("dsh-module-error");
6256
+ renderList();
6257
+ (invalidActions.values().next().value ?? save).focus({ preventScroll: true });
6258
+ });
6259
+ row.append(id, paragraph(entry.reason === "missing" ? copy.invalidMissing : copy.invalidRequired), remove);
6260
+ invalidActions.set(entry.id, remove);
6261
+ section.append(row);
5193
6262
  }
6263
+ list.append(section);
5194
6264
  }
5195
- const response = await fetch(endpoint, init);
5196
- const value = await response.json();
5197
- if (!response.ok) {
5198
- const detail = typeof value === "object" && value !== null && "detail" in value && typeof value.detail === "string" ? value.detail.slice(0, 512) : "";
5199
- if (response.status === 409 && detail !== "" && typeof value === "object" && value !== null && "error" in value && value.error === "excluded_client_modules_invalid") throw new ClientModuleUiConflict(detail);
5200
- throw new Error("client_module_request_failed");
6265
+ for (const group of [
6266
+ "extensions",
6267
+ "advanced",
6268
+ "required"
6269
+ ]) {
6270
+ const entries = visible.filter((entry) => clientModuleGroup(entry) === group);
6271
+ if (entries.length === 0) continue;
6272
+ const section = document.createElement(group === "extensions" ? "section" : "details");
6273
+ section.className = "dsh-module-group";
6274
+ section.dataset.group = group;
6275
+ section.dataset.clientModuleGroup = group === "advanced" ? "system" : group;
6276
+ const headingText = `${group === "required" ? copy.requiredGroup : copy[group]} (${entries.length})`;
6277
+ if (section instanceof HTMLDetailsElement) {
6278
+ section.open = filtered || expansion.get(group) === true;
6279
+ const heading = document.createElement("summary");
6280
+ heading.textContent = headingText;
6281
+ section.append(heading);
6282
+ section.addEventListener("toggle", () => {
6283
+ if (!filtered) expansion.set(group, section.open);
6284
+ });
6285
+ } else {
6286
+ const heading = document.createElement("h3");
6287
+ heading.textContent = headingText;
6288
+ section.append(heading);
6289
+ }
6290
+ const content = document.createElement("div");
6291
+ content.append(paragraph(group === "required" ? copy.requiredHint : group === "advanced" ? copy.advancedHint : copy.extensionsHint, "dsh-module-groupHint"));
6292
+ for (const entry of entries) {
6293
+ const row = document.createElement("div");
6294
+ row.className = "dsh-module-entry";
6295
+ row.dataset.clientModuleId = entry.id;
6296
+ const label = document.createElement("label");
6297
+ label.className = "dsh-module-choice";
6298
+ const input = document.createElement("input");
6299
+ input.type = "checkbox";
6300
+ input.checked = draft.isLoaded(entry.id);
6301
+ if (entry.required) input.dataset.required = "true";
6302
+ const name = document.createElement("span");
6303
+ const shortName = document.createElement("span");
6304
+ shortName.className = "dsh-module-choiceName";
6305
+ shortName.textContent = clientModuleDisplayName(entry.id);
6306
+ const selected = document.createElement("small");
6307
+ selected.textContent = entry.required ? copy.required : input.checked ? copy.loaded : copy.excluded;
6308
+ name.append(shortName, selected);
6309
+ label.append(input, name);
6310
+ row.append(label);
6311
+ const detail = document.createElement("details");
6312
+ detail.className = "dsh-module-entryDetails";
6313
+ const detailSummary = document.createElement("summary");
6314
+ detailSummary.textContent = copy.details;
6315
+ const id = paragraph(`${copy.moduleId}: `);
6316
+ const code = document.createElement("code");
6317
+ code.textContent = entry.id;
6318
+ id.append(code);
6319
+ detail.append(detailSummary, id, paragraph(entry.dependencies.length === 0 ? copy.noDependencies : `${copy.dependencies}${entry.dependencies.join(", ")}`));
6320
+ row.append(detail);
6321
+ input.addEventListener("change", () => {
6322
+ if (busy !== void 0 || draft === void 0) return;
6323
+ draft.setLoaded(entry.id, input.checked);
6324
+ input.checked = draft.isLoaded(entry.id);
6325
+ selected.textContent = entry.required ? copy.required : input.checked ? copy.loaded : copy.excluded;
6326
+ status.textContent = "";
6327
+ status.classList.remove("dsh-module-error");
6328
+ if (filterValue() !== "all") {
6329
+ renderList();
6330
+ filter.focus({ preventScroll: true });
6331
+ }
6332
+ updateControls();
6333
+ });
6334
+ choices.set(entry.id, input);
6335
+ content.append(row);
6336
+ }
6337
+ section.append(content);
6338
+ list.append(section);
5201
6339
  }
5202
- return parseClientModuleView(value);
6340
+ updateControls();
5203
6341
  };
5204
6342
  const render = (view) => {
5205
- if (disposed) return;
5206
- loaded = true;
5207
- choices.clear();
5208
- list.replaceChildren();
5209
- for (const entry of view.entries) {
5210
- const label = document.createElement("label");
5211
- label.className = "dsh-module-choice";
5212
- const input = document.createElement("input");
5213
- input.type = "checkbox";
5214
- input.checked = !view.excludedClientModules.includes(entry.id);
5215
- input.disabled = entry.required;
5216
- if (entry.required) input.dataset.required = "true";
5217
- const name = document.createElement("span");
5218
- name.textContent = entry.id;
5219
- const hint = document.createElement("small");
5220
- hint.textContent = entry.required ? copy.required : entry.dependencies.length === 0 ? "" : `${copy.dependencies}${entry.dependencies.join(", ")}`;
5221
- name.append(hint);
5222
- label.append(input, name);
5223
- list.append(label);
5224
- choices.set(entry.id, input);
5225
- }
5226
- status.classList.remove("dsh-module-error");
5227
- status.textContent = view.source === "device" ? copy.deviceSource : copy.computerSource;
6343
+ draft = new ClientModuleDraft(view.entries, view.excludedClientModules);
6344
+ source.textContent = view.source === "device" ? copy.deviceSource : view.source === "computer" ? copy.computerSource : copy.pluginSource;
6345
+ renderList();
5228
6346
  };
5229
- const failed = (error) => {
5230
- if (disposed || controller.signal.aborted) return;
6347
+ const failed = (error, mutation) => {
5231
6348
  status.classList.add("dsh-module-error");
5232
- status.textContent = error instanceof ClientModuleUiConflict ? `${copy.conflict}${error.detail}` : copy.failed;
6349
+ status.textContent = error instanceof ClientModuleUiConflict ? `${copy.conflict}${error.detail}` : error instanceof ClientModuleRequestTimeout ? error.mutation ? copy.writeTimeout : copy.readTimeout : mutation ? copy.saveFailed : copy.failed;
5233
6350
  status.focus({ preventScroll: true });
5234
6351
  };
5235
- const mutate = (body) => {
5236
- if (busy || !loaded) return;
5237
- setBusy(true);
5238
- request(body).then((view) => {
6352
+ const runRequest = (payload) => {
6353
+ if (disposed || busy !== void 0 || payload !== void 0 && draft === void 0) return;
6354
+ const ownGeneration = ++generation;
6355
+ busy = payload === void 0 ? "read" : "write";
6356
+ if (payload !== void 0) unconfirmedWrite = true;
6357
+ status.classList.remove("dsh-module-error");
6358
+ status.textContent = payload === void 0 ? copy.loading : copy.saving;
6359
+ updateControls();
6360
+ requestClientModuleView(scope, controller.signal, payload).then((view) => {
6361
+ if (disposed || ownGeneration !== generation) return;
6362
+ unconfirmedWrite = false;
5239
6363
  render(view);
5240
- if (!disposed) status.textContent = copy.saved;
5241
- }, failed).finally(() => {
5242
- if (!disposed) setBusy(false);
6364
+ status.textContent = payload === void 0 ? "" : copy.saved;
6365
+ }, (error) => {
6366
+ if (disposed || ownGeneration !== generation) return;
6367
+ if (error instanceof ClientModuleUiConflict) unconfirmedWrite = false;
6368
+ failed(error, payload !== void 0);
6369
+ }).finally(() => {
6370
+ if (disposed || ownGeneration !== generation) return;
6371
+ busy = void 0;
6372
+ updateControls();
5243
6373
  });
5244
6374
  };
6375
+ search.addEventListener("input", renderList);
6376
+ filter.addEventListener("change", renderList);
5245
6377
  save.addEventListener("click", () => {
5246
- mutate({ excludedClientModules: [...choices].filter(([, input]) => !input.checked).map(([id]) => id) });
6378
+ if ((draft?.changeCount ?? 0) > 0) runRequest({ excludedClientModules: draft?.excludedIds });
5247
6379
  });
5248
6380
  reset.addEventListener("click", () => {
5249
- if (window.confirm(copy.resetConfirm)) mutate({ reset: true });
6381
+ if (busy === void 0 && draft !== void 0 && window.confirm(copy.resetConfirm)) runRequest({ reset: true });
6382
+ });
6383
+ reread.addEventListener("click", () => {
6384
+ if (busy === void 0 && ((draft?.changeCount ?? 0) === 0 || window.confirm(copy.reloadConfirm))) runRequest();
5250
6385
  });
5251
- const onBack = (event) => {
5252
- if (!event.cancelable || !dialog.open || event.defaultPrevented) return;
5253
- event.preventDefault();
5254
- event.stopImmediatePropagation();
5255
- dispose();
5256
- };
5257
- window.addEventListener("dsh-mobile:native-back", onBack, true);
5258
6386
  const dispose = () => {
5259
6387
  if (disposed) return;
5260
6388
  disposed = true;
6389
+ ++generation;
5261
6390
  controller.abort();
5262
- openDialogs.delete(dispose);
6391
+ openDialogs.delete(openDialog);
5263
6392
  window.removeEventListener("dsh-mobile:native-back", onBack, true);
6393
+ window.removeEventListener("keydown", onEscape, true);
5264
6394
  if (dialog.open) dialog.close();
5265
6395
  dialog.remove();
5266
6396
  if (trigger?.isConnected) trigger.focus({ preventScroll: true });
5267
6397
  };
5268
- openDialogs.add(dispose);
6398
+ const requestClose = () => {
6399
+ if (disposed) return;
6400
+ if (unconfirmedWrite && !window.confirm(copy.savingCloseConfirm)) return;
6401
+ if (!unconfirmedWrite && (draft?.changeCount ?? 0) > 0 && !window.confirm(copy.discardConfirm)) return;
6402
+ dispose();
6403
+ };
6404
+ const onBack = (event) => {
6405
+ if (!event.cancelable || !dialog.open || event.defaultPrevented || [...openDialogs].at(-1) !== openDialog) return;
6406
+ event.preventDefault();
6407
+ event.stopImmediatePropagation();
6408
+ requestClose();
6409
+ };
6410
+ const onEscape = (event) => {
6411
+ if (event.key !== "Escape" || event.isComposing || event.ctrlKey || event.altKey || event.metaKey || event.defaultPrevented || !dialog.open || [...openDialogs].at(-1) !== openDialog) return;
6412
+ event.preventDefault();
6413
+ event.stopImmediatePropagation();
6414
+ requestClose();
6415
+ };
6416
+ const openDialog = {
6417
+ dispose,
6418
+ requestClose
6419
+ };
6420
+ openDialogs.add(openDialog);
6421
+ window.addEventListener("dsh-mobile:native-back", onBack, true);
6422
+ window.addEventListener("keydown", onEscape, true);
6423
+ dialog.addEventListener("cancel", (event) => {
6424
+ event.preventDefault();
6425
+ requestClose();
6426
+ });
5269
6427
  dialog.addEventListener("close", dispose, { once: true });
5270
- close.addEventListener("click", dispose);
6428
+ close.addEventListener("click", requestClose);
5271
6429
  document.body.append(dialog);
5272
6430
  dialog.showModal();
5273
- setBusy(true);
5274
- status.textContent = copy.loading;
5275
- request().then(render, failed).finally(() => {
5276
- if (!disposed) setBusy(false);
5277
- });
6431
+ runRequest();
5278
6432
  }
5279
6433
  /** Settings row used on mobile pages and on the computer's General settings.
5280
6434
  * @param props - Selection scope and current locale.
@@ -5780,42 +6934,29 @@ window.__ModuleLoader__.load({
5780
6934
  error: "Could not switch computers. Try again."
5781
6935
  };
5782
6936
  }
5783
- /** Render the native-app-only action in DSH's General settings row style. */
5784
- function MobileSwitchComputerRow() {
5785
- const [nativeReady, setNativeReady] = (0, react.useState)(false);
5786
- const [state, setState] = (0, react.useState)("idle");
6937
+ function useMobileNativeSettingsAction(action) {
6938
+ const [controller] = (0, react.useState)(() => new MobileNativeSettingsActionController(action, () => window.__DSH_MOBILE_NATIVE__));
6939
+ const snapshot = (0, react.useSyncExternalStore)(controller.subscribe, controller.getSnapshot);
5787
6940
  (0, react.useEffect)(() => {
5788
- let disposed = false;
5789
- const checkNativeAction = () => {
5790
- const bridge = window.__DSH_MOBILE_NATIVE__;
5791
- if (bridge === void 0) {
5792
- setNativeReady(false);
5793
- return;
5794
- }
5795
- Promise.resolve().then(() => bridge.capabilities()).then((capabilities) => {
5796
- if (!disposed) setNativeReady(capabilities.includes("mobile.switch-computer"));
5797
- }, () => {
5798
- if (!disposed) setNativeReady(false);
5799
- });
5800
- };
5801
- checkNativeAction();
5802
- window.addEventListener("dsh-mobile-native-ready", checkNativeAction);
6941
+ controller.start();
6942
+ window.addEventListener("dsh-mobile-native-ready", controller.refresh);
5803
6943
  return () => {
5804
- disposed = true;
5805
- window.removeEventListener("dsh-mobile-native-ready", checkNativeAction);
6944
+ window.removeEventListener("dsh-mobile-native-ready", controller.refresh);
6945
+ controller.stop();
5806
6946
  };
5807
- }, []);
5808
- if (!nativeReady) return null;
6947
+ }, [controller]);
6948
+ return {
6949
+ ...snapshot,
6950
+ invoke: controller.invoke
6951
+ };
6952
+ }
6953
+ /** Render the native-app-only action in DSH's General settings row style. */
6954
+ function MobileSwitchComputerRow() {
6955
+ const { supported, state, invoke } = useMobileNativeSettingsAction("mobile.switch-computer");
6956
+ if (!supported) return null;
5809
6957
  const locale = selectedMobileControlLocale();
5810
6958
  const copy = mobileSwitchComputerCopy(locale);
5811
- const invoke = () => {
5812
- const bridge = window.__DSH_MOBILE_NATIVE__;
5813
- if (bridge === void 0 || state === "busy") return;
5814
- setState("busy");
5815
- bridge.invoke("mobile.switch-computer", {}).catch(() => {
5816
- setState("error");
5817
- });
5818
- };
6959
+ const busy = state === "busy" || state === "success";
5819
6960
  return (0, react.createElement)("div", {
5820
6961
  className: "dsh-mobile-settings_row",
5821
6962
  lang: locale
@@ -5825,10 +6966,10 @@ window.__ModuleLoader__.load({
5825
6966
  }, state === "error" ? copy.error : copy.description)), (0, react.createElement)("button", {
5826
6967
  type: "button",
5827
6968
  className: "dsh-mobile-settings_selector",
5828
- disabled: state === "busy",
5829
- "aria-busy": state === "busy" ? "true" : void 0,
6969
+ disabled: busy,
6970
+ "aria-busy": busy ? "true" : void 0,
5830
6971
  onClick: invoke
5831
- }, state === "busy" ? copy.busy : state === "error" ? copy.retry : copy.action));
6972
+ }, busy ? copy.busy : state === "error" ? copy.retry : copy.action));
5832
6973
  }
5833
6974
  function mobileTaskNotificationCopy(locale) {
5834
6975
  if (locale === "zh") return {
@@ -5864,42 +7005,10 @@ window.__ModuleLoader__.load({
5864
7005
  }
5865
7006
  /** Render the Android-only notification permission entry in DSH General settings. */
5866
7007
  function MobileTaskNotificationRow() {
5867
- const [nativeReady, setNativeReady] = (0, react.useState)(false);
5868
- const [state, setState] = (0, react.useState)("idle");
5869
- (0, react.useEffect)(() => {
5870
- let disposed = false;
5871
- const checkNativeAction = () => {
5872
- const bridge = window.__DSH_MOBILE_NATIVE__;
5873
- if (bridge === void 0) {
5874
- setNativeReady(false);
5875
- return;
5876
- }
5877
- Promise.resolve().then(() => bridge.capabilities()).then((capabilities) => {
5878
- if (!disposed) setNativeReady(capabilities.includes("notification.settings"));
5879
- }, () => {
5880
- if (!disposed) setNativeReady(false);
5881
- });
5882
- };
5883
- checkNativeAction();
5884
- window.addEventListener("dsh-mobile-native-ready", checkNativeAction);
5885
- return () => {
5886
- disposed = true;
5887
- window.removeEventListener("dsh-mobile-native-ready", checkNativeAction);
5888
- };
5889
- }, []);
5890
- if (!nativeReady) return null;
7008
+ const { supported, state, invoke } = useMobileNativeSettingsAction("notification.settings");
7009
+ if (!supported) return null;
5891
7010
  const locale = selectedMobileControlLocale();
5892
7011
  const copy = mobileTaskNotificationCopy(locale);
5893
- const invoke = () => {
5894
- const bridge = window.__DSH_MOBILE_NATIVE__;
5895
- if (bridge === void 0 || state === "busy") return;
5896
- setState("busy");
5897
- bridge.invoke("notification.settings", {}).then(() => {
5898
- setState("success");
5899
- }, () => {
5900
- setState("error");
5901
- });
5902
- };
5903
7012
  return (0, react.createElement)("div", {
5904
7013
  className: "dsh-mobile-settings_row",
5905
7014
  lang: locale
@@ -6069,6 +7178,11 @@ window.__ModuleLoader__.load({
6069
7178
  cpolar_installer_missing: "cpolarExtractFailed",
6070
7179
  cpolar_executable_missing: "cpolarExtractFailed",
6071
7180
  cpolar_extract_failed: "cpolarExtractFailed",
7181
+ cpolar_extract_tools_failed: "cpolarExtractToolsFailed",
7182
+ cpolar_extract_archive_failed: "cpolarExtractArchiveFailed",
7183
+ cpolar_extract_database_failed: "cpolarExtractDatabaseFailed",
7184
+ cpolar_extract_cabinet_failed: "cpolarExtractCabinetFailed",
7185
+ cpolar_extract_file_failed: "cpolarExtractFileFailed",
6072
7186
  cpolar_storage_failed: "cpolarStorageFailed",
6073
7187
  cpolar_config_missing: "cpolarConfigMissing",
6074
7188
  cpolar_config_invalid: "cpolarConfigInvalid",
@@ -6150,6 +7264,11 @@ window.__ModuleLoader__.load({
6150
7264
  "cpolar_component_invalid",
6151
7265
  "cpolar_config_missing",
6152
7266
  "cpolar_config_invalid",
7267
+ "cpolar_extract_tools_failed",
7268
+ "cpolar_extract_archive_failed",
7269
+ "cpolar_extract_database_failed",
7270
+ "cpolar_extract_cabinet_failed",
7271
+ "cpolar_extract_file_failed",
6153
7272
  "funnel_state_invalid",
6154
7273
  "cpolar_start_timeout",
6155
7274
  "cpolar_stopped",
@@ -10136,6 +11255,7 @@ window.__ModuleLoader__.load({
10136
11255
  const activations = new PerIdActivationLifecycle();
10137
11256
  const styleNodes = /* @__PURE__ */ new Map();
10138
11257
  const styleEtags = /* @__PURE__ */ new Map();
11258
+ const scriptEtags = /* @__PURE__ */ new Map();
10139
11259
  const scriptDigests = /* @__PURE__ */ new Map();
10140
11260
  const activeHostGenerations = /* @__PURE__ */ new Map();
10141
11261
  const activationKeys = /* @__PURE__ */ new Map();
@@ -10516,6 +11636,7 @@ window.__ModuleLoader__.load({
10516
11636
  styleNodes.get(id)?.remove();
10517
11637
  styleNodes.delete(id);
10518
11638
  styleEtags.delete(id);
11639
+ scriptEtags.delete(id);
10519
11640
  scriptDigests.delete(id);
10520
11641
  activeHostGenerations.delete(id);
10521
11642
  activationKeys.delete(id);
@@ -10524,6 +11645,7 @@ window.__ModuleLoader__.load({
10524
11645
  const managedManifestIdSources = () => [
10525
11646
  styleNodes.keys(),
10526
11647
  styleEtags.keys(),
11648
+ scriptEtags.keys(),
10527
11649
  scriptDigests.keys(),
10528
11650
  managedDefinitionIds
10529
11651
  ];
@@ -10617,12 +11739,13 @@ window.__ModuleLoader__.load({
10617
11739
  commitStyle(entry.id, styleChange, pendingCss, pendingStyleEtag);
10618
11740
  activations.remove(entry.id);
10619
11741
  activeHostGenerations.delete(entry.id);
11742
+ scriptEtags.delete(entry.id);
10620
11743
  scriptDigests.delete(entry.id);
10621
11744
  if (managedDefinitionIds.delete(entry.id)) definitions.delete(entry.id);
10622
11745
  } else {
10623
11746
  const scriptHeaders = {};
10624
- const storedDigest = scriptDigests.get(entry.id);
10625
- if (storedDigest !== void 0) scriptHeaders["if-none-match"] = storedDigest;
11747
+ const storedEtag = scriptEtags.get(entry.id);
11748
+ if (storedEtag !== void 0) scriptHeaders["if-none-match"] = storedEtag;
10626
11749
  const scriptResponse = await fetch(scriptUrl, {
10627
11750
  credentials: "same-origin",
10628
11751
  cache: "no-store",
@@ -10630,8 +11753,10 @@ window.__ModuleLoader__.load({
10630
11753
  signal
10631
11754
  });
10632
11755
  let nextDigest;
11756
+ let nextEtag;
10633
11757
  if (scriptResponse.status !== 304) {
10634
11758
  if (!scriptResponse.ok) throw new Error("mobile extension script failed to load");
11759
+ nextEtag = scriptResponse.headers.get("etag") ?? void 0;
10635
11760
  const source = await scriptResponse.text();
10636
11761
  if (refreshAborted(signal)) return false;
10637
11762
  const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(source));
@@ -10673,6 +11798,10 @@ window.__ModuleLoader__.load({
10673
11798
  }
10674
11799
  managedDefinitionIds.add(entry.id);
10675
11800
  if (nextDigest !== void 0) scriptDigests.set(entry.id, nextDigest);
11801
+ if (scriptResponse.status !== 304) {
11802
+ if (nextEtag === void 0 || nextEtag === "") scriptEtags.delete(entry.id);
11803
+ else scriptEtags.set(entry.id, nextEtag);
11804
+ }
10676
11805
  }
10677
11806
  } catch {
10678
11807
  if (evaluatedDefinition) {