dsh-mobile 0.8.4 → 0.8.6

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
@@ -803,7 +803,7 @@ window.__ModuleLoader__.load({
803
803
  lanSetupConfiguring: "Creating private certificates and local network rules…",
804
804
  lanSetupRequired: "Local network setup has not been completed. Choose a network above to continue.",
805
805
  lanSetupRestart: "Setup complete for {origin}. Restart DSH once to start local network access.",
806
- lanSetupFailed: "Setup was not completed. Confirm the network selection and approve the Windows prompt, then try again.",
806
+ lanSetupFailed: "Setup was not completed. Existing firewall rules were retained when possible. Approve the Windows prompt and retry; if it still fails, check dsh-mobile.log for the exact firewall error.",
807
807
  lanChangeTitle: "Change network",
808
808
  lanChangeIntro: "Move local network access to another interface. The private CA and paired devices are kept; connected phones reconnect with the new address.",
809
809
  lanChangeSelect: "New network",
@@ -833,7 +833,7 @@ window.__ModuleLoader__.load({
833
833
  lanSetupConfiguring: "Creazione dei certificati privati e delle regole di rete locale…",
834
834
  lanSetupRequired: "La configurazione della rete locale non è completa. Scegli una rete qui sopra per continuare.",
835
835
  lanSetupRestart: "Configurazione completata per {origin}. Riavvia DSH una volta per attivare l’accesso locale.",
836
- lanSetupFailed: "Configurazione non completata. Controlla la rete e approva la richiesta di Windows, quindi riprova.",
836
+ lanSetupFailed: "Configurazione non completata. Le regole firewall esistenti sono state mantenute quando possibile. Approva la richiesta di Windows e riprova; se fallisce ancora, controlla dsh-mobile.log per l’errore esatto.",
837
837
  lanChangeTitle: "Cambia rete",
838
838
  lanChangeIntro: "Sposta l’accesso alla rete locale su un’altra interfaccia. La CA privata e i dispositivi abbinati restano; i telefoni collegati si riconnettono con il nuovo indirizzo.",
839
839
  lanChangeSelect: "Nuova rete",
@@ -863,7 +863,7 @@ window.__ModuleLoader__.load({
863
863
  lanSetupConfiguring: "正在生成私有证书并配置局域网规则…",
864
864
  lanSetupRequired: "尚未完成局域网配置,请选择上方网络继续。",
865
865
  lanSetupRestart: "已为 {origin} 完成配置。请重启一次 DSH,局域网访问会自动开启。",
866
- lanSetupFailed: "配置未完成。请确认网卡选择并允许 Windows 授权提示,然后重试。",
866
+ lanSetupFailed: "配置未完成。原有防火墙规则已尽量保留;请允许 Windows 授权提示后重试,若仍失败请查看 dsh-mobile.log 中的具体错误。",
867
867
  lanChangeTitle: "更换网络",
868
868
  lanChangeIntro: "把局域网访问切换到另一块网卡。私有 CA 与已配对设备都会保留,已连接的手机需要用新地址重新连接。",
869
869
  lanChangeSelect: "新网络",
@@ -1172,6 +1172,7 @@ window.__ModuleLoader__.load({
1172
1172
  irohConnecting: "Sidecar connected; starting the DSH authentication gateway…",
1173
1173
  irohCompanionMissing: "No verified iroh sidecar is available. Use Install official component below; a separate companion plugin is optional.",
1174
1174
  irohDataDirectoryInvalid: "Could not prepare the private sidecar directory. Check that DSH can manage its own profile directory, then retry.",
1175
+ irohDataDirectoryInsecure: "Windows parent folders of the iroh state path must not grant write or replacement rights to other users. Choose a private profile directory or correct the parent ACLs, then retry.",
1175
1176
  irohSidecarInvalid: "The installed sidecar failed checksum verification. Reinstall the component.",
1176
1177
  irohDownloadFailed: "The sidecar download failed before verification. Check the network and retry.",
1177
1178
  irohPlatformUnsupported: "iroh sidecars are published for Windows x64 and Linux x64 only.",
@@ -1207,6 +1208,7 @@ window.__ModuleLoader__.load({
1207
1208
  irohConnecting: "Sidecar connesso; avvio del gateway di autenticazione DSH…",
1208
1209
  irohCompanionMissing: "Nessun sidecar iroh verificato disponibile. Usa Installa componente ufficiale qui sotto; il plugin complementare è facoltativo.",
1209
1210
  irohDataDirectoryInvalid: "Impossibile preparare la directory privata del sidecar. Verifica che DSH possa gestire la propria directory del profilo, poi riprova.",
1211
+ irohDataDirectoryInsecure: "Le cartelle Windows superiori del percorso di stato iroh non devono concedere ad altri utenti diritti di scrittura o sostituzione. Scegli una directory profilo privata o correggi le ACL superiori, poi riprova.",
1210
1212
  irohSidecarInvalid: "Il sidecar installato non ha superato la verifica del checksum. Reinstalla il componente.",
1211
1213
  irohDownloadFailed: "Download del sidecar non riuscito prima della verifica. Controlla la rete e riprova.",
1212
1214
  irohPlatformUnsupported: "I sidecar iroh sono pubblicati solo per Windows x64 e Linux x64.",
@@ -1242,6 +1244,7 @@ window.__ModuleLoader__.load({
1242
1244
  irohConnecting: "Sidecar 已连接,正在启动 DSH 认证网关…",
1243
1245
  irohCompanionMissing: "尚无校验通过的 iroh 组件,请点击下方「安装官方组件」;无需另装配套插件。",
1244
1246
  irohDataDirectoryInvalid: "无法准备 sidecar 私有目录。请确认 DSH 能管理自己的配置目录后重试。",
1247
+ irohDataDirectoryInsecure: "iroh 状态路径的 Windows 上级目录不能向其他用户授予写入或替换权限;请改用私有配置目录或修正上级目录 ACL 后重试。",
1245
1248
  irohSidecarInvalid: "已安装的 sidecar 哈希校验失败,请重装组件。",
1246
1249
  irohDownloadFailed: "下载 sidecar 失败,还没开始校验。请检查网络后重试。",
1247
1250
  irohPlatformUnsupported: "iroh sidecar 仅发布 Windows x64 与 Linux x64 版本。",
@@ -2213,6 +2216,27 @@ window.__ModuleLoader__.load({
2213
2216
  frpConfigurationMissingAttach: "填写既有 frps 信息,在本机安装 frpc,然后保存并验证连接。",
2214
2217
  purgeFrpConfirmSelfSigned: "要停止 FRP 并删除本机 frpc、Token、配置、证书、私钥、身份记录、临时文件和日志吗?通过此自签入口配对的手机都需重新配对;VPS 不会改变。"
2215
2218
  });
2219
+ Object.assign(MOBILE_CONTROL_MESSAGES.zh, {
2220
+ macPermissionTitle: "请确认已开启「完全磁盘访问权限」",
2221
+ macPermissionSummary: "开启后,远程使用期间不会再弹出需要确认的文件夹授权窗口,连接也不会因此中断。",
2222
+ macPermissionSteps: "系统设置 → 隐私与安全性 → 完全磁盘访问权限 → 打开 DeepSeek Harness",
2223
+ macPermissionOpen: "打开系统设置",
2224
+ macPermissionDismiss: "我知道了"
2225
+ });
2226
+ Object.assign(MOBILE_CONTROL_MESSAGES.en, {
2227
+ macPermissionTitle: "Confirm Full Disk Access is enabled",
2228
+ macPermissionSummary: "With it enabled, remote use will not raise folder authorization dialogs, and the connection will not stall on them.",
2229
+ macPermissionSteps: "System Settings → Privacy & Security → Full Disk Access → turn on DeepSeek Harness",
2230
+ macPermissionOpen: "Open System Settings",
2231
+ macPermissionDismiss: "Got it"
2232
+ });
2233
+ Object.assign(MOBILE_CONTROL_MESSAGES.it, {
2234
+ macPermissionTitle: "Verifica che l’accesso completo al disco sia attivo",
2235
+ macPermissionSummary: "Se attivo, durante l’uso remoto non compariranno finestre di autorizzazione per le cartelle e la connessione non si bloccherà.",
2236
+ macPermissionSteps: "Impostazioni di Sistema → Privacy e sicurezza → Accesso completo al disco → attiva DeepSeek Harness",
2237
+ macPermissionOpen: "Apri Impostazioni di Sistema",
2238
+ macPermissionDismiss: "Ho capito"
2239
+ });
2216
2240
  /** Caddy snippet owned entirely by DSH Mobile; the main Caddyfile only imports it. */
2217
2241
  const FRP_CADDY_SNIPPET_PATH = "/etc/caddy/dsh-mobile-dsh.caddy";
2218
2242
  /** First line of the owned snippet; also the legacy whole-file marker. */
@@ -3767,6 +3791,28 @@ window.__ModuleLoader__.load({
3767
3791
  if (bridge === void 0 || typeof bridge.capabilities !== "function" || typeof bridge.invoke !== "function") return void 0;
3768
3792
  return bridge;
3769
3793
  }
3794
+ /** Encode one opaque Session id for the App's internal notification route. */
3795
+ function taskNotificationNavigate(sessionId) {
3796
+ if (sessionId === "") return void 0;
3797
+ const bytes = new TextEncoder().encode(sessionId);
3798
+ let binary = "";
3799
+ for (const byte of bytes) binary += String.fromCharCode(byte);
3800
+ const route = `/?dsh-session=${globalThis.btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "")}`;
3801
+ return route.length <= 270 ? route : void 0;
3802
+ }
3803
+ /** Decode only the App's bounded internal Session route. */
3804
+ function decodeTaskNotificationNavigate(value) {
3805
+ if (value === null || value === void 0 || !/^[A-Za-z0-9_-]{1,256}$/u.test(value)) return void 0;
3806
+ try {
3807
+ const padded = value.replaceAll("-", "+").replaceAll("_", "/") + "=".repeat((4 - value.length % 4) % 4);
3808
+ const binary = globalThis.atob(padded);
3809
+ const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
3810
+ const decoded = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
3811
+ return decoded === "" ? void 0 : decoded;
3812
+ } catch {
3813
+ return;
3814
+ }
3815
+ }
3770
3816
  /** Parse a `task-notify` SSE payload; rejects anything malformed or empty. */
3771
3817
  function parseTaskNotifyPayload(value) {
3772
3818
  let record;
@@ -3821,7 +3867,8 @@ window.__ModuleLoader__.load({
3821
3867
  return bridge.invoke("notification.notify", {
3822
3868
  title: event.title,
3823
3869
  body: event.body,
3824
- tag: event.tag
3870
+ tag: event.tag,
3871
+ ...event.navigate === void 0 ? {} : { navigate: event.navigate }
3825
3872
  });
3826
3873
  }).then(() => void 0, (error) => {
3827
3874
  const code = typeof error === "object" && error !== null && "code" in error ? String(error.code) : "";
@@ -9558,10 +9605,7 @@ window.__ModuleLoader__.load({
9558
9605
  });
9559
9606
  if (currentHostSubscription) consentedSubscriptions.delete(currentHostSubscription);
9560
9607
  currentHostSubscription = void 0;
9561
- const sub = ownedSubscription;
9562
- if (sub) {
9563
- if (await sub.unsubscribe().catch(() => false) && ownedSubscription === sub) ownedSubscription = void 0;
9564
- }
9608
+ ownedSubscription = void 0;
9565
9609
  if (await refresh(token) && current(token) && operation === attempt && config?.status === "disabled") publish({
9566
9610
  state: "disabled",
9567
9611
  preferences: config.preferences
@@ -9722,7 +9766,13 @@ window.__ModuleLoader__.load({
9722
9766
  const MOBILE_PUSH_SETTINGS_COPY = {
9723
9767
  en: {
9724
9768
  title: "Web push notifications",
9725
- desc: "Receive notifications when this page is closed.",
9769
+ desc: "Receive notifications when this page is closed. Disable stops sends but keeps the browser subscription so it can be reused safely.",
9770
+ permission: {
9771
+ granted: "System permission: granted. A reinstalled Home Screen app can inherit this site-level grant.",
9772
+ default: "System permission: not decided.",
9773
+ denied: "System permission: blocked.",
9774
+ unknown: "System permission: unavailable."
9775
+ },
9726
9776
  enable: "Enable",
9727
9777
  disable: "Disable",
9728
9778
  test: "Send test",
@@ -9747,7 +9797,13 @@ window.__ModuleLoader__.load({
9747
9797
  },
9748
9798
  zh: {
9749
9799
  title: "网页推送通知",
9750
- desc: "关闭此页面后仍可接收通知。",
9800
+ desc: "关闭此页面后仍可接收通知。停用会停止发送,但保留浏览器订阅,便于安全地重新启用。",
9801
+ permission: {
9802
+ granted: "系统权限:已允许。重新安装的主屏幕 App 可能继承站点级授权。",
9803
+ default: "系统权限:尚未选择。",
9804
+ denied: "系统权限:已阻止。",
9805
+ unknown: "系统权限:不可读取。"
9806
+ },
9751
9807
  enable: "启用",
9752
9808
  disable: "停用",
9753
9809
  test: "发送测试",
@@ -9772,7 +9828,13 @@ window.__ModuleLoader__.load({
9772
9828
  },
9773
9829
  it: {
9774
9830
  title: "Notifiche Web push",
9775
- desc: "Ricevi notifiche quando la pagina è chiusa.",
9831
+ desc: "Ricevi notifiche quando la pagina è chiusa. La disattivazione interrompe gli invii ma conserva l’iscrizione del browser per poterla riutilizzare.",
9832
+ permission: {
9833
+ granted: "Permesso di sistema: concesso. Una Web App reinstallata può ereditare questa autorizzazione del sito.",
9834
+ default: "Permesso di sistema: non ancora scelto.",
9835
+ denied: "Permesso di sistema: bloccato.",
9836
+ unknown: "Permesso di sistema: non disponibile."
9837
+ },
9776
9838
  enable: "Attiva",
9777
9839
  disable: "Disattiva",
9778
9840
  test: "Invia test",
@@ -9821,6 +9883,7 @@ window.__ModuleLoader__.load({
9821
9883
  if (snapshot.state === "unsupported") return null;
9822
9884
  const prefs = snapshot.preferences;
9823
9885
  const Switch = switchComponent ?? requirePublicMobilePushSwitch();
9886
+ const permission = typeof Notification === "undefined" ? "unknown" : Notification.permission;
9824
9887
  const toggle = (key, checked) => {
9825
9888
  if (!prefs) return;
9826
9889
  controller.updatePreferences({
@@ -9842,7 +9905,7 @@ window.__ModuleLoader__.load({
9842
9905
  }, (0, react.createElement)("div", { className: "dsh-mobile-settings_title" }, copy.title), (0, react.createElement)("div", {
9843
9906
  className: "dsh-mobile-settings_desc",
9844
9907
  "aria-live": "polite"
9845
- }, message(snapshot, copy)), prefs && Switch !== void 0 ? (0, react.createElement)("div", { className: "dsh-mobile-push-categories" }, ...KEYS.map((key, index) => {
9908
+ }, message(snapshot, copy)), (0, react.createElement)("div", { className: "dsh-mobile-settings_desc" }, copy.permission[permission]), prefs && Switch !== void 0 ? (0, react.createElement)("div", { className: "dsh-mobile-push-categories" }, ...KEYS.map((key, index) => {
9846
9909
  const name = copy.categories[index];
9847
9910
  if (name === void 0) return null;
9848
9911
  return (0, react.createElement)("label", {
@@ -9883,7 +9946,6 @@ window.__ModuleLoader__.load({
9883
9946
  }, copy.busy)));
9884
9947
  }
9885
9948
  // src/mobile-push/navigation.ts
9886
- const REF_PARAMETER = "mobile-push";
9887
9949
  /**
9888
9950
  * Consume one authenticated tap reference after the public Workspace and Session
9889
9951
  * list baselines are ready. Return the disposal function for list subscriptions.
@@ -9894,15 +9956,16 @@ window.__ModuleLoader__.load({
9894
9956
  location: window.location,
9895
9957
  history: window.history
9896
9958
  };
9959
+ const refParameter = options.parameter ?? "mobile-push";
9897
9960
  const params = new URLSearchParams(browser.location.search);
9898
- const reference = params.get(REF_PARAMETER);
9961
+ const reference = params.get(refParameter);
9899
9962
  let disposed = false;
9900
9963
  let consumed = false;
9901
9964
  let resolving = false;
9902
9965
  let opened = false;
9903
9966
  const clearReference = () => {
9904
- if (!params.has(REF_PARAMETER)) return;
9905
- params.delete(REF_PARAMETER);
9967
+ if (!params.has(refParameter)) return;
9968
+ params.delete(refParameter);
9906
9969
  const query = params.toString();
9907
9970
  const url = `${browser.location.pathname}${query ? `?${query}` : ""}${browser.location.hash}`;
9908
9971
  browser.history.replaceState(null, "", url);
@@ -11388,6 +11451,111 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
11388
11451
  onClick: invoke
11389
11452
  }, state === "busy" ? copy.busy : state === "success" ? copy.reopen : state === "error" ? copy.retry : copy.action));
11390
11453
  }
11454
+ function mobileBackgroundNotificationCopy(locale) {
11455
+ if (locale === "zh") return {
11456
+ title: "后台任务提醒",
11457
+ description: "App 页面被系统挂起后,通过可关闭的常驻连接继续接收任务完成提醒。",
11458
+ enabled: "关闭后台提醒",
11459
+ disabled: "开启后台提醒",
11460
+ busy: "正在处理…",
11461
+ error: "无法更新后台提醒;请确认任务通知权限和当前连接。"
11462
+ };
11463
+ if (locale === "it") return {
11464
+ title: "Promemoria in background",
11465
+ description: "Continua a ricevere gli avvisi quando Android sospende la pagina, tramite una connessione persistente disattivabile.",
11466
+ enabled: "Disattiva promemoria",
11467
+ disabled: "Attiva promemoria",
11468
+ busy: "Operazione…",
11469
+ error: "Impossibile aggiornare i promemoria; controlla il permesso notifiche e la connessione attiva."
11470
+ };
11471
+ return {
11472
+ title: "Background task reminders",
11473
+ description: "Keep receiving completion alerts after Android suspends the page, through a dismissible persistent connection.",
11474
+ enabled: "Disable background reminders",
11475
+ disabled: "Enable background reminders",
11476
+ busy: "Working…",
11477
+ error: "Could not update background reminders. Check notification permission and the active connection."
11478
+ };
11479
+ }
11480
+ /** Android-only opt-in for the foreground SSE fallback when the WebView is suspended. */
11481
+ function MobileBackgroundNotificationRow() {
11482
+ const [supported, setSupported] = (0, react.useState)(false);
11483
+ const [enabled, setEnabled] = (0, react.useState)(false);
11484
+ const [busy, setBusy] = (0, react.useState)(false);
11485
+ const [error, setError] = (0, react.useState)(false);
11486
+ const generation = (0, react.useRef)(0);
11487
+ (0, react.useEffect)(() => {
11488
+ let active = true;
11489
+ const refresh = () => {
11490
+ const attempt = ++generation.current;
11491
+ const bridge = window.__DSH_MOBILE_NATIVE__;
11492
+ if (bridge === void 0) {
11493
+ if (active) {
11494
+ setSupported(false);
11495
+ setError(false);
11496
+ }
11497
+ return;
11498
+ }
11499
+ Promise.resolve(bridge.capabilities()).then(async (capabilities) => {
11500
+ const available = capabilities.includes("notification.background.get") && capabilities.includes("notification.background.set");
11501
+ if (!active || attempt !== generation.current) return;
11502
+ setSupported(available);
11503
+ if (!available) return;
11504
+ const result = await bridge.invoke("notification.background.get", {});
11505
+ if (!active || attempt !== generation.current) return;
11506
+ if (typeof result?.enabled !== "boolean") throw new Error("invalid background notification status");
11507
+ setEnabled(result.enabled);
11508
+ setError(false);
11509
+ }).catch(() => {
11510
+ if (active && attempt === generation.current) {
11511
+ setSupported(true);
11512
+ setError(true);
11513
+ }
11514
+ });
11515
+ };
11516
+ refresh();
11517
+ window.addEventListener("dsh-mobile-native-ready", refresh);
11518
+ return () => {
11519
+ active = false;
11520
+ generation.current += 1;
11521
+ window.removeEventListener("dsh-mobile-native-ready", refresh);
11522
+ };
11523
+ }, []);
11524
+ if (!supported) return null;
11525
+ const locale = selectedMobileControlLocale();
11526
+ const copy = mobileBackgroundNotificationCopy(locale);
11527
+ const toggle = () => {
11528
+ const bridge = window.__DSH_MOBILE_NATIVE__;
11529
+ if (bridge === void 0 || busy) return;
11530
+ const attempt = ++generation.current;
11531
+ setBusy(true);
11532
+ setError(false);
11533
+ bridge.invoke("notification.background.set", { enabled: !enabled }).then((result) => {
11534
+ if (attempt !== generation.current) return;
11535
+ const next = result?.enabled;
11536
+ if (typeof next !== "boolean") throw new Error("invalid background notification status");
11537
+ setEnabled(next);
11538
+ }).catch(() => {
11539
+ if (attempt === generation.current) setError(true);
11540
+ }).finally(() => {
11541
+ if (attempt === generation.current) setBusy(false);
11542
+ });
11543
+ };
11544
+ return (0, react.createElement)("div", {
11545
+ className: "dsh-mobile-settings_row",
11546
+ lang: locale
11547
+ }, (0, react.createElement)("div", { className: "dsh-mobile-settings_rowText" }, (0, react.createElement)("div", { className: "dsh-mobile-settings_title" }, copy.title), (0, react.createElement)("div", {
11548
+ className: "dsh-mobile-settings_desc",
11549
+ "aria-live": "polite"
11550
+ }, error ? copy.error : copy.description)), (0, react.createElement)("button", {
11551
+ type: "button",
11552
+ className: "dsh-mobile-settings_selector",
11553
+ disabled: busy,
11554
+ "aria-busy": busy ? "true" : void 0,
11555
+ "aria-pressed": enabled ? "true" : "false",
11556
+ onClick: toggle
11557
+ }, busy ? copy.busy : enabled ? copy.enabled : copy.disabled));
11558
+ }
11391
11559
  const CONTROL_REQUEST_TIMEOUT_MS = 15e3;
11392
11560
  const LONG_CONTROL_REQUEST_TIMEOUT_MS = 21e4;
11393
11561
  const GITHUB_RELEASES_URL = "https://github.com/saya-ch/dsh-mobile/releases";
@@ -11617,6 +11785,7 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
11617
11785
  frp_exited: "frpExited",
11618
11786
  iroh_companion_missing: "irohCompanionMissing",
11619
11787
  iroh_data_directory_invalid: "irohDataDirectoryInvalid",
11788
+ iroh_data_directory_insecure: "irohDataDirectoryInsecure",
11620
11789
  iroh_sidecar_invalid: "irohSidecarInvalid",
11621
11790
  iroh_download_failed: "irohDownloadFailed",
11622
11791
  iroh_download_size_mismatch: "irohDownloadFailed",
@@ -12950,6 +13119,7 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
12950
13119
  remoteStatus.setAttribute("aria-live", "polite");
12951
13120
  const remoteGuide = element("section", "dsh-mobile-control__guide");
12952
13121
  remoteGuide.hidden = true;
13122
+ remoteGuide.dataset.dshMobileGuide = "funnel";
12953
13123
  remoteGuide.setAttribute("aria-label", t("funnelGuideAria"));
12954
13124
  const remoteGuideTitle = element("h3", "dsh-mobile-control__guide-title");
12955
13125
  remoteGuideTitle.textContent = t("funnelGuideTitle");
@@ -12976,6 +13146,26 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
12976
13146
  remoteSetupRetry.textContent = t("retryNow");
12977
13147
  remoteGuideActions.append(remoteSetup, remoteSetupRetry);
12978
13148
  remoteGuide.append(remoteGuideTitle, remoteGuideSummary, remoteGuideSteps, remoteGuideNote, remoteGuideActions);
13149
+ const remotePermission = element("section", "dsh-mobile-control__guide");
13150
+ remotePermission.hidden = true;
13151
+ remotePermission.dataset.dshMobileGuide = "mac-file-access";
13152
+ remotePermission.setAttribute("aria-label", t("macPermissionTitle"));
13153
+ remotePermission.setAttribute("role", "status");
13154
+ const remotePermissionTitle = element("h3", "dsh-mobile-control__guide-title");
13155
+ remotePermissionTitle.textContent = t("macPermissionTitle");
13156
+ const remotePermissionSummary = element("p", "dsh-mobile-control__guide-summary");
13157
+ remotePermissionSummary.textContent = t("macPermissionSummary");
13158
+ const remotePermissionSteps = element("p", "dsh-mobile-control__guide-note");
13159
+ remotePermissionSteps.textContent = t("macPermissionSteps");
13160
+ const remotePermissionActions = element("div", "dsh-mobile-control__guide-actions");
13161
+ const remotePermissionOpen = element("button", "dsh-mobile-control__primary");
13162
+ remotePermissionOpen.type = "button";
13163
+ remotePermissionOpen.textContent = t("macPermissionOpen");
13164
+ const remotePermissionDismissButton = element("button", "dsh-mobile-control__secondary");
13165
+ remotePermissionDismissButton.type = "button";
13166
+ remotePermissionDismissButton.textContent = t("macPermissionDismiss");
13167
+ remotePermissionActions.append(remotePermissionOpen, remotePermissionDismissButton);
13168
+ remotePermission.append(remotePermissionTitle, remotePermissionSummary, remotePermissionSteps, remotePermissionActions);
12979
13169
  const remoteActions = element("div", "dsh-mobile-control__actions");
12980
13170
  const remoteToggle = element("button", "dsh-mobile-control__primary");
12981
13171
  remoteToggle.type = "button";
@@ -13224,7 +13414,7 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
13224
13414
  macosFolderHelpBody.append(macosFolderHelpNote);
13225
13415
  macosFolderHelp.append(macosFolderHelpSummary, macosFolderHelpBody);
13226
13416
  setupDetailsRow.append(macosFolderHelp);
13227
- remoteWorkspace.append(remoteStatus, remoteAccess, remoteGuide, providerSetupBody, remoteQr, pairingValidity, providerInstallRow, remoteActions, remotePairLink, setupDetailsRow);
13417
+ remoteWorkspace.append(remoteStatus, remoteAccess, remoteGuide, remotePermission, providerSetupBody, remoteQr, pairingValidity, providerInstallRow, remoteActions, remotePairLink, setupDetailsRow);
13228
13418
  const diagnosticsView = element("div", "dsh-mobile-control__view is-diagnostics");
13229
13419
  diagnosticsView.hidden = true;
13230
13420
  const devicesView = element("div", "dsh-mobile-control__view is-devices");
@@ -13323,6 +13513,7 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
13323
13513
  let remoteLoginUrl = "";
13324
13514
  let remoteSetupUrl = "";
13325
13515
  let remoteSetupPending = false;
13516
+ let remotePermissionDismissed = false;
13326
13517
  let remoteSetupOpenedAt = 0;
13327
13518
  let remoteReconnectBusy = false;
13328
13519
  let remoteProviderBusy = false;
@@ -14202,6 +14393,8 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
14202
14393
  const needsFunnelSetup = state === "error" && remoteSetupUrl !== "";
14203
14394
  remoteReady = remoteRunning && state === "ready" && remoteOrigin !== "";
14204
14395
  macosFolderHelp.hidden = data.macosHost !== true || !remoteReady;
14396
+ const macFileAccess = data.macFileAccess === "missing" ? "missing" : data.macFileAccess === "ok" ? "ok" : void 0;
14397
+ remotePermission.hidden = macFileAccess !== "missing" || remotePermissionDismissed || !remoteReady;
14205
14398
  frpChoiceBadge.textContent = frpConfigured && frpInstalled ? t("ready") : t("advanced");
14206
14399
  originChoiceBadge.textContent = managedOrigin ? t(remoteReady ? "ready" : "advanced") : originListening ? t("originBackendReady") : t("advanced");
14207
14400
  if (remoteStatus.style) remoteStatus.style.display = state === "off" && remoteFailureText === "" ? "none" : "";
@@ -15620,6 +15813,13 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
15620
15813
  remoteSetupPending = false;
15621
15814
  reconnectRemote();
15622
15815
  });
15816
+ remotePermissionOpen.addEventListener("click", () => {
15817
+ controlRequestJson("/api/mobile-access/remote/macos-file-access/open-settings", { method: "POST" }).catch(() => {});
15818
+ });
15819
+ remotePermissionDismissButton.addEventListener("click", () => {
15820
+ remotePermissionDismissed = true;
15821
+ remotePermission.hidden = true;
15822
+ });
15623
15823
  remoteSetup.addEventListener("click", () => {
15624
15824
  if (remoteSetupUrl === "") return;
15625
15825
  remoteSetupPending = true;
@@ -17196,7 +17396,8 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
17196
17396
  kind: "done",
17197
17397
  title: label("Attività completata", "Task finished", "任务已完成"),
17198
17398
  body: label("Il tuo task DSH è terminato", "Your DSH task finished", "你的 DSH 任务已完成"),
17199
- tag: taskCompletionTag(parsed.sessionId, parsed.turn)
17399
+ tag: taskCompletionTag(parsed.sessionId, parsed.turn),
17400
+ navigate: taskNotificationNavigate(parsed.sessionId)
17200
17401
  });
17201
17402
  }, () => {
17202
17403
  fireDeviceRevoked();
@@ -17415,6 +17616,46 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
17415
17616
  page.body.append(node);
17416
17617
  view.setTimeout(() => node.remove(), 3e3);
17417
17618
  }
17619
+ /** Consume the Android background service's opaque Session route on cold or warm launch. */
17620
+ function installTaskNotificationNavigation(ctx, page, view) {
17621
+ if (view.__DSH_MOBILE_FRONTEND__ !== "dedicated" || ctx.inject === void 0) return () => void 0;
17622
+ if (new URLSearchParams(view.location.search).get("dsh-session") === null) return () => void 0;
17623
+ let closed = false;
17624
+ let removeNavigation;
17625
+ const fiber = ctx.inject(mobilePushNavigationServices, (navigationContext) => {
17626
+ if (closed) return () => void 0;
17627
+ if (new URLSearchParams(view.location.search).get("dsh-session") === null) return () => void 0;
17628
+ const remove = installPushNavigation(navigationContext, {
17629
+ parameter: "dsh-session",
17630
+ resolve: async (value) => {
17631
+ const sessionId = decodeTaskNotificationNavigate(value);
17632
+ return sessionId === void 0 ? void 0 : {
17633
+ sessionId,
17634
+ interaction: "current"
17635
+ };
17636
+ },
17637
+ notify: (key) => {
17638
+ if (key === "session-unavailable" || key === "session-resolved") showMobilePushNavigationNotice(page, view, key);
17639
+ },
17640
+ browser: {
17641
+ location: view.location,
17642
+ history: view.history
17643
+ }
17644
+ });
17645
+ removeNavigation = remove;
17646
+ return () => {
17647
+ if (removeNavigation === remove) removeNavigation = void 0;
17648
+ remove();
17649
+ };
17650
+ });
17651
+ return () => {
17652
+ if (closed) return;
17653
+ closed = true;
17654
+ removeNavigation?.();
17655
+ removeNavigation = void 0;
17656
+ fiber.dispose();
17657
+ };
17658
+ }
17418
17659
  /** Own the browser-only push controller, settings row, and optional authenticated tap navigation. */
17419
17660
  function installMobilePushBrowserClient(ctx, runtime) {
17420
17661
  const { page, view } = runtime;
@@ -17526,6 +17767,7 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
17526
17767
  page: document,
17527
17768
  view: window
17528
17769
  }), "dsh-mobile: authenticated browser push lifecycle");
17770
+ ctx.effect(() => installTaskNotificationNavigation(ctx, document, window), "dsh-mobile: native task notification navigation");
17529
17771
  ctx.effect(() => {
17530
17772
  const desktopAdmin = isDesktopAdminSurface(location.hostname, location.search, window.__DSH_MOBILE_FRONTEND__, location.protocol);
17531
17773
  const gatewayCustomization = ownsMobileCustomization(document.querySelector("meta[name=\"dsh-mobile-gateway\"][content=\"authenticated\"]") !== null, window.__DSH_MOBILE_FRONTEND__);
@@ -17583,7 +17825,13 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
17583
17825
  id: "dsh-mobile-task-notifications",
17584
17826
  order: 110
17585
17827
  }, MobileTaskNotificationRow);
17828
+ const removeBackgroundNotifications = ctx.slots.register({
17829
+ name: "settings.general.item",
17830
+ id: "dsh-mobile-background-notifications",
17831
+ order: 111
17832
+ }, MobileBackgroundNotificationRow);
17586
17833
  return () => {
17834
+ removeBackgroundNotifications();
17587
17835
  removeTaskNotifications();
17588
17836
  removeSwitchComputer();
17589
17837
  removeDisplayScale();
@@ -17696,6 +17944,7 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
17696
17944
  exports.installDshLanguageBoundSurface = installDshLanguageBoundSurface;
17697
17945
  exports.installMobileNativeBack = installMobileNativeBack;
17698
17946
  exports.installMobilePushBrowserClient = installMobilePushBrowserClient;
17947
+ exports.installTaskNotificationNavigation = installTaskNotificationNavigation;
17699
17948
  exports.localizedDiagnosticEntryCopy = localizedDiagnosticEntryCopy;
17700
17949
  exports.mobileBackBlockedMessage = mobileBackBlockedMessage;
17701
17950
  exports.mobilePushNavigationNotice = mobilePushNavigationNotice;