dsh-mobile 0.5.1 → 0.5.3

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
@@ -664,6 +664,9 @@ window.__ModuleLoader__.load({
664
664
  frpVhostProbeFailed: "The VPS address could not be checked. Confirm DNS and retry.",
665
665
  frpLaunchFailed: "frpc failed to start.",
666
666
  frpTimeout: "The public endpoint did not become ready. Check frps, Caddy, DNS, and firewall.",
667
+ frpProxyNameInUse: "The frps already has a proxy with this installation name in use — usually another DSH computer, or a session that has not expired yet. Disable self-hosted FRP on that machine, or remove that proxy on the frps, then retry.",
668
+ frpRemotePortInUse: "Another proxy on the frps already owns this public entry port. Choose a different entry port and retry.",
669
+ frpProxyStartFailed: "The frps refused frpc's proxy registration. Check its allowPorts, the ports already in use, and its log.",
667
670
  frpDiscoveryMismatch: "The public domain points to another DSH computer. Check the Caddy and frps mappings.",
668
671
  frpDiscoveryInvalid: "The public domain did not return a valid DSH Mobile discovery response.",
669
672
  frpStopped: "The FRP connection stopped.",
@@ -751,6 +754,9 @@ window.__ModuleLoader__.load({
751
754
  frpVhostProbeFailed: "Impossibile controllare il VPS. Verifica il DNS e riprova.",
752
755
  frpLaunchFailed: "Avvio di frpc non riuscito.",
753
756
  frpTimeout: "Endpoint pubblico non pronto. Controlla frps, Caddy, DNS e firewall.",
757
+ frpProxyNameInUse: "Sul frps è già in uso un proxy con il nome di questa installazione — di solito un altro computer DSH o una sessione non ancora scaduta. Disattiva l’FRP autogestito su quella macchina, oppure rimuovi quel proxy sul frps, e riprova.",
758
+ frpRemotePortInUse: "Un altro proxy sul frps occupa già questa porta di ingresso pubblica. Scegli un’altra porta di ingresso e riprova.",
759
+ frpProxyStartFailed: "Il frps ha rifiutato la registrazione del proxy da parte di frpc. Controlla allowPorts, le porte già in uso e il log del server.",
754
760
  frpDiscoveryMismatch: "Il dominio pubblico punta a un altro computer DSH. Controlla Caddy e frps.",
755
761
  frpDiscoveryInvalid: "Il dominio pubblico non ha restituito una risposta DSH Mobile valida.",
756
762
  frpStopped: "Connessione FRP arrestata.",
@@ -838,6 +844,9 @@ window.__ModuleLoader__.load({
838
844
  frpVhostProbeFailed: "无法检查 VPS 地址,请确认 DNS 后重试。",
839
845
  frpLaunchFailed: "frpc 未能启动。",
840
846
  frpTimeout: "公网端点未能就绪,请检查 frps、Caddy、DNS 和防火墙。",
847
+ frpProxyNameInUse: "该 frps 上已有本机安装名对应的 FRP 代理在使用中——通常来自另一台 DSH 电脑,或尚未过期的旧连接。请在那台机器上关闭自建 FRP,或在 frps 上移除该代理后重试。",
848
+ frpRemotePortInUse: "该公网入口端口已被 frps 上的其他代理占用,请改用其他入口端口后重试。",
849
+ frpProxyStartFailed: "frps 拒绝了 frpc 的代理注册,请检查 frps 的 allowPorts、端口占用与服务器日志。",
841
850
  frpDiscoveryMismatch: "公开域名连接到了另一台 DSH 电脑,请检查 Caddy 与 frps 映射。",
842
851
  frpDiscoveryInvalid: "公开域名没有返回有效的 DSH Mobile 发现信息。",
843
852
  frpStopped: "FRP 连接已停止。",
@@ -2040,13 +2049,39 @@ window.__ModuleLoader__.load({
2040
2049
  return JSON.stringify(value);
2041
2050
  }
2042
2051
  /**
2052
+ * Legacy proxy name, kept only as the default of {@link createFrpcToml}.
2053
+ *
2054
+ * A shared frps accepts one proxy per name across *all* its clients, so a fixed
2055
+ * name let any other DSH installation — or a not yet reaped session of this one —
2056
+ * own `dsh-mobile` first. frpc then logged `start error: proxy name [dsh-mobile]
2057
+ * is already in use`, kept running without publishing the entry port, and the
2058
+ * start-up self-check ended in `frp_start_timeout` ("the public endpoint did not
2059
+ * become ready"). Product callers therefore pass an installation-derived name
2060
+ * from {@link frpProxyName}; this constant only keeps the exported API and the
2061
+ * byte-for-byte compatibility test working for legacy callers.
2062
+ */
2063
+ const FRP_DEFAULT_PROXY_NAME = "dsh-mobile";
2064
+ /**
2065
+ * Validate a proxy name for the generated frpc configuration.
2066
+ *
2067
+ * frps accepts one proxy per name on the whole server, so the name must be a
2068
+ * single safe token: no whitespace, no quotes, no path or bracket characters.
2069
+ */
2070
+ function validateFrpProxyName(value) {
2071
+ if (typeof value !== "string" || value.length < 1 || value.length > 128 || !/^[A-Za-z0-9._-]+$/u.test(value)) throw new Error("frp_proxy_name_invalid");
2072
+ return value;
2073
+ }
2074
+ /**
2043
2075
  * Build the single-purpose frpc configuration for the current loopback gateway.
2044
2076
  *
2045
2077
  * The default (public-CA) entry is an HTTP vhost behind Caddy and is emitted byte
2046
2078
  * for byte as before. The self-signed entry cannot use a vhost at all: frps only
2047
2079
  * forwards raw TCP, and the gateway terminates TLS on that connection.
2080
+ *
2081
+ * `proxyName` defaults to the legacy fixed name only for legacy callers; the
2082
+ * provider and the attach preview always pass {@link frpProxyName}.
2048
2083
  */
2049
- function createFrpcToml(settings, localPort) {
2084
+ function createFrpcToml(settings, localPort, proxyName = FRP_DEFAULT_PROXY_NAME) {
2050
2085
  if (!Number.isSafeInteger(localPort) || localPort < 1 || localPort > 65535) throw new Error("frp_local_port_invalid");
2051
2086
  const header = [
2052
2087
  `serverAddr = ${tomlString(settings.serverAddress)}`,
@@ -2056,7 +2091,7 @@ window.__ModuleLoader__.load({
2056
2091
  "transport.tls.enable = true",
2057
2092
  "",
2058
2093
  "[[proxies]]",
2059
- "name = \"dsh-mobile\""
2094
+ `name = ${tomlString(validateFrpProxyName(proxyName))}`
2060
2095
  ];
2061
2096
  if (isFrpSelfSignedIngress(settings)) return [
2062
2097
  ...header,
@@ -2125,7 +2160,7 @@ window.__ModuleLoader__.load({
2125
2160
  * the provider writes stays byte-for-byte identical to the upstream output.
2126
2161
  */
2127
2162
  function createFrpAttachFrpcToml(settings, options = {}) {
2128
- const frpcToml = createFrpcToml(settings, attachLocalPort(options));
2163
+ const frpcToml = createFrpcToml(settings, attachLocalPort(options), options.proxyName ?? "dsh-mobile");
2129
2164
  return options.revealToken === true ? frpcToml : redactFrpcTomlToken(frpcToml);
2130
2165
  }
2131
2166
  const DEFAULT_ATTACH_CONFIG_FILE = "<DSH Mobile 私有目录>/remote/frp/config/frpc.toml";
@@ -2700,7 +2735,9 @@ window.__ModuleLoader__.load({
2700
2735
  [data-dsh-mobile-history-loader] { position:relative !important; min-height:1px !important; }
2701
2736
  [data-dsh-mobile-history-loader] button:not(:disabled) { position:absolute !important; width:1px !important; height:1px !important; margin:-1px !important; padding:0 !important; clip-path:inset(50%) !important; opacity:0 !important; overflow:hidden !important; pointer-events:none !important; }
2702
2737
  [data-dsh-mobile-history-loader] button:disabled { min-height:28px !important; padding:4px 12px !important; }
2703
- [data-dsh-mobile-message-column] { box-sizing:border-box !important; width:100% !important; max-width:none !important; margin:0 !important; padding:0 !important; gap:10px !important; }
2738
+ /* DSH's sibling margins exclude hidden and empty Chat Node Seats. Keep that
2739
+ spacing rule so searchable hidden seats contribute no folded-turn gaps. */
2740
+ [data-dsh-mobile-message-column] { box-sizing:border-box !important; width:100% !important; max-width:none !important; margin:0 !important; padding:0 !important; gap:0 !important; --dsh-chat-flow-gap:10px; }
2704
2741
  [data-dsh-mobile-message-column] > * { width:100% !important; max-width:100% !important; }
2705
2742
  [data-dsh-mobile-message-column] [data-disclosure-row] { box-sizing:border-box !important; display:grid !important; grid-template-columns:16px minmax(0,1fr) !important; grid-auto-rows:auto !important; align-items:center !important; column-gap:6px !important; width:100% !important; height:auto !important; min-height:40px !important; padding:4px 0 !important; }
2706
2743
  [data-dsh-mobile-message-column] [data-disclosure-row] > [class*="_leading"] { grid-column:1 !important; grid-row:1 !important; margin-right:0 !important; }
@@ -2722,12 +2759,27 @@ window.__ModuleLoader__.load({
2722
2759
  /* The context ring is a composer-metadata _root too, so the metadata rule
2723
2760
  above stretched it across the whole dock. It cannot shrink, which left the
2724
2761
  session statistics beside it a few pixels wide, every number clipped to an
2725
- ellipsis. Put the numbers in a wrapping left column and the ring in a right
2726
- column it cannot grow out of: a phone then shows every number in full, and
2727
- a wide screen keeps the ring beside them on one line. */
2762
+ ellipsis. Keep the ring at its own size and let it hold the row the numbers
2763
+ are on. */
2728
2764
  [data-dsh-mobile-center] [data-composer-card] ~ * [class*="_root"]:has(> [class*="_trigger"][aria-haspopup="dialog"]) { width:auto !important; max-width:none !important; flex:0 0 auto !important; }
2729
- [data-dsh-mobile-center] [class*="_dock"]:has([data-composer-stats]) { display:grid !important; grid-template-columns:minmax(0,1fr) max-content !important; align-items:center !important; column-gap:12px !important; }
2730
- [data-dsh-mobile-center] [data-composer-card] ~ * [class*="_root"][data-composer-stats] { box-sizing:border-box !important; width:auto !important; max-width:none !important; flex:0 1 auto !important; flex-wrap:wrap !important; justify-content:flex-start !important; row-gap:2px !important; }
2765
+ /* Slot entries participate in this flex layout through display:contents.
2766
+ Keep core statistics and the meter together; extra entries use their own
2767
+ rows instead of taking the statistics' remaining width on phones. */
2768
+ [data-dsh-mobile-center] [class*="_dock"]:has([data-composer-stats]) { display:flex !important; flex-wrap:wrap !important; justify-content:flex-start !important; align-items:center !important; column-gap:12px !important; row-gap:2px !important; width:100% !important; max-width:100% !important; }
2769
+ /* Dock row gaps replace the single-row metadata's negative margin. */
2770
+ [data-dsh-mobile-center] [class*="_dock"]:has([data-composer-stats]) > *,
2771
+ [data-dsh-mobile-center] [class*="_dock"]:has([data-composer-stats]) [data-slot="conversation.composer.dock"] > * { margin-bottom:0 !important; }
2772
+ [data-dsh-mobile-center] [class*="_dock"]:has([data-composer-stats]) [data-slot="conversation.composer.dock"] > :not([data-composer-stats]) { flex:0 1 100% !important; min-width:0 !important; max-width:100% !important; }
2773
+ [data-dsh-mobile-center] [class*="_dock"]:has([data-composer-stats]) [data-composer-stats] { order:-2 !important; }
2774
+ [data-dsh-mobile-center] [class*="_dock"]:has([data-composer-stats]) > [class*="_root"]:has(> [class*="_trigger"][aria-haspopup="dialog"]) { order:-1 !important; }
2775
+ /* Interactive pills expose complete values in their labels and dialogs.
2776
+ Static compact or untimed pills have no disclosure, so they may wrap. */
2777
+ [data-dsh-mobile-center] [data-composer-card] ~ * [class*="_root"][data-composer-stats] { box-sizing:border-box !important; width:auto !important; max-width:none !important; flex:1 1 0 !important; flex-wrap:nowrap !important; justify-content:flex-start !important; overflow:hidden !important; row-gap:2px !important; }
2778
+ [data-dsh-mobile-center] [data-composer-stats] > * { min-width:0 !important; }
2779
+ [data-dsh-mobile-center] [data-composer-stats] [class*="_pill"] { display:block !important; min-width:0 !important; white-space:nowrap !important; overflow:hidden !important; text-overflow:ellipsis !important; }
2780
+ [data-dsh-mobile-center] [data-composer-stats] [class*="_pill"] svg { display:inline-block !important; vertical-align:-2px !important; margin-right:6px !important; }
2781
+ [data-dsh-mobile-center] [data-composer-stats]:has(span[class*="_pill"]) { flex-wrap:wrap !important; overflow:visible !important; }
2782
+ [data-dsh-mobile-center] [data-composer-stats] span[class*="_pill"] { flex:0 1 auto !important; max-width:100% !important; white-space:normal !important; overflow:visible !important; overflow-wrap:anywhere !important; text-overflow:clip !important; }
2731
2783
  /* Message runtime details are inline on desktop. Give the clock/runtime
2732
2784
  label its own wrapping row on narrow screens so TTFT and throughput do
2733
2785
  not push the action buttons or clip at the viewport edge. */
@@ -3762,6 +3814,107 @@ window.__ModuleLoader__.load({
3762
3814
  };
3763
3815
  }
3764
3816
  //#endregion
3817
+ //#region src/voice-session.ts
3818
+ /** Screen wake-lock ownership and mobile composer focus during DSH dictation. */
3819
+ /** Voice phases during which the user is dictating. */
3820
+ const DICTATING_PHASES = [
3821
+ "requesting",
3822
+ "recording",
3823
+ "transcribing"
3824
+ ];
3825
+ /** Whether DSH's published voice phase requires an active dictation surface. */
3826
+ function phaseIsDictating(phase) {
3827
+ return typeof phase === "string" && DICTATING_PHASES.includes(phase);
3828
+ }
3829
+ /** The wake lock API, or undefined where the browser does not offer it. */
3830
+ function wakeLockApi() {
3831
+ const api = navigator.wakeLock;
3832
+ return api === void 0 ? void 0 : api;
3833
+ }
3834
+ /**
3835
+ * Hold an available screen wake lock while DSH dictation is active and visible.
3836
+ * @param allowKeyboardDismissal Whether a mobile composer may lose text focus when capture starts.
3837
+ * @returns A disposer that releases held and subsequently returned wake locks.
3838
+ */
3839
+ function installVoiceSession(allowKeyboardDismissal = true) {
3840
+ const api = wakeLockApi();
3841
+ if (api === void 0 && !allowKeyboardDismissal) return () => {};
3842
+ let sentinel = null;
3843
+ let pending = false;
3844
+ let disposed = false;
3845
+ let generation = 0;
3846
+ let lastRow = null;
3847
+ let lastVisible = !document.hidden;
3848
+ const currentRow = () => Array.from(document.querySelectorAll("[data-voice-activity]")).find((row) => phaseIsDictating(row.getAttribute("data-voice-activity"))) ?? null;
3849
+ const releaseLock = (lock) => {
3850
+ if (!lock.released) lock.release().catch((_error) => {});
3851
+ };
3852
+ const release = () => {
3853
+ const held = sentinel;
3854
+ sentinel = null;
3855
+ if (held !== null) releaseLock(held);
3856
+ };
3857
+ const acquireIfWanted = () => {
3858
+ if (api === void 0 || disposed || pending || sentinel !== null || document.hidden) return;
3859
+ const row = currentRow();
3860
+ if (row === null) return;
3861
+ const requestedGeneration = generation;
3862
+ pending = true;
3863
+ api.request("screen").then((lock) => {
3864
+ lock.addEventListener("release", () => {
3865
+ if (sentinel === lock) sentinel = null;
3866
+ });
3867
+ if (disposed || document.hidden || requestedGeneration !== generation || currentRow() !== row) releaseLock(lock);
3868
+ else if (!lock.released) sentinel = lock;
3869
+ }, (_error) => {}).finally(() => {
3870
+ pending = false;
3871
+ if (!disposed && requestedGeneration !== generation) acquireIfWanted();
3872
+ });
3873
+ };
3874
+ const dismissKeyboard = (row) => {
3875
+ if (!allowKeyboardDismissal) return;
3876
+ if (!(window.__DSH_MOBILE_NATIVE__ !== void 0 ? window.__DSH_MOBILE_KEYBOARD_STATE__?.noHardwareKeyboard === true : window.matchMedia("(pointer:coarse) and (hover:none)").matches)) return;
3877
+ const active = document.activeElement;
3878
+ const composer = row.closest("[data-composer-card]");
3879
+ if (active instanceof HTMLElement && composer?.contains(active) && active.closest("[data-composer-input]") !== null && (active.isContentEditable || active instanceof HTMLTextAreaElement || active instanceof HTMLInputElement)) active.blur();
3880
+ };
3881
+ const apply = () => {
3882
+ const row = currentRow();
3883
+ const visible = !document.hidden;
3884
+ const changed = row !== lastRow || visible !== lastVisible;
3885
+ if (changed) {
3886
+ generation++;
3887
+ release();
3888
+ if (row !== null && row !== lastRow && visible) dismissKeyboard(row);
3889
+ }
3890
+ lastRow = row;
3891
+ lastVisible = visible;
3892
+ if (changed) acquireIfWanted();
3893
+ };
3894
+ const containsVoiceRow = (node) => node instanceof Element && (node.matches("[data-voice-activity]") || node.querySelector("[data-voice-activity]") !== null);
3895
+ const observer = new MutationObserver((records) => {
3896
+ if (records.some((record) => record.type === "attributes" || [...record.addedNodes, ...record.removedNodes].some(containsVoiceRow))) apply();
3897
+ });
3898
+ observer.observe(document.documentElement, {
3899
+ subtree: true,
3900
+ childList: true,
3901
+ attributes: true,
3902
+ attributeFilter: ["data-voice-activity"]
3903
+ });
3904
+ const onVisibility = () => {
3905
+ apply();
3906
+ };
3907
+ document.addEventListener("visibilitychange", onVisibility);
3908
+ apply();
3909
+ return () => {
3910
+ disposed = true;
3911
+ generation++;
3912
+ observer.disconnect();
3913
+ document.removeEventListener("visibilitychange", onVisibility);
3914
+ release();
3915
+ };
3916
+ }
3917
+ //#endregion
3765
3918
  //#region src/local-admin-host.ts
3766
3919
  /** Browser-safe desktop-admin Host checks. Do not import Node APIs here. */
3767
3920
  const DESKTOP_ADMIN_HEADER = "x-dsh-mobile-desktop-admin";
@@ -4365,6 +4518,9 @@ window.__ModuleLoader__.load({
4365
4518
  frp_vhost_probe_failed: "frpVhostProbeFailed",
4366
4519
  frp_launch_failed: "frpLaunchFailed",
4367
4520
  frp_start_timeout: "frpTimeout",
4521
+ frp_proxy_name_in_use: "frpProxyNameInUse",
4522
+ frp_remote_port_in_use: "frpRemotePortInUse",
4523
+ frp_proxy_start_failed: "frpProxyStartFailed",
4368
4524
  frp_discovery_mismatch: "frpDiscoveryMismatch",
4369
4525
  frp_discovery_invalid: "frpDiscoveryInvalid",
4370
4526
  frp_stopped: "frpStopped",
@@ -4426,6 +4582,9 @@ window.__ModuleLoader__.load({
4426
4582
  "frp_vhost_probe_failed",
4427
4583
  "frp_launch_failed",
4428
4584
  "frp_start_timeout",
4585
+ "frp_proxy_name_in_use",
4586
+ "frp_remote_port_in_use",
4587
+ "frp_proxy_start_failed",
4429
4588
  "frp_discovery_mismatch",
4430
4589
  "frp_discovery_invalid",
4431
4590
  "frp_stopped",
@@ -4972,10 +5131,12 @@ window.__ModuleLoader__.load({
4972
5131
  frpCopyAttachPlan.type = "button";
4973
5132
  frpCopyAttachPlan.textContent = t("frpAttachPlan");
4974
5133
  frpCopyAttachPlan.hidden = true;
5134
+ frpCopyAttachPlan.disabled = true;
4975
5135
  const frpCopyAttachToken = element("button", "dsh-mobile-control__secondary dsh-mobile-control__frp-action");
4976
5136
  frpCopyAttachToken.type = "button";
4977
5137
  frpCopyAttachToken.textContent = t("frpAttachToken");
4978
5138
  frpCopyAttachToken.hidden = true;
5139
+ frpCopyAttachToken.disabled = true;
4979
5140
  const frpSelfCheckButton = element("button", "dsh-mobile-control__secondary dsh-mobile-control__frp-action");
4980
5141
  frpSelfCheckButton.type = "button";
4981
5142
  frpSelfCheckButton.textContent = t("frpAttachSelfCheck");
@@ -5555,6 +5716,8 @@ window.__ModuleLoader__.load({
5555
5716
  let configuredFrpOrigin = "";
5556
5717
  let configuredFrpVhostPort = 7080;
5557
5718
  let configuredFrpPublicPort = 33080;
5719
+ /** Proxy name the host writes into its frpc.toml; the local clipboard copy must match. */
5720
+ let configuredFrpProxyName;
5558
5721
  let frpConfiguredMode = "deploy";
5559
5722
  let frpConfiguredEntryTls = "public-ip-cert";
5560
5723
  let frpModeDraft = false;
@@ -5985,6 +6148,7 @@ window.__ModuleLoader__.load({
5985
6148
  if (frpConfigured && !frpEntryTlsDraft) frpEntryTls.value = configuredFrpEntryTls;
5986
6149
  if (typeof frpConfiguration.vhostHttpPort === "number") configuredFrpVhostPort = frpConfiguration.vhostHttpPort;
5987
6150
  if (typeof frpConfiguration.publicPort === "number") configuredFrpPublicPort = frpConfiguration.publicPort;
6151
+ configuredFrpProxyName = typeof frpConfiguration.proxyName === "string" && frpConfiguration.proxyName !== "" ? frpConfiguration.proxyName : void 0;
5988
6152
  if (!frpVhostDraft) {
5989
6153
  if (frpMode.value === "attach" && configuredFrpMode === "attach" && configuredFrpEntryTls === "public-ip-cert") {
5990
6154
  frpVhostPort.value = String(configuredFrpVhostPort);
@@ -5995,12 +6159,14 @@ window.__ModuleLoader__.load({
5995
6159
  const attachSelected = frpMode.value === "attach";
5996
6160
  const selfSignedSelected = attachSelected && frpEntryTls.value === "self-signed";
5997
6161
  frpStep2Title.textContent = t(attachSelected ? "frpStep2TitleAttach" : "frpStep2Title");
5998
- frpStep2Text.textContent = t(attachSelected ? "frpStep2TextAttach" : "frpStep2Text");
6162
+ frpStep2Text.textContent = attachSelected && configuredFrpProxyName === void 0 ? t("loadingRemoteStatus") : t(attachSelected ? "frpStep2TextAttach" : "frpStep2Text");
5999
6163
  frpStep4Text.textContent = t(attachSelected ? "frpStep4TextAttach" : "frpStep4Text");
6000
6164
  frpAppRequirement.textContent = t(selfSignedSelected ? "frpAppRequirementSelfSigned" : "frpAppRequirement");
6001
6165
  frpCopyTemplate.hidden = attachSelected;
6002
6166
  frpCopyAttachPlan.hidden = !attachSelected;
6003
6167
  frpCopyAttachToken.hidden = !attachSelected;
6168
+ frpCopyAttachPlan.disabled = remoteProviderBusy || configuredFrpProxyName === void 0;
6169
+ frpCopyAttachToken.disabled = remoteProviderBusy || configuredFrpProxyName === void 0;
6004
6170
  frpSelfCheckButton.hidden = !frpConfigured;
6005
6171
  frpVhostPort.disabled = selfSignedSelected;
6006
6172
  frpVhostLabel.hidden = selfSignedSelected;
@@ -6478,6 +6644,13 @@ window.__ModuleLoader__.load({
6478
6644
  }
6479
6645
  remoteStatus.textContent = message;
6480
6646
  };
6647
+ const frpClipboardProxyName = () => {
6648
+ if (configuredFrpProxyName === void 0) {
6649
+ remoteStatus.textContent = t("loadingRemoteStatus");
6650
+ loadRemote();
6651
+ }
6652
+ return configuredFrpProxyName;
6653
+ };
6481
6654
  frpCopyAttachPlan.addEventListener("click", () => {
6482
6655
  const form = frpForm();
6483
6656
  if (!validFrpServer(form.serverAddress) || !Number.isSafeInteger(form.serverPort) || form.serverPort < 1 || form.serverPort > 65535 || form.publicOrigin === "") {
@@ -6491,7 +6664,9 @@ window.__ModuleLoader__.load({
6491
6664
  showFrpAttachError(code);
6492
6665
  return;
6493
6666
  }
6494
- navigator.clipboard.writeText(createFrpAttachTemplateForClipboard(attachForm)).then(() => {
6667
+ const proxyName = frpClipboardProxyName();
6668
+ if (proxyName === void 0) return;
6669
+ navigator.clipboard.writeText(createFrpAttachTemplateForClipboard(attachForm, { proxyName })).then(() => {
6495
6670
  remoteStatus.textContent = t("frpAttachPlanCopied");
6496
6671
  }, () => {
6497
6672
  remoteStatus.textContent = t("frpAttachPlanFailed", { error: t("templateCopyFailed") });
@@ -6519,7 +6694,7 @@ window.__ModuleLoader__.load({
6519
6694
  remoteStatus.textContent = t("frpAttachPlanFailed", { error: String(error) });
6520
6695
  }).finally(() => {
6521
6696
  remoteProviderBusy = false;
6522
- frpCopyAttachPlan.disabled = false;
6697
+ frpCopyAttachPlan.disabled = configuredFrpProxyName === void 0;
6523
6698
  });
6524
6699
  });
6525
6700
  /** Copy a token-bearing frpc.toml only from the token the user just typed. */
@@ -6536,7 +6711,12 @@ window.__ModuleLoader__.load({
6536
6711
  showFrpAttachError(code);
6537
6712
  return;
6538
6713
  }
6539
- navigator.clipboard.writeText(createFrpAttachFrpcTomlForClipboard(attachForm, { revealToken: true })).then(() => {
6714
+ const proxyName = frpClipboardProxyName();
6715
+ if (proxyName === void 0) return;
6716
+ navigator.clipboard.writeText(createFrpAttachFrpcTomlForClipboard(attachForm, {
6717
+ revealToken: true,
6718
+ proxyName
6719
+ })).then(() => {
6540
6720
  remoteStatus.textContent = t("frpAttachTokenCopied");
6541
6721
  }, () => {
6542
6722
  remoteStatus.textContent = t("frpAttachTokenFailed", { error: t("templateCopyFailed") });
@@ -8707,6 +8887,7 @@ window.__ModuleLoader__.load({
8707
8887
  style.dataset.plugin = "dsh-mobile";
8708
8888
  style.textContent = desktopAdmin ? CONTROL_STYLES : NATIVE_MOBILE_STYLES;
8709
8889
  document.head.append(style);
8890
+ const removeVoiceSession = installVoiceSession(!desktopAdmin);
8710
8891
  if (!desktopAdmin) {
8711
8892
  const removeSettingsAction = ctx.slots.inject("settings.general.item", () => {
8712
8893
  const removeSwitchComputer = ctx.slots.register({
@@ -8733,6 +8914,7 @@ window.__ModuleLoader__.load({
8733
8914
  removeSettingsAction();
8734
8915
  removeCustom();
8735
8916
  removeSurface();
8917
+ removeVoiceSession();
8736
8918
  style.remove();
8737
8919
  };
8738
8920
  }
@@ -8778,6 +8960,7 @@ window.__ModuleLoader__.load({
8778
8960
  });
8779
8961
  return () => {
8780
8962
  removeControl();
8963
+ removeVoiceSession();
8781
8964
  style.remove();
8782
8965
  };
8783
8966
  }, "dsh-mobile: stock mobile adaptation and local control");