dsh-mobile 0.3.16 → 0.4.1

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
@@ -39,6 +39,7 @@ window.__ModuleLoader__.load({
39
39
  clearAllDevices: "Clear all devices",
40
40
  pairingQr: "Pairing QR code",
41
41
  remoteIntro: "Choose the remote channel that suits you. Switching or disabling remote access does not affect the local network.",
42
+ httpFrameWarning: "HTTP pages may be embedded for compatibility, but their contents are unencrypted and can be altered. Use HTTPS for sensitive work.",
42
43
  chooseProvider: "Choose connection method",
43
44
  currentProvider: "Current connection",
44
45
  remoteStateOff: "Off",
@@ -83,6 +84,7 @@ window.__ModuleLoader__.load({
83
84
  continueLogin: "Continue login",
84
85
  reconnect: "Reconnect",
85
86
  generateRemoteQr: "Generate remote pairing QR code",
87
+ copyRemoteLink: "Copy remote pairing link",
86
88
  manageRemoteDevices: "Manage remote devices",
87
89
  resetRemoteLogin: "Sign out and clear remote login",
88
90
  resetRemoteDevices: "Disable and clear remote devices",
@@ -206,6 +208,7 @@ window.__ModuleLoader__.load({
206
208
  clearAllDevices: "Rimuovi tutti i dispositivi",
207
209
  pairingQr: "Codice QR di abbinamento",
208
210
  remoteIntro: "Scegli il canale remoto più adatto. Cambiare o disattivare l’accesso remoto non influisce sulla rete locale.",
211
+ httpFrameWarning: "Le pagine HTTP possono essere incorporate per compatibilità, ma i contenuti non sono cifrati e possono essere alterati. Usa HTTPS per le attività sensibili.",
209
212
  chooseProvider: "Scegli metodo di connessione",
210
213
  currentProvider: "Connessione attuale",
211
214
  remoteStateOff: "Disattivata",
@@ -250,6 +253,7 @@ window.__ModuleLoader__.load({
250
253
  continueLogin: "Continua accesso",
251
254
  reconnect: "Riconnetti",
252
255
  generateRemoteQr: "Genera QR di abbinamento remoto",
256
+ copyRemoteLink: "Copia link di abbinamento remoto",
253
257
  manageRemoteDevices: "Gestisci dispositivi remoti",
254
258
  resetRemoteLogin: "Esci e rimuovi accesso remoto",
255
259
  resetRemoteDevices: "Disattiva e rimuovi dispositivi remoti",
@@ -375,6 +379,7 @@ window.__ModuleLoader__.load({
375
379
  clearAllDevices: "清除所有设备",
376
380
  pairingQr: "配对二维码",
377
381
  remoteIntro: "选择更适合你的远程通道。切换或关闭远程访问不会影响局域网。",
382
+ httpFrameWarning: "为兼容性允许嵌入 HTTP 页面,但内容未加密,可能被篡改;处理敏感内容请使用 HTTPS。",
378
383
  chooseProvider: "选择连接方式",
379
384
  currentProvider: "当前连接",
380
385
  remoteStateOff: "未启用",
@@ -419,6 +424,7 @@ window.__ModuleLoader__.load({
419
424
  continueLogin: "继续登录",
420
425
  reconnect: "重新连接",
421
426
  generateRemoteQr: "生成远程配对二维码",
427
+ copyRemoteLink: "复制远程配对链接",
422
428
  manageRemoteDevices: "管理远程设备",
423
429
  resetRemoteLogin: "退出并清除远程登录",
424
430
  resetRemoteDevices: "关闭并清除远程设备",
@@ -510,6 +516,51 @@ window.__ModuleLoader__.load({
510
516
  wsPathsAnnounce: "{n} 条插件连接等待放行",
511
517
  wsPathsAdvancedHint: "通常不需要手动填写:用上方按钮即可。只有当你确切知道所需路径时才填。"
512
518
  });
519
+ Object.assign(MOBILE_CONTROL_MESSAGES.en, {
520
+ lanSetupTitle: "Finish local network setup",
521
+ lanSetupIntro: "Choose the network shared by this computer and your phone. DSH Mobile creates private certificates only after you confirm.",
522
+ lanSetupNetwork: "Computer network",
523
+ lanSetupChooseNetwork: "Choose a network",
524
+ lanSetupRecommended: "Recommended",
525
+ lanSetupNoNetworks: "No private Wi-Fi or Ethernet address was found. Connect this computer to the local network, then scan again.",
526
+ lanSetupRefresh: "Scan again",
527
+ lanSetupFirewallNote: "Windows may request administrator approval for two inbound rules limited to the local subnet.",
528
+ lanSetupConfigure: "Configure local network",
529
+ lanSetupConfiguring: "Creating private certificates and local network rules…",
530
+ lanSetupRequired: "Local network setup has not been completed. Choose a network above to continue.",
531
+ lanSetupRestart: "Setup complete for {origin}. Restart DSH once to start local network access.",
532
+ lanSetupFailed: "Setup was not completed. Confirm the network selection and approve the Windows prompt, then try again."
533
+ });
534
+ Object.assign(MOBILE_CONTROL_MESSAGES.it, {
535
+ lanSetupTitle: "Completa la configurazione della rete locale",
536
+ lanSetupIntro: "Scegli la rete condivisa da questo computer e dal telefono. DSH Mobile crea certificati privati solo dopo la conferma.",
537
+ lanSetupNetwork: "Rete del computer",
538
+ lanSetupChooseNetwork: "Scegli una rete",
539
+ lanSetupRecommended: "Consigliata",
540
+ lanSetupNoNetworks: "Nessun indirizzo Wi-Fi o Ethernet privato trovato. Collega il computer alla rete locale e ripeti la ricerca.",
541
+ lanSetupRefresh: "Cerca di nuovo",
542
+ lanSetupFirewallNote: "Windows può richiedere l’approvazione amministratore per due regole in ingresso limitate alla sottorete locale.",
543
+ lanSetupConfigure: "Configura rete locale",
544
+ lanSetupConfiguring: "Creazione dei certificati privati e delle regole di rete locale…",
545
+ lanSetupRequired: "La configurazione della rete locale non è completa. Scegli una rete qui sopra per continuare.",
546
+ lanSetupRestart: "Configurazione completata per {origin}. Riavvia DSH una volta per attivare l’accesso locale.",
547
+ lanSetupFailed: "Configurazione non completata. Controlla la rete e approva la richiesta di Windows, quindi riprova."
548
+ });
549
+ Object.assign(MOBILE_CONTROL_MESSAGES.zh, {
550
+ lanSetupTitle: "完成局域网配置",
551
+ lanSetupIntro: "选择电脑与手机共同使用的网络。确认后才会生成私有证书,不会在安装时自动开放端口。",
552
+ lanSetupNetwork: "电脑当前网络",
553
+ lanSetupChooseNetwork: "请选择网络",
554
+ lanSetupRecommended: "推荐",
555
+ lanSetupNoNetworks: "未找到可用的私有 Wi-Fi 或以太网地址。请先让电脑接入局域网,然后重新扫描。",
556
+ lanSetupRefresh: "重新扫描",
557
+ lanSetupFirewallNote: "Windows 可能请求管理员授权,仅创建两条限本地子网访问的入站规则。",
558
+ lanSetupConfigure: "完成配置",
559
+ lanSetupConfiguring: "正在生成私有证书并配置局域网规则…",
560
+ lanSetupRequired: "尚未完成局域网配置,请选择上方网络继续。",
561
+ lanSetupRestart: "已为 {origin} 完成配置。请重启一次 DSH,局域网访问会自动开启。",
562
+ lanSetupFailed: "配置未完成。请确认网卡选择并允许 Windows 授权提示,然后重试。"
563
+ });
513
564
  Object.assign(MOBILE_CONTROL_MESSAGES.en, {
514
565
  selfHostedConnections: "Self-hosted connection",
515
566
  selfHostedDescription: "For users with a VPS and domain. It does not depend on public tunnel bandwidth.",
@@ -966,12 +1017,32 @@ window.__ModuleLoader__.load({
966
1017
  generated: "Generato"
967
1018
  },
968
1019
  zh: {
1020
+ versions: "已显示插件、DSH 和 Android App 最低版本。",
1021
+ networkOk: "已配置的局域网网卡可用。",
1022
+ networkError: "已保存的局域网网卡不可用。",
1023
+ networkInfo: "当前使用固定局域网配置。",
1024
+ networkAction: "重新运行 dsh-mobile setup。",
1025
+ lanOk: "局域网网关正在监听,配对入口可用。",
1026
+ lanInfo: "局域网访问当前未开启。",
1027
+ lanAction: "需要手机直连时开启局域网访问。",
1028
+ firewallOk: "局域网 TCP 与发现防火墙规则已启用。",
1029
+ firewallWarning: "未找到完整的局域网防火墙规则。",
1030
+ firewallInfo: "系统未允许插件读取防火墙状态。",
1031
+ firewallAction: "以管理员身份运行 dsh-mobile setup 并检查防火墙规则。",
1032
+ remoteOk: "远程公网地址已通过可达性检查。",
1033
+ remoteWarning: "远程通道需要处理或仍在连接。",
1034
+ remoteError: "远程连接当前不可达。",
1035
+ remoteInfo: "远程访问当前未开启。",
1036
+ remoteAction: "返回远程访问,按提供方提示操作并重新连接。",
1037
+ phone: "电脑无法判断路由器是否隔离了手机。",
1038
+ phoneAction: "确认手机与电脑使用同一网络,并关闭访客网络或 AP 隔离。",
969
1039
  reportTitle: "DSH Mobile 诊断报告",
970
1040
  generated: "生成时间"
971
1041
  }
972
1042
  };
973
1043
  const DIAGNOSTIC_REASON_MESSAGES = {
974
1044
  en: {
1045
+ "lan-setup-required": ["Local network setup has not been completed.", "Return to Local network and choose a network to finish setup."],
975
1046
  "versions-current": ["Installed plugin, DSH, and minimum Android app versions are shown.", ""],
976
1047
  "network-unavailable": ["The saved local network adapter is unavailable.", "Run dsh-mobile setup again."],
977
1048
  "network-interface": ["Using network interface {interfaceName}.", ""],
@@ -992,6 +1063,7 @@ window.__ModuleLoader__.load({
992
1063
  "phone-network-unknown": ["The computer cannot determine whether the router isolates the phone.", "Confirm the phone and computer use the same network, then disable guest-network or AP isolation."]
993
1064
  },
994
1065
  it: {
1066
+ "lan-setup-required": ["La configurazione della rete locale non è completa.", "Torna a Rete locale e scegli una rete per completare la configurazione."],
995
1067
  "versions-current": ["Sono indicate le versioni installate di plugin e DSH e la versione minima dell’app Android.", ""],
996
1068
  "network-unavailable": ["La scheda di rete locale salvata non è disponibile.", "Esegui di nuovo dsh-mobile setup."],
997
1069
  "network-interface": ["È in uso l’interfaccia di rete {interfaceName}.", ""],
@@ -1012,6 +1084,7 @@ window.__ModuleLoader__.load({
1012
1084
  "phone-network-unknown": ["Il computer non può stabilire se il router isola il telefono.", "Verifica che telefono e computer usino la stessa rete, poi disattiva rete ospiti o isolamento AP."]
1013
1085
  },
1014
1086
  zh: {
1087
+ "lan-setup-required": ["尚未完成局域网初始化。", "返回局域网页选择网卡并完成配置。"],
1015
1088
  "versions-current": ["已显示插件、DSH 和 Android App 最低版本。", ""],
1016
1089
  "network-unavailable": ["已保存的局域网网卡当前不可用。", "重新运行 dsh-mobile setup。"],
1017
1090
  "network-interface": ["正在使用网卡 {interfaceName}。", ""],
@@ -1129,6 +1202,304 @@ window.__ModuleLoader__.load({
1129
1202
  if (publicIpv4Address(publicHost)) lines.push(manualIpCertificateGuide(publicHost), "");
1130
1203
  return lines.join("\n");
1131
1204
  }
1205
+ /** Wire bounds mirrored by the Android notification policy. */
1206
+ const TASK_NOTIFY_LIMITS = Object.freeze({
1207
+ title: 80,
1208
+ body: 200,
1209
+ tag: 64
1210
+ });
1211
+ const MAX_SEEN_QUESTION_KEYS = 20;
1212
+ function sanitizeTagFragment(value) {
1213
+ return value.toLowerCase().replaceAll(/[^a-z0-9-]+/gu, "-").replaceAll(/^-+|-+$/gu, "").slice(0, TASK_NOTIFY_LIMITS.tag);
1214
+ }
1215
+ /** Stable per-turn tag so concurrent sessions keep separate notifications. */
1216
+ function taskCompletionTag(sessionId, turn) {
1217
+ return `dsh-task-done-${sanitizeTagFragment(`${sessionId}-${String(turn)}`) || "task"}`.slice(0, TASK_NOTIFY_LIMITS.tag);
1218
+ }
1219
+ /** Decide whether the current snapshot deserves a system notification. */
1220
+ var TaskNotifyTracker = class {
1221
+ lastActivityAt;
1222
+ notifiedDoneAt;
1223
+ wasBusy = false;
1224
+ busySince;
1225
+ idleAnchoredAt;
1226
+ seenQuestionKeys = /* @__PURE__ */ new Set();
1227
+ quietMs;
1228
+ graceMs;
1229
+ minBusyMs;
1230
+ now;
1231
+ format;
1232
+ constructor(options) {
1233
+ this.quietMs = options.quietMs ?? 9e4;
1234
+ this.graceMs = options.graceMs ?? 1e4;
1235
+ this.minBusyMs = options.minBusyMs ?? 2e4;
1236
+ this.now = options.now ?? Date.now;
1237
+ this.format = options.format;
1238
+ }
1239
+ /** Record assistant-side activity; re-arms completion after a notification. */
1240
+ markActivity() {
1241
+ const now = this.now();
1242
+ this.lastActivityAt = now;
1243
+ if (this.notifiedDoneAt !== void 0 && this.notifiedDoneAt <= now) this.notifiedDoneAt = void 0;
1244
+ }
1245
+ /**
1246
+ * Milliseconds until a pending completion anchor fires, or undefined when
1247
+ * no anchor exists. Lets the watcher schedule one exact timer instead of
1248
+ * waiting for the next coarse poll tick.
1249
+ */
1250
+ pendingAnchorDelayMs() {
1251
+ if (this.idleAnchoredAt === void 0) return void 0;
1252
+ return Math.max(0, this.graceMs - (this.now() - this.idleAnchoredAt));
1253
+ }
1254
+ evaluate(snapshot) {
1255
+ const now = this.now();
1256
+ if (!this.wasBusy && snapshot.composerBusy) this.busySince = now;
1257
+ if (this.wasBusy && !snapshot.composerBusy) {
1258
+ const busyFor = this.busySince === void 0 ? 0 : now - this.busySince;
1259
+ this.busySince = void 0;
1260
+ if (snapshot.pageHidden && !snapshot.pendingQuestion && busyFor >= this.minBusyMs) {
1261
+ this.idleAnchoredAt = void 0;
1262
+ this.notifiedDoneAt = now;
1263
+ return this.event("done", snapshot.sessionLabel, void 0);
1264
+ }
1265
+ this.idleAnchoredAt = snapshot.pageHidden ? now : void 0;
1266
+ if (!snapshot.pageHidden) this.notifiedDoneAt = now;
1267
+ }
1268
+ this.wasBusy = snapshot.composerBusy;
1269
+ if (snapshot.composerBusy) this.idleAnchoredAt = void 0;
1270
+ else if (this.idleAnchoredAt !== void 0 && !snapshot.pageHidden) {
1271
+ this.idleAnchoredAt = void 0;
1272
+ this.notifiedDoneAt = now;
1273
+ }
1274
+ if (!snapshot.pageHidden) {
1275
+ if (snapshot.pendingQuestion && snapshot.questionKey !== void 0) this.rememberQuestionKey(snapshot.questionKey);
1276
+ return;
1277
+ }
1278
+ if (snapshot.pendingQuestion) {
1279
+ if (snapshot.questionKey === void 0 || this.seenQuestionKeys.has(snapshot.questionKey)) return void 0;
1280
+ this.rememberQuestionKey(snapshot.questionKey);
1281
+ return this.event("question", snapshot.sessionLabel, snapshot.questionKey);
1282
+ }
1283
+ if (this.idleAnchoredAt !== void 0) {
1284
+ if (now - this.idleAnchoredAt < this.graceMs) return void 0;
1285
+ this.idleAnchoredAt = void 0;
1286
+ this.notifiedDoneAt = now;
1287
+ return this.event("done", snapshot.sessionLabel, void 0);
1288
+ }
1289
+ if (this.lastActivityAt === void 0) return void 0;
1290
+ if (now - this.lastActivityAt < this.quietMs) return void 0;
1291
+ if (now - this.lastActivityAt > 18e5) {
1292
+ this.lastActivityAt = void 0;
1293
+ return;
1294
+ }
1295
+ if (this.notifiedDoneAt !== void 0 && this.notifiedDoneAt >= this.lastActivityAt) return void 0;
1296
+ this.notifiedDoneAt = now;
1297
+ return this.event("done", snapshot.sessionLabel, void 0);
1298
+ }
1299
+ rememberQuestionKey(key) {
1300
+ this.seenQuestionKeys.add(key);
1301
+ if (this.seenQuestionKeys.size > MAX_SEEN_QUESTION_KEYS) {
1302
+ const oldest = this.seenQuestionKeys.values().next();
1303
+ if (!oldest.done) this.seenQuestionKeys.delete(oldest.value);
1304
+ }
1305
+ }
1306
+ event(kind, sessionLabel, questionKey) {
1307
+ const text = this.format(kind, sessionLabel);
1308
+ const tag = kind === "question" && questionKey !== void 0 ? `dsh-task-question-${sanitizeTagFragment(questionKey) || "pending"}` : "dsh-task-done";
1309
+ return Object.freeze({
1310
+ kind,
1311
+ title: text.title,
1312
+ body: text.body,
1313
+ tag
1314
+ });
1315
+ }
1316
+ };
1317
+ /**
1318
+ * Mirror of the composer busy signal used for media actions: the run is busy
1319
+ * while the composer is aria-busy, disabled, read-only, or non-interactive.
1320
+ */
1321
+ function readComposerBusyState(root) {
1322
+ if (typeof root.querySelector !== "function") return false;
1323
+ const card = root.querySelector("[data-composer-card]");
1324
+ if (card === null || typeof card.getAttribute !== "function" || typeof card.querySelector !== "function") return false;
1325
+ if (card.getAttribute("aria-busy") === "true") return true;
1326
+ const field = card.querySelector("textarea,input,[contenteditable=\"true\"],[contenteditable=\"plaintext-only\"]");
1327
+ if (field === null) return false;
1328
+ return field.disabled === true || field.readOnly === true || field.getAttribute?.("aria-disabled") === "true";
1329
+ }
1330
+ /**
1331
+ * Best-effort session label for the notification body, bounded for the wire.
1332
+ * Callers accept that titles may name the active session on the lock screen.
1333
+ */
1334
+ function readSessionLabel(title) {
1335
+ if (typeof title !== "string") return "";
1336
+ return title.normalize("NFC").replace(/[\u0000-\u001f\u007f]+/gu, " ").trim().slice(0, 80);
1337
+ }
1338
+ /** Whether the document currently shows a card that waits for user input. */
1339
+ function hasPendingInputQuestion(root) {
1340
+ if (typeof root.querySelector !== "function") return false;
1341
+ return root.querySelector("[data-question-key],[data-plan-review-key]") !== null;
1342
+ }
1343
+ /** Stable key of the waiting input card, when the DOM exposes one. */
1344
+ function pendingInputQuestionKey(root) {
1345
+ if (typeof root.querySelector !== "function") return void 0;
1346
+ const card = root.querySelector("[data-question-key],[data-plan-review-key]");
1347
+ const getAttribute = card?.getAttribute;
1348
+ if (typeof getAttribute !== "function") return void 0;
1349
+ const key = getAttribute.call(card, "data-question-key") ?? getAttribute.call(card, "data-plan-review-key");
1350
+ if (key === null || key === "") return void 0;
1351
+ return key;
1352
+ }
1353
+ /** Observe assistant-side conversation mutations; returns a disposer. */
1354
+ function observeTaskActivity(target, onActivity, observe = typeof MutationObserver === "function" ? MutationObserver : void 0) {
1355
+ if (observe === void 0) return () => void 0;
1356
+ const observer = new observe(() => {
1357
+ onActivity();
1358
+ });
1359
+ try {
1360
+ observer.observe(target, {
1361
+ childList: true,
1362
+ characterData: true,
1363
+ subtree: true
1364
+ });
1365
+ } catch {
1366
+ return () => void 0;
1367
+ }
1368
+ return () => {
1369
+ observer.disconnect();
1370
+ };
1371
+ }
1372
+ function readNativeBridge() {
1373
+ if (typeof window === "undefined") return void 0;
1374
+ const bridge = window.__DSH_MOBILE_NATIVE__;
1375
+ if (bridge === void 0 || typeof bridge.capabilities !== "function" || typeof bridge.invoke !== "function") return void 0;
1376
+ return bridge;
1377
+ }
1378
+ /** Parse a `task-notify` SSE payload; rejects anything malformed or empty. */
1379
+ function parseTaskNotifyPayload(value) {
1380
+ let record;
1381
+ if (typeof value === "string") try {
1382
+ const parsed = JSON.parse(value);
1383
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return void 0;
1384
+ record = parsed;
1385
+ } catch {
1386
+ return;
1387
+ }
1388
+ else if (value !== null && typeof value === "object" && !Array.isArray(value)) record = value;
1389
+ else return void 0;
1390
+ if (typeof record.sessionId !== "string" || record.sessionId.trim() === "") return void 0;
1391
+ const turn = typeof record.turn === "number" && Number.isSafeInteger(record.turn) && record.turn >= 0 ? record.turn : 0;
1392
+ return Object.freeze({
1393
+ sessionId: record.sessionId.slice(0, 128),
1394
+ turn
1395
+ });
1396
+ }
1397
+ /** Parse the small authenticated event used when the Host revokes this device. */
1398
+ function isDeviceRevokedPayload(value) {
1399
+ let record;
1400
+ if (typeof value === "string") try {
1401
+ const parsed = JSON.parse(value);
1402
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return false;
1403
+ record = parsed;
1404
+ } catch {
1405
+ return false;
1406
+ }
1407
+ else if (value !== null && typeof value === "object" && !Array.isArray(value)) record = value;
1408
+ else return false;
1409
+ return record.reason === "device_revoked";
1410
+ }
1411
+ /** Tell the Android shell to transition the active record to the revoked state. */
1412
+ function fireDeviceRevoked() {
1413
+ const bridge = readNativeBridge();
1414
+ if (bridge === void 0) return false;
1415
+ bridge.invoke("device.revoked", {}).catch(() => void 0);
1416
+ return true;
1417
+ }
1418
+ /**
1419
+ * Deliver one notification through the native bridge when available.
1420
+ * Returns false when there is no bridge (desktop browsers keep their own
1421
+ * UI) so callers can fall back; a denial flips the module sticky flag via
1422
+ * onDenied for the session-lifetime nag guard.
1423
+ */
1424
+ function fireTaskNotifyEvent(event, onDenied) {
1425
+ const bridge = readNativeBridge();
1426
+ if (bridge === void 0) return false;
1427
+ Promise.resolve().then(() => bridge.capabilities()).then((capabilities) => {
1428
+ if (!capabilities.includes("notification.notify")) return;
1429
+ return bridge.invoke("notification.notify", {
1430
+ title: event.title,
1431
+ body: event.body,
1432
+ tag: event.tag
1433
+ });
1434
+ }).then(() => void 0, (error) => {
1435
+ const code = typeof error === "object" && error !== null && "code" in error ? String(error.code) : "";
1436
+ if ((code === "permission_denied" || code === "denied") && onDenied !== void 0) onDenied();
1437
+ });
1438
+ return true;
1439
+ }
1440
+ /**
1441
+ * Watch the rendered conversation for finished runs and waiting questions,
1442
+ * notifying through the native bridge while the page is hidden. Returns a
1443
+ * disposer. Silent when no native bridge exists (desktop browsers keep their
1444
+ * own UI) or after the user denies the permission.
1445
+ */
1446
+ function installTaskCompletionWatcher(options) {
1447
+ if (typeof document === "undefined" || typeof window === "undefined") return () => void 0;
1448
+ const tracker = new TaskNotifyTracker({ format: options.label });
1449
+ const completionFallback = options.completionFallback ?? true;
1450
+ let notifyBlocked = false;
1451
+ let timer = 0;
1452
+ let anchorTimer = 0;
1453
+ const fire = (event) => {
1454
+ if (notifyBlocked) return;
1455
+ if (!fireTaskNotifyEvent(event, () => {
1456
+ notifyBlocked = true;
1457
+ })) return;
1458
+ };
1459
+ const evaluateNow = () => {
1460
+ const busy = completionFallback && readComposerBusyState(document);
1461
+ const event = tracker.evaluate({
1462
+ pageHidden: document.hidden,
1463
+ pendingQuestion: hasPendingInputQuestion(document),
1464
+ questionKey: pendingInputQuestionKey(document),
1465
+ sessionLabel: readSessionLabel(document.title),
1466
+ composerBusy: busy
1467
+ });
1468
+ if (event !== void 0) fire(event);
1469
+ scheduleAnchorTimer();
1470
+ };
1471
+ const scheduleAnchorTimer = () => {
1472
+ const delay = tracker.pendingAnchorDelayMs();
1473
+ if (delay === void 0) {
1474
+ if (anchorTimer !== 0) {
1475
+ window.clearTimeout(anchorTimer);
1476
+ anchorTimer = 0;
1477
+ }
1478
+ return;
1479
+ }
1480
+ if (anchorTimer !== 0) return;
1481
+ anchorTimer = window.setTimeout(() => {
1482
+ anchorTimer = 0;
1483
+ evaluateNow();
1484
+ }, delay);
1485
+ };
1486
+ const disposeObservation = observeTaskActivity(document.documentElement, () => {
1487
+ if (completionFallback) tracker.markActivity();
1488
+ evaluateNow();
1489
+ });
1490
+ const onVisibility = () => {
1491
+ evaluateNow();
1492
+ };
1493
+ document.addEventListener("visibilitychange", onVisibility);
1494
+ if (typeof window.setInterval === "function") timer = window.setInterval(evaluateNow, 15e3);
1495
+ evaluateNow();
1496
+ return () => {
1497
+ disposeObservation();
1498
+ document.removeEventListener("visibilitychange", onVisibility);
1499
+ if (timer !== 0) window.clearInterval(timer);
1500
+ if (anchorTimer !== 0) window.clearTimeout(anchorTimer);
1501
+ };
1502
+ }
1132
1503
  //#endregion
1133
1504
  //#region src/native-mobile.ts
1134
1505
  /**
@@ -1148,7 +1519,14 @@ window.__ModuleLoader__.load({
1148
1519
  still collapsed the sidebar. Keep the neutral state explicitly invisible —
1149
1520
  the query restores the fixed scrim, and its more specific [hidden] rule
1150
1521
  keeps winning there. */
1151
- .dsh-native-mobile-backdrop,.dsh-mobile-branch-toast,.dsh-mobile-media-toast { display:none; }
1522
+ .dsh-native-mobile-backdrop,.dsh-mobile-branch-toast,.dsh-mobile-media-toast { display:none; }
1523
+ .dsh-mobile-settings_row { display:flex; align-items:center; gap:8px; padding:16px 0; border-bottom:0.5px solid var(--dsw-alias-border-l2); }
1524
+ .dsh-mobile-settings_rowText { flex:1; min-width:0; display:flex; flex-direction:column; gap:4px; padding-right:48px; }
1525
+ .dsh-mobile-settings_title { color:var(--dsw-alias-label-primary); font-size:14px; font-weight:400; line-height:22px; }
1526
+ .dsh-mobile-settings_desc { color:var(--dsw-alias-label-tertiary); font-size:12px; font-weight:400; line-height:18px; }
1527
+ .dsh-mobile-settings_selector { display:inline-flex; align-items:center; justify-content:center; min-width:max-content; height:36px; padding:0 14px; border:0; border-radius:18px; background:var(--dsw-alias-bg-module-platform); color:var(--dsw-alias-label-primary); font:inherit; font-size:14px; line-height:22px; cursor:pointer; }
1528
+ .dsh-mobile-settings_selector:hover:not(:disabled) { background:var(--dsw-alias-interactive-bg-hover); }
1529
+ .dsh-mobile-settings_selector:disabled { cursor:wait; opacity:.55; }
1152
1530
  @media ${NATIVE_MOBILE_OVERLAY_QUERY} {
1153
1531
  html.dsh-native-mobile-active,html.dsh-native-mobile-active body { width:100%; height:100%; overflow:hidden; }
1154
1532
  html.dsh-native-mobile-active { --dsh-mobile-motion-duration:200ms; --dsh-mobile-motion-ease:cubic-bezier(.22,1,.36,1); }
@@ -1191,8 +1569,12 @@ window.__ModuleLoader__.load({
1191
1569
  [data-dsh-mobile-sidebar][data-open="false"] [data-dsh-mobile-sidebar-root] { width:0 !important; border:0 !important; background:transparent !important; box-shadow:none !important; overflow:visible !important; }
1192
1570
  [data-dsh-mobile-sidebar][data-open="false"] [data-dsh-mobile-sidebar-root] > :not(:has([data-dsh-mobile-toggle])) { display:none !important; }
1193
1571
  [data-dsh-mobile-sidebar][data-open="false"] [data-dsh-mobile-sidebar-root] > :has([data-dsh-mobile-toggle]) { position:fixed !important; z-index:244 !important; top:env(safe-area-inset-top) !important; left:0 !important; box-sizing:border-box !important; width:50px !important; height:52px !important; padding:4px !important; border:0 !important; background:transparent !important; }
1194
- [data-dsh-mobile-sidebar][data-open="false"] [data-dsh-mobile-sidebar-root] > :has([data-dsh-mobile-toggle]) > :not([data-dsh-mobile-toggle]) { display:none !important; }
1195
- [data-dsh-mobile-toggle] { width:44px !important; height:44px !important; min-width:44px !important; min-height:44px !important; }
1572
+ [data-dsh-mobile-sidebar][data-open="false"] [data-dsh-mobile-sidebar-root] > :has([data-dsh-mobile-toggle]) > :not([data-dsh-mobile-toggle]) { display:none !important; }
1573
+ /* The stock settings shell is mounted under the collapsed sidebar. Let
1574
+ its fixed overlay escape the zero-width drawer while it is open. */
1575
+ [data-dsh-mobile-sidebar]:has([data-dsh-mobile-settings]) { width:100vw !important; inset:0 !important; }
1576
+ [data-dsh-mobile-sidebar]:has([data-dsh-mobile-settings]) > * { width:100vw !important; overflow:visible !important; }
1577
+ [data-dsh-mobile-toggle] { width:44px !important; height:44px !important; min-width:44px !important; min-height:44px !important; }
1196
1578
  [data-dsh-mobile-sidebar][data-open="false"] [data-dsh-mobile-toggle] > svg[class*="_railFish"] { transform:translateY(-4px) !important; }
1197
1579
  .dsh-native-mobile-backdrop { display:block; position:fixed; z-index:235; inset:env(safe-area-inset-top) 0 0; border:0; background:rgb(15 23 42 / 32%); }
1198
1580
  .dsh-native-mobile-backdrop:not([hidden]) { animation:dsh-mobile-fade-in var(--dsh-mobile-motion-duration) ease-out; }
@@ -1208,11 +1590,11 @@ window.__ModuleLoader__.load({
1208
1590
  [data-dsh-mobile-settings-list] [class*="_navCell"] { flex:0 0 auto !important; min-width:max-content !important; height:44px !important; padding:10px 12px !important; }
1209
1591
  [data-dsh-mobile-settings-list] [aria-current="true"] { border-color:transparent !important; outline:0 !important; box-shadow:none !important; }
1210
1592
  [data-dsh-mobile-settings-content] { flex:1 1 auto !important; width:100% !important; min-height:0 !important; }
1211
- [data-dsh-mobile-settings-header] { height:48px !important; min-height:48px !important; padding:10px 12px 6px !important; }
1212
- [data-dsh-mobile-settings-header] [class*="_close"] { width:36px !important; height:36px !important; }
1213
- [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; }
1214
- [data-dsh-mobile-settings-options] > * { width:100% !important; min-width:0 !important; }
1215
- [data-dsh-mobile-settings-options] [data-slot="settings.general.item"] > [class*="_row"] { flex-direction:column !important; align-items:stretch !important; gap:12px !important; }
1593
+ [data-dsh-mobile-settings-header] { height:48px !important; min-height:48px !important; padding:10px 12px 6px !important; }
1594
+ [data-dsh-mobile-settings-header] [class*="_close"] { width:36px !important; height:36px !important; }
1595
+ [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; }
1596
+ [data-dsh-mobile-settings-options] > * { width:100% !important; min-width:0 !important; }
1597
+ [data-dsh-mobile-settings-options] [data-slot="settings.general.item"] > [class*="_row"] { flex-direction:column !important; align-items:stretch !important; gap:12px !important; }
1216
1598
  [data-dsh-mobile-settings-options] [data-slot="settings.general.item"] [class*="_rowText"] { width:100% !important; padding-right:0 !important; }
1217
1599
  [data-dsh-mobile-settings-options] [data-slot="settings.general.item"] [class*="_selector"] { box-sizing:border-box !important; align-self:flex-start !important; justify-content:space-between !important; min-width:0 !important; min-height:44px !important; max-width:100% !important; }
1218
1600
  [data-dsh-mobile-settings-options] :is(input,select,textarea,button) { max-width:100%; }
@@ -1281,8 +1663,9 @@ window.__ModuleLoader__.load({
1281
1663
  [data-dsh-mobile-center] [role="dialog"][aria-label*="Context"] :is(dt,dd) { white-space:nowrap !important; word-break:keep-all !important; }
1282
1664
  .dsh-mobile-branch-toast,.dsh-mobile-media-toast { display:block; position:fixed; z-index:330; top:max(12px,env(safe-area-inset-top)); left:50%; max-width:calc(100vw - 32px); box-sizing:border-box; padding:7px 14px; border:1px solid rgb(15 23 42 / 10%); border-radius:999px; background:rgb(15 23 42 / 92%); color:#fff; font-size:13px; line-height:20px; text-align:center; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; opacity:0; pointer-events:none; transform:translate(-50%,-8px); transition:opacity 160ms ease,transform 160ms ease; }
1283
1665
  .dsh-mobile-branch-toast[data-visible="true"],.dsh-mobile-media-toast[data-visible="true"] { opacity:1; transform:translate(-50%,0); }
1284
- .dsh-mobile-media-shortcuts { box-sizing:border-box; display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:8px; width:100%; padding:4px 4px 8px; margin-bottom:4px; border-bottom:1px solid var(--dsw-alias-border-inverted,var(--dsw-alias-border-subtle,rgb(148 163 184 / 28%))); }
1285
- .dsh-mobile-media-action { box-sizing:border-box; display:flex; align-items:center; justify-content:flex-start; gap:8px; width:100%; min-width:0; min-height:44px; padding:8px 10px; border:0; border-radius:10px; background:var(--dsw-alias-interactive-bg-hover,rgb(148 163 184 / 12%)); color:var(--dsw-alias-label-primary,inherit); cursor:pointer; font:inherit; font-size:14px; line-height:22px; text-align:left; touch-action:manipulation; }
1666
+ /* Fallback only: when the native Add group has no item to clone, keep the
1667
+ camera action on the same row metrics and tokens as the native menu. */
1668
+ .dsh-mobile-media-action { box-sizing:border-box; display:flex; align-items:center; justify-content:flex-start; gap:8px; width:100%; min-width:0; min-height:40px; padding:8px 10px; border:0; border-radius:10px; background:transparent; color:var(--dsw-alias-label-primary,inherit); cursor:pointer; font:inherit; font-size:14px; line-height:22px; text-align:left; touch-action:manipulation; }
1286
1669
  .dsh-mobile-media-action:active { opacity:.72; }
1287
1670
  .dsh-mobile-media-action:focus-visible { outline:2px solid var(--dsw-alias-interactive-border-focus,#4c82f7); outline-offset:1px; }
1288
1671
  .dsh-mobile-media-action:disabled { cursor:default; opacity:.38; }
@@ -1416,19 +1799,12 @@ window.__ModuleLoader__.load({
1416
1799
  const language = resolveNativeMobileLanguage(document.documentElement.lang, browserLanguages);
1417
1800
  const restoreLanguageMarker = applyNativeMobileLanguageMarker(document.documentElement, language);
1418
1801
  const label = (italian, english, chinese) => language === "it" ? italian : language === "zh" ? chinese : english;
1419
- const mediaIcon = (kind) => {
1802
+ const mediaIcon = () => {
1420
1803
  const namespace = "http://www.w3.org/2000/svg";
1421
1804
  const icon = document.createElementNS(namespace, "svg");
1422
1805
  icon.setAttribute("viewBox", "0 0 16 16");
1423
1806
  icon.setAttribute("fill", "none");
1424
1807
  icon.setAttribute("aria-hidden", "true");
1425
- if (kind === "attachment") {
1426
- const path = document.createElementNS(namespace, "path");
1427
- path.setAttribute("d", "M5.5498 9.75V5H6.9502V9.75C6.9502 10.3299 7.4201 10.7998 8 10.7998C8.5799 10.7998 9.0498 10.3299 9.0498 9.75V4.5C9.0498 2.9536 7.7964 1.7002 6.25 1.7002C4.7036 1.7002 3.4502 2.9536 3.4502 4.5V9.75C3.4502 12.2629 5.4871 14.2998 8 14.2998C10.5129 14.2998 12.5498 12.2629 12.5498 9.75V4H13.9502V9.75C13.9502 13.0361 11.2861 15.7002 8 15.7002C4.71391 15.7002 2.0498 13.0361 2.0498 9.75V4.5C2.04981 2.1804 3.9304 0.299806 6.25 0.299805C8.5696 0.299805 10.4502 2.1804 10.4502 4.5V9.75C10.4502 11.1031 9.3531 12.2002 8 12.2002C6.6469 12.2002 5.5498 11.1031 5.5498 9.75Z");
1428
- path.setAttribute("fill", "currentColor");
1429
- icon.append(path);
1430
- return icon;
1431
- }
1432
1808
  const body = document.createElementNS(namespace, "path");
1433
1809
  body.setAttribute("d", "M5.15 3.2 6.05 2h3.9l.9 1.2h1.45c1.05 0 1.9.85 1.9 1.9v6c0 1.05-.85 1.9-1.9 1.9H3.7a1.9 1.9 0 0 1-1.9-1.9v-6c0-1.05.85-1.9 1.9-1.9h1.45Zm-1.45 1.3a.6.6 0 0 0-.6.6v6c0 .33.27.6.6.6h8.6a.6.6 0 0 0 .6-.6v-6a.6.6 0 0 0-.6-.6h-2.1l-.9-1.2H6.7l-.9 1.2H3.7Z");
1434
1810
  body.setAttribute("fill", "currentColor");
@@ -1441,13 +1817,15 @@ window.__ModuleLoader__.load({
1441
1817
  icon.append(body, lens);
1442
1818
  return icon;
1443
1819
  };
1444
- const createMediaAction = (kind, text) => {
1820
+ const createMediaAction = (text) => {
1445
1821
  const button = document.createElement("button");
1446
1822
  button.type = "button";
1447
1823
  button.className = "dsh-mobile-media-action";
1448
1824
  button.lang = language;
1449
- button.dataset.dshMobileMediaAction = kind;
1450
- button.append(mediaIcon(kind));
1825
+ button.dataset.dshMobileMediaAction = "camera";
1826
+ const icon = document.createElement("span");
1827
+ icon.append(mediaIcon());
1828
+ button.append(icon);
1451
1829
  const caption = document.createElement("span");
1452
1830
  caption.textContent = text;
1453
1831
  button.append(caption);
@@ -1484,17 +1862,8 @@ window.__ModuleLoader__.load({
1484
1862
  mediaToast.setAttribute("role", "status");
1485
1863
  mediaToast.setAttribute("aria-live", "polite");
1486
1864
  document.body.append(mediaToast);
1487
- const mediaActions = document.createElement("div");
1488
- mediaActions.className = "dsh-mobile-media-shortcuts";
1489
- mediaActions.lang = language;
1490
- mediaActions.dataset.dshMobileMediaShortcuts = "true";
1491
- mediaActions.setAttribute("role", "group");
1492
- mediaActions.setAttribute("aria-label", label("Aggiungi immagine", "Add image", "添加图片"));
1493
- const fileButton = createMediaAction("attachment", label("Scegli immagine", "Choose image", "选择图片"));
1494
- const cameraButton = createMediaAction("camera", label("Scatta foto", "Take photo", "拍照"));
1495
- fileButton.disabled = true;
1865
+ const cameraButton = createMediaAction(label("Scatta foto", "Take photo", "拍照"));
1496
1866
  cameraButton.disabled = true;
1497
- mediaActions.append(fileButton, cameraButton);
1498
1867
  let branchToastTimer = 0;
1499
1868
  let mediaToastTimer = 0;
1500
1869
  const showMediaToast = (message) => {
@@ -1566,11 +1935,11 @@ window.__ModuleLoader__.load({
1566
1935
  if (files.length === 0 || !mediaRequestIsCurrent(context)) return;
1567
1936
  dispatchComposerImageDrop(document, files);
1568
1937
  };
1569
- const launchBrowserPicker = (camera, context) => {
1938
+ const launchBrowserCamera = (context) => {
1570
1939
  const input = document.createElement("input");
1571
1940
  input.type = "file";
1572
- input.accept = "image/png,image/jpeg,image/webp,image/gif";
1573
- if (camera) input.capture = "environment";
1941
+ input.accept = "image/*";
1942
+ input.capture = "environment";
1574
1943
  input.hidden = true;
1575
1944
  const signal = mediaPickerAbortController.signal;
1576
1945
  let cleanupTimer = 0;
@@ -1619,17 +1988,51 @@ window.__ModuleLoader__.load({
1619
1988
  input.click();
1620
1989
  } catch {
1621
1990
  cleanup();
1622
- if (mediaRequestIsCurrent(context)) showMediaToast(label("Impossibile aprire il selettore immagini", "Could not open the image picker", "无法打开图片选择器"));
1991
+ if (mediaRequestIsCurrent(context)) showMediaToast(label("Impossibile aprire la fotocamera", "Could not open the camera", "无法打开相机"));
1623
1992
  }
1624
1993
  };
1625
1994
  const dismissCommandMenu = () => {
1626
1995
  (boundComposer?.querySelector("button[aria-haspopup=\"listbox\"][aria-expanded=\"true\"]"))?.click();
1627
1996
  };
1628
- const setMediaActionsDisabled = (disabled, title) => {
1629
- for (const button of [fileButton, cameraButton]) {
1630
- if (button.disabled !== disabled) button.disabled = disabled;
1631
- if (button.title !== title) button.title = title;
1997
+ const addGroupLabel = label("Aggiungi", "Add", "添加");
1998
+ const placeCameraAction = (commandMenu) => {
1999
+ const sectionTitle = Array.from(commandMenu.querySelectorAll("[role=\"listbox\"] [role=\"presentation\"]")).find((candidate) => !candidate.hasAttribute("data-source") && candidate.textContent?.trim() === addGroupLabel);
2000
+ if (sectionTitle === void 0) {
2001
+ cameraButton.remove();
2002
+ return false;
1632
2003
  }
2004
+ cameraButton.className = "dsh-mobile-media-action";
2005
+ const nativeItem = (() => {
2006
+ let sibling = sectionTitle.nextElementSibling;
2007
+ while (sibling instanceof HTMLElement && sibling.getAttribute("role") !== "presentation") {
2008
+ if (sibling.matches("button[role=\"option\"]:not([aria-selected=\"true\"])")) return sibling;
2009
+ sibling = sibling.nextElementSibling;
2010
+ }
2011
+ })();
2012
+ if (nativeItem !== void 0) {
2013
+ cameraButton.className = nativeItem.className;
2014
+ const cameraIcon = cameraButton.firstElementChild;
2015
+ const cameraLabel = cameraButton.lastElementChild;
2016
+ const nativeIcon = firstByClassSuffix(nativeItem, "_itemIcon");
2017
+ const nativeLabel = firstByClassSuffix(nativeItem, "_itemName");
2018
+ if (cameraIcon instanceof HTMLElement && nativeIcon !== void 0) cameraIcon.className = nativeIcon.className;
2019
+ if (cameraLabel instanceof HTMLElement && nativeLabel !== void 0) cameraLabel.className = nativeLabel.className;
2020
+ }
2021
+ cameraButton.setAttribute("role", "option");
2022
+ cameraButton.setAttribute("aria-selected", "false");
2023
+ cameraButton.setAttribute("aria-label", label("Scatta foto", "Take photo", "拍照"));
2024
+ let lastItem = sectionTitle;
2025
+ let sibling = sectionTitle.nextElementSibling;
2026
+ while (sibling instanceof HTMLElement && sibling.getAttribute("role") !== "presentation") {
2027
+ if (sibling !== cameraButton) lastItem = sibling;
2028
+ sibling = sibling.nextElementSibling;
2029
+ }
2030
+ if (cameraButton.parentElement !== sectionTitle.parentElement || cameraButton.previousElementSibling !== lastItem) lastItem.after(cameraButton);
2031
+ return true;
2032
+ };
2033
+ const setCameraActionDisabled = (disabled, title) => {
2034
+ if (cameraButton.disabled !== disabled) cameraButton.disabled = disabled;
2035
+ if (cameraButton.title !== title) cameraButton.title = title;
1633
2036
  };
1634
2037
  const quietMediaPointer = (event) => {
1635
2038
  event.preventDefault();
@@ -1637,9 +2040,9 @@ window.__ModuleLoader__.load({
1637
2040
  const active = document.activeElement;
1638
2041
  if (active instanceof HTMLElement && (active.matches("input,textarea") || active.isContentEditable)) active.blur();
1639
2042
  };
1640
- const pickImage = (camera) => {
2043
+ const takePhoto = () => {
1641
2044
  if (!canAcceptComposerDrop()) {
1642
- setMediaActionsDisabled(true, label("Allegati immagine non disponibili", "Image attachments are unavailable", "图片附件不可用"));
2045
+ setCameraActionDisabled(true, label("Allegati immagine non disponibili", "Image attachments are unavailable", "图片附件不可用"));
1643
2046
  dismissCommandMenu();
1644
2047
  return;
1645
2048
  }
@@ -1647,36 +2050,21 @@ window.__ModuleLoader__.load({
1647
2050
  dismissCommandMenu();
1648
2051
  const bridge = window.__DSH_MOBILE_NATIVE__;
1649
2052
  if (bridge === void 0) {
1650
- launchBrowserPicker(camera, context);
2053
+ launchBrowserCamera(context);
1651
2054
  return;
1652
2055
  }
1653
- const action = camera ? "camera.capture" : "files.pick";
1654
- const input = camera ? {} : { accept: [
1655
- "image/png",
1656
- "image/jpeg",
1657
- "image/webp",
1658
- "image/gif"
1659
- ] };
1660
- Promise.resolve().then(() => bridge.invoke(action, input)).then((value) => {
2056
+ Promise.resolve().then(() => bridge.invoke("camera.capture", {})).then((value) => {
1661
2057
  if (!mediaRequestIsCurrent(context)) return;
1662
2058
  if (value instanceof File) deliverImages([value], context);
1663
- else showMediaToast(label("Il file selezionato non è utilizzabile", "The selected file is unavailable", "所选文件不可用"));
2059
+ else showMediaToast(label("La foto scattata non è utilizzabile", "The captured photo is unavailable", "所拍照片不可用"));
1664
2060
  }).catch((error) => {
1665
2061
  if (!mediaRequestIsCurrent(context)) return;
1666
2062
  const code = typeof error === "object" && error !== null && "code" in error ? String(error.code) : "";
1667
2063
  if (code === "cancelled") return;
1668
- showMediaToast(code === "payload_too_large" ? label("L’immagine supera il limite di 8 MiB", "The image exceeds the 8 MiB limit", "图片超过 8 MiB 限制") : label("Impossibile aggiungere l’immagine", "Could not attach the image", "无法附加图片"));
2064
+ showMediaToast(code === "payload_too_large" ? label("La foto supera il limite di 8 MiB", "The photo exceeds the 8 MiB limit", "照片超过 8 MiB 限制") : label("Impossibile aggiungere la foto", "Could not attach the photo", "无法附加照片"));
1669
2065
  });
1670
2066
  };
1671
- const chooseImage = () => {
1672
- pickImage(false);
1673
- };
1674
- const takePhoto = () => {
1675
- pickImage(true);
1676
- };
1677
- fileButton.addEventListener("pointerdown", quietMediaPointer);
1678
2067
  cameraButton.addEventListener("pointerdown", quietMediaPointer);
1679
- fileButton.addEventListener("click", chooseImage);
1680
2068
  cameraButton.addEventListener("click", takePhoto);
1681
2069
  const showBranchToast = () => {
1682
2070
  const header = document.querySelector("[data-dsh-mobile-header]");
@@ -1785,8 +2173,8 @@ window.__ModuleLoader__.load({
1785
2173
  if (center === void 0) {
1786
2174
  bindHistoryScroller(void 0);
1787
2175
  syncMediaBinding(null);
1788
- setMediaActionsDisabled(true, label("Apri prima una sessione", "Open a session first", "请先打开会话"));
1789
- mediaActions.remove();
2176
+ setCameraActionDisabled(true, label("Apri prima una sessione", "Open a session first", "请先打开会话"));
2177
+ cameraButton.remove();
1790
2178
  return;
1791
2179
  }
1792
2180
  if (center !== void 0) {
@@ -1824,8 +2212,8 @@ window.__ModuleLoader__.load({
1824
2212
  const composerRow = composerCard?.querySelector(":scope > [data-input-scroll]")?.nextElementSibling;
1825
2213
  if (!(composerRow instanceof HTMLElement)) {
1826
2214
  syncMediaBinding(null);
1827
- setMediaActionsDisabled(true, label("Apri prima una sessione", "Open a session first", "请先打开会话"));
1828
- mediaActions.remove();
2215
+ setCameraActionDisabled(true, label("Apri prima una sessione", "Open a session first", "请先打开会话"));
2216
+ cameraButton.remove();
1829
2217
  }
1830
2218
  if (composerRow instanceof HTMLElement) {
1831
2219
  composerRow.dataset.dshMobileComposerRow = "true";
@@ -1840,10 +2228,11 @@ window.__ModuleLoader__.load({
1840
2228
  const composerBusy = composerCard?.getAttribute("aria-busy") === "true" || composerInput?.disabled === true || composerInput?.readOnly === true || composerEditor?.getAttribute("aria-disabled") === "true";
1841
2229
  const attachmentBlocked = !canAcceptComposerDrop();
1842
2230
  const mediaDisabled = conversation === null || currentSessionOrigin().sessionId === null || composerBusy || attachmentBlocked;
1843
- const mediaTitle = composerBusy ? label("Attendi il completamento della risposta", "Wait for the response to finish", "请等待回复完成") : attachmentBlocked ? label("Allegati immagine non disponibili", "Image attachments are unavailable", "图片附件不可用") : mediaDisabled ? label("Apri prima una sessione", "Open a session first", "请先打开会话") : label("Allega screenshot, immagine o foto", "Attach screenshot, image, or photo", "附加截图、图片或照片");
1844
- setMediaActionsDisabled(mediaDisabled, mediaTitle);
2231
+ const mediaTitle = composerBusy ? label("Attendi il completamento della risposta", "Wait for the response to finish", "请等待回复完成") : attachmentBlocked ? label("Allegati immagine non disponibili", "Image attachments are unavailable", "图片附件不可用") : mediaDisabled ? label("Apri prima una sessione", "Open a session first", "请先打开会话") : label("Scatta una foto e allegala", "Take a photo and attach it", "拍照并附加到当前消息");
2232
+ setCameraActionDisabled(mediaDisabled, mediaTitle);
1845
2233
  const commandMenu = composerCard?.querySelector("[data-trigger-menu]") ?? null;
1846
- if (commandMenu !== null && (mediaActions.parentElement !== commandMenu || commandMenu.firstElementChild !== mediaActions)) commandMenu.prepend(mediaActions);
2234
+ if (commandMenu !== null) placeCameraAction(commandMenu);
2235
+ else cameraButton.remove();
1847
2236
  }
1848
2237
  if (composerTrailing !== void 0 && composerTrailing !== composerTools) {
1849
2238
  composerTrailing.dataset.dshMobileComposerTrailing = "true";
@@ -1899,8 +2288,19 @@ window.__ModuleLoader__.load({
1899
2288
  if (sidebar?.dataset.open === "true") toggle?.click();
1900
2289
  });
1901
2290
  sync();
2291
+ const disposeTaskWatcher = installTaskCompletionWatcher({
2292
+ completionFallback: false,
2293
+ label: (kind, sessionLabel) => kind === "done" ? {
2294
+ title: label("Attività completata", "Task finished", "任务已完成"),
2295
+ body: sessionLabel === "" ? label("Il tuo task DSH è terminato", "Your DSH task finished", "你的 DSH 任务已完成") : label(`Il tuo task DSH è terminato: ${sessionLabel}`, `Your DSH task finished: ${sessionLabel}`, `你的 DSH 任务已完成:${sessionLabel}`)
2296
+ } : {
2297
+ title: label("È richiesto un input", "Input needed", "需要你确认"),
2298
+ body: label("DSH attende una tua scelta", "DSH waits for your choice", "DSH 等待你的选择")
2299
+ }
2300
+ });
1902
2301
  return () => {
1903
2302
  disposed = true;
2303
+ disposeTaskWatcher();
1904
2304
  mediaRequestGeneration += 1;
1905
2305
  mediaPickerAbortController.abort();
1906
2306
  restoreLanguageMarker();
@@ -1908,15 +2308,13 @@ window.__ModuleLoader__.load({
1908
2308
  observer.disconnect();
1909
2309
  overlayQuery.removeEventListener("change", schedule);
1910
2310
  document.removeEventListener("click", onBranchClick, true);
1911
- fileButton.removeEventListener("pointerdown", quietMediaPointer);
1912
2311
  cameraButton.removeEventListener("pointerdown", quietMediaPointer);
1913
- fileButton.removeEventListener("click", chooseImage);
1914
2312
  cameraButton.removeEventListener("click", takePhoto);
1915
2313
  if (branchToastTimer !== 0) window.clearTimeout(branchToastTimer);
1916
2314
  if (mediaToastTimer !== 0) window.clearTimeout(mediaToastTimer);
1917
2315
  branchToast.remove();
1918
2316
  mediaToast.remove();
1919
- mediaActions.remove();
2317
+ cameraButton.remove();
1920
2318
  if (scheduled !== 0) cancelAnimationFrame(scheduled);
1921
2319
  if (transitionFrame !== 0) cancelAnimationFrame(transitionFrame);
1922
2320
  if (transitionRestartFrame !== 0) cancelAnimationFrame(transitionRestartFrame);
@@ -1932,6 +2330,55 @@ window.__ModuleLoader__.load({
1932
2330
  };
1933
2331
  }
1934
2332
  //#endregion
2333
+ //#region src/local-admin-host.ts
2334
+ /** Browser-safe desktop-admin Host checks. Do not import Node APIs here. */
2335
+ function unwrapBrackets(hostname) {
2336
+ return hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
2337
+ }
2338
+ function parseIpv4(hostname) {
2339
+ const parts = hostname.split(".");
2340
+ if (parts.length !== 4) return void 0;
2341
+ const octets = [];
2342
+ for (const part of parts) {
2343
+ if (!/^(?:0|[1-9]\d{0,2})$/u.test(part)) return void 0;
2344
+ const value = Number(part);
2345
+ if (value > 255) return void 0;
2346
+ octets.push(value);
2347
+ }
2348
+ return octets;
2349
+ }
2350
+ function isLoopbackIpv4(octets) {
2351
+ return octets[0] === 127;
2352
+ }
2353
+ function isPrivateIpv4(octets) {
2354
+ return octets[0] === 10 || octets[0] === 172 && octets[1] >= 16 && octets[1] <= 31 || octets[0] === 192 && octets[1] === 168;
2355
+ }
2356
+ function isLinkLocalIpv4(octets) {
2357
+ return octets[0] === 169 && octets[1] === 254;
2358
+ }
2359
+ /**
2360
+ * Hostnames that may appear on the DSH desktop admin surface.
2361
+ * Loopback, RFC1918, and IPv4 link-local are accepted. Public IPs,
2362
+ * CGNAT, and arbitrary DNS names stay rejected so DNS rebinding
2363
+ * cannot reach `/api/mobile-access`.
2364
+ */
2365
+ function isLocalAdminHostname(hostname) {
2366
+ const host = unwrapBrackets(hostname).trim().toLowerCase();
2367
+ if (host === "localhost" || host === "::1") return true;
2368
+ const octets = parseIpv4(host);
2369
+ if (octets === void 0) return false;
2370
+ return isLoopbackIpv4(octets) || isPrivateIpv4(octets) || isLinkLocalIpv4(octets);
2371
+ }
2372
+ /**
2373
+ * Whether the current browser document should mount the desktop Mobile access
2374
+ * control. Dedicated Mobile HTTPS (the phone surface) stays native even when
2375
+ * the Host is a private LAN address.
2376
+ */
2377
+ function isDesktopAdminSurface(hostname, search = "", frontend) {
2378
+ const query = search.startsWith("?") ? search.slice(1) : search;
2379
+ return isLocalAdminHostname(hostname) && frontend !== "dedicated" && !new URLSearchParams(query).has("dsh-mobile-preview");
2380
+ }
2381
+ //#endregion
1935
2382
  //#region src/client.ts
1936
2383
  const queuedDefinitions = [];
1937
2384
  let queuedLegacyMount;
@@ -1943,9 +2390,6 @@ window.__ModuleLoader__.load({
1943
2390
  queuedDefinitions.push(definition);
1944
2391
  }
1945
2392
  };
1946
- function isLoopbackHost(hostname) {
1947
- return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
1948
- }
1949
2393
  function selectMobileControlLocale(documentLanguage = "", navigatorLanguages = []) {
1950
2394
  for (const value of [documentLanguage, ...navigatorLanguages]) {
1951
2395
  const language = value.trim().toLowerCase().split(/[-_]/u)[0];
@@ -2053,6 +2497,82 @@ window.__ModuleLoader__.load({
2053
2497
  if (className !== void 0) node.className = className;
2054
2498
  return node;
2055
2499
  }
2500
+ function mobileSwitchComputerCopy(locale) {
2501
+ if (locale === "zh") return {
2502
+ title: "切换电脑",
2503
+ description: "返回已配对设备列表,选择要打开的电脑。",
2504
+ action: "切换电脑",
2505
+ busy: "正在切换…",
2506
+ retry: "重试",
2507
+ error: "切换失败,请重试。"
2508
+ };
2509
+ if (locale === "it") return {
2510
+ title: "Cambia computer",
2511
+ description: "Torna all’elenco dei computer associati e scegli quale aprire.",
2512
+ action: "Cambia computer",
2513
+ busy: "Cambio in corso…",
2514
+ retry: "Riprova",
2515
+ error: "Cambio non riuscito. Riprova."
2516
+ };
2517
+ return {
2518
+ title: "Switch computer",
2519
+ description: "Return to the paired-device list and choose a computer to open.",
2520
+ action: "Switch computer",
2521
+ busy: "Switching…",
2522
+ retry: "Retry",
2523
+ error: "Could not switch computers. Try again."
2524
+ };
2525
+ }
2526
+ /** Render the native-app-only action in DSH's General settings row style. */
2527
+ function MobileSwitchComputerRow() {
2528
+ const [nativeReady, setNativeReady] = (0, react.useState)(false);
2529
+ const [state, setState] = (0, react.useState)("idle");
2530
+ (0, react.useEffect)(() => {
2531
+ let disposed = false;
2532
+ const checkNativeAction = () => {
2533
+ const bridge = window.__DSH_MOBILE_NATIVE__;
2534
+ if (bridge === void 0) {
2535
+ setNativeReady(false);
2536
+ return;
2537
+ }
2538
+ Promise.resolve().then(() => bridge.capabilities()).then((capabilities) => {
2539
+ if (!disposed) setNativeReady(capabilities.includes("mobile.switch-computer"));
2540
+ }, () => {
2541
+ if (!disposed) setNativeReady(false);
2542
+ });
2543
+ };
2544
+ checkNativeAction();
2545
+ window.addEventListener("dsh-mobile-native-ready", checkNativeAction);
2546
+ return () => {
2547
+ disposed = true;
2548
+ window.removeEventListener("dsh-mobile-native-ready", checkNativeAction);
2549
+ };
2550
+ }, []);
2551
+ if (!nativeReady) return null;
2552
+ const locale = selectedMobileControlLocale();
2553
+ const copy = mobileSwitchComputerCopy(locale);
2554
+ const invoke = () => {
2555
+ const bridge = window.__DSH_MOBILE_NATIVE__;
2556
+ if (bridge === void 0 || state === "busy") return;
2557
+ setState("busy");
2558
+ bridge.invoke("mobile.switch-computer", {}).catch(() => {
2559
+ setState("error");
2560
+ });
2561
+ };
2562
+ return (0, react.createElement)("div", {
2563
+ className: "dsh-mobile-settings_row",
2564
+ lang: locale
2565
+ }, (0, react.createElement)("div", { className: "dsh-mobile-settings_rowText" }, (0, react.createElement)("div", { className: "dsh-mobile-settings_title" }, copy.title), (0, react.createElement)("div", {
2566
+ className: "dsh-mobile-settings_desc",
2567
+ "aria-live": "polite"
2568
+ }, state === "error" ? copy.error : copy.description)), (0, react.createElement)("button", {
2569
+ type: "button",
2570
+ className: "dsh-mobile-settings_selector",
2571
+ disabled: state === "busy",
2572
+ "aria-busy": state === "busy" ? "true" : void 0,
2573
+ onClick: invoke
2574
+ }, state === "busy" ? copy.busy : state === "error" ? copy.retry : copy.action));
2575
+ }
2056
2576
  const CONTROL_REQUEST_TIMEOUT_MS = 15e3;
2057
2577
  const LONG_CONTROL_REQUEST_TIMEOUT_MS = 21e4;
2058
2578
  const GITHUB_RELEASES_URL = "https://github.com/saya-ch/dsh-mobile/releases";
@@ -2193,6 +2713,33 @@ window.__ModuleLoader__.load({
2193
2713
  remoteTab.setAttribute("aria-pressed", "false");
2194
2714
  switcher.append(lanTab, remoteTab);
2195
2715
  const lanView = element("div", "dsh-mobile-control__view");
2716
+ const lanSetup = element("section", "dsh-mobile-control__lan-setup");
2717
+ lanSetup.hidden = true;
2718
+ const lanSetupTitle = element("h3", "dsh-mobile-control__section-title");
2719
+ lanSetupTitle.textContent = t("lanSetupTitle");
2720
+ const lanSetupIntro = element("p", "dsh-mobile-control__intro");
2721
+ lanSetupIntro.textContent = t("lanSetupIntro");
2722
+ const lanSetupForm = element("div", "dsh-mobile-control__lan-setup-form");
2723
+ const lanSetupLabel = element("label", "dsh-mobile-control__field");
2724
+ lanSetupLabel.textContent = t("lanSetupNetwork");
2725
+ const lanSetupSelect = element("select");
2726
+ lanSetupSelect.setAttribute("aria-label", t("lanSetupNetwork"));
2727
+ const lanSetupNote = element("p", "dsh-mobile-control__component-note");
2728
+ lanSetupNote.textContent = t("lanSetupFirewallNote");
2729
+ const lanSetupActions = element("div", "dsh-mobile-control__lan-setup-actions");
2730
+ const lanSetupRefresh = element("button", "dsh-mobile-control__secondary");
2731
+ lanSetupRefresh.type = "button";
2732
+ lanSetupRefresh.textContent = t("lanSetupRefresh");
2733
+ const lanSetupConfigure = element("button", "dsh-mobile-control__primary");
2734
+ lanSetupConfigure.type = "button";
2735
+ lanSetupConfigure.textContent = t("lanSetupConfigure");
2736
+ const lanSetupStatus = element("p", "dsh-mobile-control__status");
2737
+ lanSetupStatus.setAttribute("role", "status");
2738
+ lanSetupStatus.setAttribute("aria-live", "polite");
2739
+ lanSetupLabel.append(lanSetupSelect);
2740
+ lanSetupActions.append(lanSetupRefresh, lanSetupConfigure);
2741
+ lanSetupForm.append(lanSetupLabel, lanSetupNote, lanSetupActions);
2742
+ lanSetup.append(lanSetupTitle, lanSetupIntro, lanSetupForm, lanSetupStatus);
2196
2743
  const access = element("div", "dsh-mobile-control__access");
2197
2744
  access.hidden = true;
2198
2745
  const accessLabel = element("span", "dsh-mobile-control__access-label");
@@ -2230,6 +2777,10 @@ window.__ModuleLoader__.load({
2230
2777
  remoteView.hidden = true;
2231
2778
  const remoteIntro = element("p", "dsh-mobile-control__intro");
2232
2779
  remoteIntro.textContent = t("remoteIntro");
2780
+ const httpFrameWarning = element("p", "dsh-mobile-control__http-frame-warning");
2781
+ httpFrameWarning.textContent = t("httpFrameWarning");
2782
+ httpFrameWarning.hidden = location.protocol !== "https:";
2783
+ httpFrameWarning.setAttribute("role", "note");
2233
2784
  const providerSection = element("section", "dsh-mobile-control__provider-section");
2234
2785
  const providerHeading = element("h3", "dsh-mobile-control__section-title");
2235
2786
  providerHeading.textContent = t("chooseProvider");
@@ -2625,7 +3176,15 @@ window.__ModuleLoader__.load({
2625
3176
  remotePair.type = "button";
2626
3177
  remotePair.textContent = t("generateRemoteQr");
2627
3178
  remotePair.disabled = true;
2628
- remoteActions.append(remoteToggle, remoteLogin, remoteReconnect, remotePair);
3179
+ const remoteCopyLink = element("button", "dsh-mobile-control__secondary");
3180
+ remoteCopyLink.type = "button";
3181
+ remoteCopyLink.textContent = t("copyRemoteLink");
3182
+ remoteCopyLink.disabled = true;
3183
+ const remotePairLink = element("p", "dsh-mobile-control__status");
3184
+ remotePairLink.hidden = true;
3185
+ let activeRemotePairUrl = "";
3186
+ let activeRemotePairExpiresAt = 0;
3187
+ remoteActions.append(remoteToggle, remoteLogin, remoteReconnect, remotePair, remoteCopyLink);
2629
3188
  const remoteManageRow = element("div", "dsh-mobile-control__manage-row");
2630
3189
  const remoteDevices = element("button", "dsh-mobile-control__manage");
2631
3190
  remoteDevices.type = "button";
@@ -2821,7 +3380,7 @@ window.__ModuleLoader__.load({
2821
3380
  saveWsPaths([...wsPathsCurrent, value]);
2822
3381
  });
2823
3382
  const remoteWorkspace = element("section", "dsh-mobile-control__remote-workspace");
2824
- remoteWorkspace.append(providerSetupHeader, remoteStatus, remoteAccess, remoteGuide, providerSetupBody, remoteActions, remoteQr, remoteManageRow, remoteDevicePanel);
3383
+ remoteWorkspace.append(providerSetupHeader, remoteStatus, remoteAccess, remoteGuide, providerSetupBody, remoteActions, remoteQr, remotePairLink, remoteManageRow, remoteDevicePanel);
2825
3384
  const diagnosticsView = element("div", "dsh-mobile-control__view is-diagnostics");
2826
3385
  diagnosticsView.hidden = true;
2827
3386
  const diagnosticsIntro = element("p", "dsh-mobile-control__intro");
@@ -2865,14 +3424,17 @@ window.__ModuleLoader__.load({
2865
3424
  diagnosticsDetails.append(diagnosticsDetailsSummary, diagnosticsReport);
2866
3425
  header.append(title, headerActions);
2867
3426
  actions.append(toggle, pair, linkPair);
2868
- lanView.append(access, qrBox, status, extensionStatus, actions, manageRow, devicePanel);
2869
- remoteView.append(remoteIntro, providerSection, remoteWorkspace);
3427
+ lanView.append(lanSetup, access, qrBox, status, extensionStatus, actions, manageRow, devicePanel);
3428
+ remoteView.append(remoteIntro, httpFrameWarning, providerSection, remoteWorkspace);
2870
3429
  diagnosticsView.append(diagnosticsIntro, diagnosticsSummary, diagnosticsToolbar, diagnosticsFeedback, diagnosticsChecks, wsPathsSection, diagnosticsDetails);
2871
3430
  panel.append(header, releaseNotice, updateCard, appDownload, switcher, lanView, remoteView, diagnosticsView);
2872
3431
  root.append(panel);
2873
3432
  document.body.append(root);
2874
3433
  let running = false;
2875
3434
  let origin = "";
3435
+ let lanConfigured = true;
3436
+ let lanRestartRequired = false;
3437
+ let lanSetupLoaded = false;
2876
3438
  let remoteRunning = false;
2877
3439
  let remoteReady = false;
2878
3440
  let remoteProvider = "tailscale";
@@ -2976,14 +3538,25 @@ window.__ModuleLoader__.load({
2976
3538
  for (const trigger of document.querySelectorAll(".dsh-mobile-control__trigger")) trigger.setAttribute("aria-expanded", String(open));
2977
3539
  };
2978
3540
  const render = (data) => {
3541
+ lanConfigured = data.configured !== false;
3542
+ lanRestartRequired = data.restartRequired === true;
2979
3543
  running = data.running === true;
2980
3544
  origin = running && typeof data.origin === "string" ? data.origin : "";
3545
+ lanSetup.hidden = lanConfigured && !lanRestartRequired;
3546
+ lanSetupForm.hidden = lanRestartRequired;
3547
+ lanSetupIntro.hidden = lanRestartRequired;
3548
+ status.hidden = !lanConfigured || lanRestartRequired;
3549
+ if (!lanConfigured && !lanSetupLoaded) loadLanSetup();
3550
+ if (lanRestartRequired) {
3551
+ const pendingOrigin = typeof data.pendingOrigin === "string" ? data.pendingOrigin : "";
3552
+ lanSetupStatus.textContent = t("lanSetupRestart", { origin: pendingOrigin });
3553
+ } else if (!lanConfigured && lanSetupStatus.textContent === "") lanSetupStatus.textContent = t("lanSetupRequired");
2981
3554
  access.hidden = origin === "";
2982
3555
  accessLink.href = origin;
2983
3556
  accessLink.textContent = origin;
2984
3557
  accessLink.title = origin;
2985
3558
  status.classList.toggle("is-running", running);
2986
- status.textContent = running ? t("lanOn") : t("lanOff");
3559
+ status.textContent = !lanConfigured ? t("lanSetupRequired") : lanRestartRequired ? t("lanSetupRestart", { origin: typeof data.pendingOrigin === "string" ? data.pendingOrigin : "" }) : running ? t("lanOn") : t("lanOff");
2987
3560
  const extensionData = data.extensions;
2988
3561
  if (extensionData !== null && typeof extensionData === "object") {
2989
3562
  const loaded = typeof extensionData.loaded === "number" ? extensionData.loaded : 0;
@@ -2996,11 +3569,78 @@ window.__ModuleLoader__.load({
2996
3569
  } else extensionStatus.hidden = true;
2997
3570
  if (!running) qrBox.hidden = true;
2998
3571
  toggle.textContent = running ? t("disableLan") : t("enableLan");
3572
+ actions.hidden = !lanConfigured || lanRestartRequired;
3573
+ manageRow.hidden = !lanConfigured || lanRestartRequired;
2999
3574
  pair.disabled = !running;
3000
3575
  linkPair.disabled = !running;
3001
3576
  manageDevices.disabled = !running;
3002
3577
  resetAll.disabled = !running;
3003
3578
  };
3579
+ const renderLanSetup = (data) => {
3580
+ if (data.restartRequired === true || data.configured === true) {
3581
+ render(data);
3582
+ return;
3583
+ }
3584
+ const networks = Array.isArray(data.networks) ? data.networks.filter((entry) => typeof entry === "object" && entry !== null) : [];
3585
+ lanSetupSelect.replaceChildren();
3586
+ const hasRecommended = networks.some((network) => network.recommended === true);
3587
+ if (!hasRecommended && networks.length > 1) {
3588
+ const placeholder = element("option");
3589
+ placeholder.value = "";
3590
+ placeholder.textContent = t("lanSetupChooseNetwork");
3591
+ placeholder.disabled = true;
3592
+ placeholder.selected = true;
3593
+ lanSetupSelect.append(placeholder);
3594
+ }
3595
+ for (const network of networks) {
3596
+ if (typeof network.address !== "string" || typeof network.name !== "string") continue;
3597
+ const option = element("option");
3598
+ option.value = network.address;
3599
+ option.textContent = `${network.name} · ${network.address}${network.recommended === true ? ` · ${t("lanSetupRecommended")}` : ""}`;
3600
+ option.selected = network.recommended === true || !hasRecommended && networks.length === 1;
3601
+ lanSetupSelect.append(option);
3602
+ }
3603
+ const available = networks.some((network) => typeof network.address === "string" && typeof network.name === "string");
3604
+ lanSetupConfigure.disabled = lanSetupSelect.value === "";
3605
+ lanSetupStatus.textContent = available ? t("lanSetupRequired") : t("lanSetupNoNetworks");
3606
+ lanSetupNote.hidden = data.windowsFirewall !== true;
3607
+ };
3608
+ lanSetupSelect.addEventListener("change", () => {
3609
+ lanSetupConfigure.disabled = lanSetupSelect.value === "";
3610
+ });
3611
+ const loadLanSetup = () => {
3612
+ lanSetupLoaded = true;
3613
+ lanSetupRefresh.disabled = true;
3614
+ controlRequestJson("/api/mobile-access/lan/setup").then(renderLanSetup, () => {
3615
+ lanSetupStatus.textContent = t("lanSetupFailed");
3616
+ }).finally(() => {
3617
+ lanSetupRefresh.disabled = false;
3618
+ });
3619
+ };
3620
+ lanSetupRefresh.addEventListener("click", () => {
3621
+ loadLanSetup();
3622
+ });
3623
+ lanSetupConfigure.addEventListener("click", () => {
3624
+ if (lanSetupSelect.value === "") return;
3625
+ lanSetupConfigure.disabled = true;
3626
+ lanSetupRefresh.disabled = true;
3627
+ lanSetupConfigure.textContent = t("lanSetupConfiguring");
3628
+ lanSetupStatus.textContent = t("lanSetupConfiguring");
3629
+ controlRequestJson("/api/mobile-access/lan/setup", {
3630
+ method: "POST",
3631
+ body: JSON.stringify({
3632
+ confirm: true,
3633
+ address: lanSetupSelect.value,
3634
+ configureFirewall: true
3635
+ })
3636
+ }, LONG_CONTROL_REQUEST_TIMEOUT_MS).then(renderLanSetup, () => {
3637
+ lanSetupStatus.textContent = t("lanSetupFailed");
3638
+ }).finally(() => {
3639
+ lanSetupConfigure.textContent = t("lanSetupConfigure");
3640
+ lanSetupRefresh.disabled = false;
3641
+ lanSetupConfigure.disabled = lanRestartRequired || lanSetupSelect.options.length === 0;
3642
+ });
3643
+ });
3004
3644
  const showQr = (svg, target = qrBox) => {
3005
3645
  target.replaceChildren();
3006
3646
  if (svg === "") {
@@ -3263,8 +3903,14 @@ window.__ModuleLoader__.load({
3263
3903
  remoteReconnect.hidden = needsFunnelSetup || state !== "error" && state !== "unavailable" || !providerPrepared;
3264
3904
  remoteActions.hidden = !providerPrepared;
3265
3905
  remotePair.disabled = !remoteReady;
3906
+ remoteCopyLink.disabled = !remoteReady;
3266
3907
  remoteDevices.disabled = !remoteReady;
3267
- if (!remoteReady) remoteQr.hidden = true;
3908
+ if (!remoteReady) {
3909
+ remoteQr.hidden = true;
3910
+ remotePairLink.hidden = true;
3911
+ activeRemotePairUrl = "";
3912
+ activeRemotePairExpiresAt = 0;
3913
+ }
3268
3914
  if (!needsFunnelSetup) remoteSetupPending = false;
3269
3915
  };
3270
3916
  let remoteLoadInFlight = false;
@@ -3765,24 +4411,48 @@ window.__ModuleLoader__.load({
3765
4411
  };
3766
4412
  window.addEventListener("focus", retryAfterSetup);
3767
4413
  document.addEventListener("visibilitychange", retryAfterSetup);
4414
+ const copyRemotePairUrl = async () => {
4415
+ try {
4416
+ await navigator.clipboard.writeText(activeRemotePairUrl);
4417
+ remoteStatus.textContent = t("linkCopied");
4418
+ } catch {
4419
+ remoteStatus.textContent = t("remoteQrReady");
4420
+ }
4421
+ };
4422
+ const openRemotePairing = () => controlRequestJson("/api/mobile-access/remote/pairing/open", {
4423
+ method: "POST",
4424
+ body: "{}"
4425
+ }).then(async (data) => {
4426
+ const pairUrl = typeof data.pairUrl === "string" ? data.pairUrl : "";
4427
+ const expiresAt = typeof data.expiresAt === "number" && Number.isFinite(data.expiresAt) ? data.expiresAt : 0;
4428
+ showQr(typeof data.qrSvg === "string" ? data.qrSvg : "", remoteQr);
4429
+ if (pairUrl === "") {
4430
+ remoteStatus.textContent = t("keyGenerationFailed");
4431
+ return;
4432
+ }
4433
+ activeRemotePairUrl = pairUrl;
4434
+ activeRemotePairExpiresAt = expiresAt;
4435
+ remotePairLink.textContent = pairUrl;
4436
+ remotePairLink.classList.add("is-key");
4437
+ remotePairLink.hidden = false;
4438
+ await copyRemotePairUrl();
4439
+ });
3768
4440
  remotePair.addEventListener("click", () => {
3769
4441
  remotePair.disabled = true;
3770
- controlRequestJson("/api/mobile-access/remote/pairing/open", {
3771
- method: "POST",
3772
- body: "{}"
3773
- }).then(async (data) => {
3774
- const pairUrl = typeof data.pairUrl === "string" ? data.pairUrl : "";
3775
- showQr(typeof data.qrSvg === "string" ? data.qrSvg : "", remoteQr);
3776
- if (pairUrl !== "") try {
3777
- await navigator.clipboard.writeText(pairUrl);
3778
- } catch {}
3779
- remoteStatus.textContent = t("remoteQrReady");
3780
- }, (error) => {
4442
+ openRemotePairing().catch((error) => {
3781
4443
  remoteStatus.textContent = t("requestFailed", { error: String(error) });
3782
4444
  }).finally(() => {
3783
4445
  remotePair.disabled = !remoteReady;
3784
4446
  });
3785
4447
  });
4448
+ remoteCopyLink.addEventListener("click", () => {
4449
+ remoteCopyLink.disabled = true;
4450
+ (activeRemotePairUrl !== "" && activeRemotePairExpiresAt > Date.now() ? copyRemotePairUrl() : openRemotePairing()).catch((error) => {
4451
+ remoteStatus.textContent = t("requestFailed", { error: String(error) });
4452
+ }).finally(() => {
4453
+ remoteCopyLink.disabled = !remoteReady;
4454
+ });
4455
+ });
3786
4456
  const renderRemoteDevices = (data) => {
3787
4457
  const devices = Array.isArray(data.devices) ? data.devices : [];
3788
4458
  remoteDevicePanel.replaceChildren();
@@ -4301,6 +4971,15 @@ window.__ModuleLoader__.load({
4301
4971
  if (generation !== void 0) result.set("x-dsh-mobile-extension-generation", generation);
4302
4972
  return result;
4303
4973
  }
4974
+ /** Build the JSON request used by a mobile extension to invoke a Host action. */
4975
+ function extensionActionRequestInit(generation, input, signal) {
4976
+ return {
4977
+ method: "POST",
4978
+ headers: extensionGenerationHeaders(generation, { "content-type": "application/json" }),
4979
+ body: JSON.stringify(input ?? {}),
4980
+ signal
4981
+ };
4982
+ }
4304
4983
  function registerUniqueDisposable(entries, claimedIds, id, mount) {
4305
4984
  if (claimedIds.has(id)) throw new Error(`duplicate lifecycle id: ${id}`);
4306
4985
  claimedIds.add(id);
@@ -4473,7 +5152,7 @@ window.__ModuleLoader__.load({
4473
5152
  function startExtensionChangeStream(changed, runtime = {
4474
5153
  window,
4475
5154
  create: (url) => new EventSource(url, { withCredentials: true })
4476
- }) {
5155
+ }, onTaskEvent, onDeviceRevoked) {
4477
5156
  let source;
4478
5157
  let timer;
4479
5158
  let disposed = false;
@@ -4492,6 +5171,14 @@ window.__ModuleLoader__.load({
4492
5171
  retryMs = 1e3;
4493
5172
  };
4494
5173
  next.addEventListener("extensions-changed", changed);
5174
+ if (onTaskEvent !== void 0) next.addEventListener("task-notify", (message) => {
5175
+ const data = message.data;
5176
+ onTaskEvent(data);
5177
+ });
5178
+ if (onDeviceRevoked !== void 0) next.addEventListener("device-revoked", (message) => {
5179
+ const data = message.data;
5180
+ if (isDeviceRevokedPayload(data)) onDeviceRevoked();
5181
+ });
4495
5182
  next.onerror = () => {
4496
5183
  if (source !== next) return;
4497
5184
  next.close();
@@ -4857,12 +5544,7 @@ window.__ModuleLoader__.load({
4857
5544
  host: {
4858
5545
  invoke: (action, input) => {
4859
5546
  ensureCurrent();
4860
- return mobileRequest(`/mobile-access/extensions/${encodeURIComponent(id)}/actions/${encodeURIComponent(action)}`, {
4861
- method: "POST",
4862
- headers: extensionGenerationHeaders(hostGeneration),
4863
- body: JSON.stringify(input ?? {}),
4864
- signal: controller.signal
4865
- }).then(async (response) => {
5547
+ return mobileRequest(`/mobile-access/extensions/${encodeURIComponent(id)}/actions/${encodeURIComponent(action)}`, extensionActionRequestInit(hostGeneration, input, controller.signal)).then(async (response) => {
4866
5548
  const value = await response.json();
4867
5549
  if (!response.ok) throw new Error(typeof value === "object" && value !== null && "error" in value ? String(value.error) : `HTTP ${String(response.status)}`);
4868
5550
  return value;
@@ -5252,6 +5934,20 @@ window.__ModuleLoader__.load({
5252
5934
  });
5253
5935
  const stopEvents = startExtensionChangeStream(() => {
5254
5936
  stopRefresh.refresh();
5937
+ }, void 0, (payload) => {
5938
+ if (document.hidden !== true) return;
5939
+ const parsed = parseTaskNotifyPayload(payload);
5940
+ if (parsed === void 0) return;
5941
+ const language = resolveNativeMobileLanguage(document.documentElement.lang, navigator.languages.length > 0 ? [...navigator.languages] : [navigator.language]);
5942
+ const label = (italian, english, chinese) => language === "it" ? italian : language === "zh" ? chinese : english;
5943
+ fireTaskNotifyEvent({
5944
+ kind: "done",
5945
+ title: label("Attività completata", "Task finished", "任务已完成"),
5946
+ body: label("Il tuo task DSH è terminato", "Your DSH task finished", "你的 DSH 任务已完成"),
5947
+ tag: taskCompletionTag(parsed.sessionId, parsed.turn)
5948
+ });
5949
+ }, () => {
5950
+ fireDeviceRevoked();
5255
5951
  });
5256
5952
  return () => {
5257
5953
  disposed = true;
@@ -5314,12 +6010,13 @@ window.__ModuleLoader__.load({
5314
6010
  .dsh-mobile-control__panel{box-sizing:border-box;width:min(380px,calc(100vw - 32px));max-height:calc(100vh - 140px);overflow-y:auto;padding:16px;border:1px solid var(--dsw-alias-border-subtle,#e1e5eb);border-radius:18px;background:var(--dsw-alias-bg-layer-2,#fff);box-shadow:0 18px 50px rgb(15 23 42 / 18%)}
5315
6011
  .dsh-mobile-control__header{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:10px}.dsh-mobile-control__panel h2{margin:0;font-size:17px;line-height:24px}.dsh-mobile-control__header-actions{display:flex;align-items:center;gap:2px}.dsh-mobile-control__update-plugin,.dsh-mobile-control__diagnostic-entry,.dsh-mobile-control__close{display:inline-flex;align-items:center;justify-content:center;min-width:44px;height:44px;padding:0;border:0;border-radius:10px;background:transparent;color:inherit;cursor:pointer}.dsh-mobile-control__update-plugin,.dsh-mobile-control__diagnostic-entry{padding:0 8px;color:#2563eb;font:650 12px/1 system-ui;white-space:nowrap}.dsh-mobile-control__update-plugin[hidden]{display:none}.dsh-mobile-control__update-plugin:disabled{cursor:wait;opacity:.55}.dsh-mobile-control__close{font-size:24px;line-height:1}.dsh-mobile-control__update-plugin:hover:not(:disabled),.dsh-mobile-control__diagnostic-entry:hover,.dsh-mobile-control__close:hover{background:var(--dsw-alias-interactive-bg-hover,#f1f3f6)}
5316
6012
  .dsh-mobile-control__release-notice{margin:-2px 0 10px;padding:8px 10px;border-radius:9px;background:var(--dsw-alias-bg-layer-1,#eff6ff);color:var(--dsw-alias-label-primary,#1d4ed8);font-size:11px;line-height:1.45}.dsh-mobile-control__release-notice.is-error{color:#dc2626}.dsh-mobile-control__release-notice[hidden]{display:none}.dsh-mobile-control__update-card{margin:0 0 10px;padding:11px;border:1px solid var(--dsw-alias-border-subtle,#dbe1e8);border-radius:12px;background:var(--dsw-alias-bg-layer-1,#f8fafc)}.dsh-mobile-control__update-card[hidden]{display:none}.dsh-mobile-control__update-card-title{margin:0 0 7px;color:var(--dsw-alias-label-primary,#16181d);font:650 13px/1.4 system-ui}.dsh-mobile-control__update-notes{display:block;box-sizing:border-box;max-height:150px;overflow-y:auto;margin:0 0 8px;padding:8px 10px;border:1px solid var(--dsw-alias-border-subtle,#e1e5eb);border-radius:9px;background:var(--dsw-alias-bg-layer-2,#fff);color:var(--dsw-alias-label-secondary,#475569);white-space:pre-wrap;word-break:break-word;font:11px/1.5 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}.dsh-mobile-control__update-notes[hidden]{display:none}.dsh-mobile-control__update-notice{margin:0 0 9px;padding:7px 9px;border-radius:8px;background:#fff4d6;color:#92400e;font-size:11px;line-height:1.5}.dsh-mobile-control__update-actions{display:flex;align-items:center;justify-content:flex-end;gap:8px}.dsh-mobile-control__update-actions .dsh-mobile-control__primary,.dsh-mobile-control__update-actions .dsh-mobile-control__secondary{min-height:34px;padding:0 12px}.dsh-mobile-control__app-download{display:flex;align-items:center;justify-content:space-between;box-sizing:border-box;min-height:38px;margin:0 0 10px;padding:8px 11px;border:1px solid var(--dsw-alias-border-subtle,#dbe1e8);border-radius:11px;background:var(--dsw-alias-bg-layer-1,#f7f8fa);color:var(--dsw-alias-label-primary,#16181d);font:600 12px/1.3 system-ui;text-decoration:none}.dsh-mobile-control__app-download[hidden]{display:none}.dsh-mobile-control__app-download::after{color:#2563eb;font-size:14px;content:"↗"}.dsh-mobile-control__app-download:hover{border-color:#9fb9e8;background:var(--dsw-alias-interactive-bg-hover-solid,var(--dsw-alias-bg-layer-2,#f5f8ff));color:var(--dsw-alias-label-primary,#1d4ed8)}
5317
- .dsh-mobile-control__switcher{display:grid;grid-template-columns:repeat(2,1fr);gap:4px;margin:0 0 14px;padding:4px;border-radius:12px;background:var(--dsw-alias-bg-layer-1,#f3f5f8)}.dsh-mobile-control__switcher[hidden]{display:none}.dsh-mobile-control__tab{min-height:36px;border:0;border-radius:9px;background:transparent;color:var(--dsw-alias-label-secondary,#606873);font:600 13px/1 system-ui;cursor:pointer}.dsh-mobile-control__tab.is-active{background:var(--dsw-alias-bg-layer-2,#fff);color:var(--dsw-alias-label-primary,#16181d);box-shadow:0 1px 3px rgb(15 23 42 / 10%)}.dsh-mobile-control__view[hidden]{display:none}.dsh-mobile-control__intro{margin:0 0 12px;color:var(--dsw-alias-label-secondary,#606873);font-size:12px;line-height:1.55}.dsh-mobile-control__view.is-remote .dsh-mobile-control__actions{display:grid;grid-template-columns:1fr 1fr;gap:8px}.dsh-mobile-control__view.is-remote .dsh-mobile-control__actions button[hidden]{display:none}
6013
+ .dsh-mobile-control__switcher{display:grid;grid-template-columns:repeat(2,1fr);gap:4px;margin:0 0 14px;padding:4px;border-radius:12px;background:var(--dsw-alias-bg-layer-1,#f3f5f8)}.dsh-mobile-control__switcher[hidden]{display:none}.dsh-mobile-control__tab{min-height:36px;border:0;border-radius:9px;background:transparent;color:var(--dsw-alias-label-secondary,#606873);font:600 13px/1 system-ui;cursor:pointer}.dsh-mobile-control__tab.is-active{background:var(--dsw-alias-bg-layer-2,#fff);color:var(--dsw-alias-label-primary,#16181d);box-shadow:0 1px 3px rgb(15 23 42 / 10%)}.dsh-mobile-control__view[hidden]{display:none}.dsh-mobile-control__intro{margin:0 0 12px;color:var(--dsw-alias-label-secondary,#606873);font-size:12px;line-height:1.55}.dsh-mobile-control__http-frame-warning{box-sizing:border-box;margin:-4px 0 12px;padding:8px 10px;border:1px solid #d6a83f;border-radius:9px;background:var(--dsw-alias-bg-layer-1,#fff7df);color:var(--dsw-alias-label-primary,#694b00);font-size:11px;line-height:1.5;overflow-wrap:anywhere}.dsh-mobile-control__http-frame-warning[hidden]{display:none}.dsh-mobile-control__view.is-remote .dsh-mobile-control__actions{display:grid;grid-template-columns:1fr 1fr;gap:8px}.dsh-mobile-control__view.is-remote .dsh-mobile-control__actions button[hidden]{display:none}
5318
6014
  .dsh-mobile-control__provider-section{position:relative;margin:0 0 14px}.dsh-mobile-control__section-title{margin:0 0 8px;color:var(--dsw-alias-label-primary,#16181d);font:650 13px/1.4 system-ui}.dsh-mobile-control__provider-section>.dsh-mobile-control__section-title{padding-right:42px}.dsh-mobile-control__provider-choices{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}.dsh-mobile-control__provider{display:flex;min-width:0;flex-direction:column;gap:6px;min-height:94px;padding:10px 11px;border:1px solid var(--dsw-alias-border-subtle,#dbe1e8);border-radius:13px;background:var(--dsw-alias-bg-layer-2,#fff);color:var(--dsw-alias-label-primary,#16181d);text-align:left;cursor:pointer;touch-action:manipulation;transition:border-color 160ms ease,background-color 160ms ease,box-shadow 160ms ease}.dsh-mobile-control__provider:hover{border-color:#6f96db;background:var(--dsw-alias-interactive-bg-hover-solid,var(--dsw-alias-bg-layer-1,#f8fbff))}.dsh-mobile-control__provider.is-selected{border-color:#2563eb;background:var(--dsw-alias-interactive-bg-active,var(--dsw-alias-bg-layer-1,#f5f8ff));box-shadow:0 0 0 1px #2563eb inset}.dsh-mobile-control__provider:disabled{cursor:wait;opacity:.62}.dsh-mobile-control__provider-top{display:flex;min-width:0;align-items:flex-start;justify-content:space-between;gap:5px}.dsh-mobile-control__provider-top strong{min-width:0;font-size:12px;line-height:1.3}.dsh-mobile-control__provider-badge{flex:none;padding:2px 5px;border-radius:999px;background:#e8f0ff;color:#1d4ed8;font:650 9px/1.25 system-ui}.dsh-mobile-control__provider-badge.is-cpolar{background:#eaf8f2;color:#087454}.dsh-mobile-control__provider-description{color:var(--dsw-alias-label-secondary,#606873);font-size:10px;line-height:1.45}.dsh-mobile-control__provider-info{position:absolute;z-index:5;top:-13px;right:-8px}.dsh-mobile-control__provider-info-button{display:flex;align-items:center;justify-content:center;width:44px;height:44px;padding:0;border:0;border-radius:50%;background:transparent;color:var(--dsw-alias-label-secondary,#475569);cursor:pointer;touch-action:manipulation}.dsh-mobile-control__provider-info-button:hover{background:var(--dsw-alias-interactive-bg-hover,#f1f5f9);color:#2563eb}.dsh-mobile-control__provider-info-glyph{display:flex;align-items:center;justify-content:center;box-sizing:border-box;width:18px;height:18px;border:1.5px solid currentColor;border-radius:50%;font:700 12px/1 system-ui}.dsh-mobile-control__provider-info-popover{position:absolute;z-index:6;top:38px;right:4px;box-sizing:border-box;width:min(292px,calc(100vw - 72px));padding:10px 12px;border:1px solid var(--dsw-alias-border-subtle,#dbe1e8);border-radius:12px;background:var(--dsw-alias-bg-layer-2,#fff);box-shadow:0 10px 28px rgb(15 23 42 / 16%)}.dsh-mobile-control__provider-info-popover[hidden]{display:none}.dsh-mobile-control__provider-info-popover strong,.dsh-mobile-control__provider-info-popover span{display:block}.dsh-mobile-control__provider-info-popover strong{margin-bottom:3px;font-size:12px}.dsh-mobile-control__provider-info-popover span{color:var(--dsw-alias-label-secondary,#606873);font-size:11px;line-height:1.55}
5319
6015
  .dsh-mobile-control__self-hosted{margin:8px 0 0;border:1px solid var(--dsw-alias-border-subtle,#dbe1e8);border-radius:12px;background:var(--dsw-alias-bg-layer-1,#f8fafc)}.dsh-mobile-control__self-hosted-summary{display:flex;box-sizing:border-box;min-height:48px;align-items:center;justify-content:space-between;gap:10px;padding:8px 11px;cursor:pointer;list-style-position:inside}.dsh-mobile-control__self-hosted-summary>span:first-child{display:flex;min-width:0;flex-direction:column;gap:1px}.dsh-mobile-control__self-hosted-summary strong{font-size:11px}.dsh-mobile-control__self-hosted-summary span span{color:var(--dsw-alias-label-secondary,#606873);font-size:9px;line-height:1.35}.dsh-mobile-control__provider-badge.is-frp{background:#eef0f3;color:#475569}.dsh-mobile-control__self-hosted-body{padding:0 8px 8px}.dsh-mobile-control__provider.is-frp{width:100%;min-height:64px;background:var(--dsw-alias-bg-layer-2,#fff)}
5320
6016
  .dsh-mobile-control__cpolar-setup{margin:0 0 12px;padding:12px;border:1px solid var(--dsw-alias-border-subtle,#dbe1e8);border-radius:13px;background:var(--dsw-alias-bg-layer-2,#fff)}.dsh-mobile-control__cpolar-setup[hidden],.dsh-mobile-control__cpolar-account[hidden],.dsh-mobile-control__details[hidden],.dsh-mobile-control__view.is-remote .dsh-mobile-control__actions[hidden],.dsh-mobile-control__danger[hidden]{display:none}.dsh-mobile-control__component-status,.dsh-mobile-control__component-note{margin:0 0 10px;color:var(--dsw-alias-label-secondary,#606873);font-size:11px;line-height:1.55}.dsh-mobile-control__cpolar-setup>.dsh-mobile-control__primary{width:100%;min-height:44px;padding:9px 12px;border-radius:10px;font:600 12px/1.3 system-ui;cursor:pointer}.dsh-mobile-control__cpolar-account{margin-top:10px}.dsh-mobile-control__link-row{display:flex;flex-wrap:wrap;gap:6px 12px;margin:0 0 10px}.dsh-mobile-control__text-link{color:#2563eb;font-size:11px;text-decoration:none}.dsh-mobile-control__text-link:hover{text-decoration:underline}.dsh-mobile-control__token-label{display:flex;flex-direction:column;gap:5px;margin:0 0 8px;color:var(--dsw-alias-label-secondary,#606873);font-size:11px}.dsh-mobile-control__token{box-sizing:border-box;width:100%;min-height:44px;padding:9px 10px;border:1px solid var(--dsw-alias-border-normal,#cfd5dd);border-radius:10px;background:var(--dsw-alias-bg-layer-3,var(--dsw-alias-bg-layer-2,#fff));color:var(--dsw-alias-label-primary,#16181d);font:16px/1.4 system-ui}.dsh-mobile-control__cpolar-connect{display:flex;align-items:center;justify-content:center;box-sizing:border-box;width:100%;min-height:44px;padding:10px 14px;border-radius:12px;font:650 13px/1.2 system-ui;cursor:pointer;transition:background-color 160ms ease,border-color 160ms ease,opacity 160ms ease}.dsh-mobile-control__cpolar-connect:hover:not(:disabled){border-color:#1d4ed8;background:#1d4ed8}.dsh-mobile-control__cpolar-connect:active:not(:disabled){border-color:#1e40af;background:#1e40af}.dsh-mobile-control__cpolar-connect:disabled{cursor:wait;opacity:.55}.dsh-mobile-control__details{margin:10px 0 0;border-top:1px solid var(--dsw-alias-border-subtle,#e1e5eb);padding-top:9px}.dsh-mobile-control__details>summary{min-height:30px;color:var(--dsw-alias-label-secondary,#606873);font-size:11px;line-height:30px;cursor:pointer}.dsh-mobile-control__details-body{display:flex;flex-wrap:wrap;align-items:center;gap:7px 12px;padding:4px 0}.dsh-mobile-control__details-body p{flex:1 0 100%;margin:0;color:var(--dsw-alias-label-secondary,#606873);font-size:11px;line-height:1.5}.dsh-mobile-control__storage{display:block;flex:1 0 100%;max-width:100%;overflow:hidden;padding:7px 8px;border-radius:8px;background:var(--dsw-alias-bg-layer-1,#f3f5f8);color:var(--dsw-alias-label-secondary,#475569);font:10px/1.4 ui-monospace,SFMono-Regular,Consolas,monospace;text-overflow:ellipsis;white-space:nowrap}.dsh-mobile-control__danger{flex:1 0 100%;min-height:38px;margin-top:3px;padding:7px 10px;border:1px solid #dc2626;border-radius:9px;background:transparent;color:#dc2626;font:12px/1.3 system-ui;cursor:pointer}
5321
6017
  .dsh-mobile-control__frp-setup{margin:0;padding:12px;border:1px solid var(--dsw-alias-border-subtle,#dbe1e8);border-radius:13px;background:var(--dsw-alias-bg-layer-2,#fff)}.dsh-mobile-control__frp-setup[hidden]{display:none}.dsh-mobile-control__frp-step{padding:11px 0}.dsh-mobile-control__frp-step + .dsh-mobile-control__frp-step{border-top:1px solid var(--dsw-alias-border-subtle,#e1e5eb)}.dsh-mobile-control__frp-step>strong{display:block;margin-bottom:3px;font-size:12px;line-height:1.4}.dsh-mobile-control__frp-step>p{margin:0 0 9px;color:var(--dsw-alias-label-secondary,#606873);font-size:11px;line-height:1.5}.dsh-mobile-control__frp-changes{margin:0 0 9px;padding:0;list-style:none;color:var(--dsw-alias-label-secondary,#606873);font-size:11px;line-height:1.5}.dsh-mobile-control__frp-changes li{position:relative;margin:0;padding-left:12px}.dsh-mobile-control__frp-changes li+li{margin-top:5px}.dsh-mobile-control__frp-changes li::before{content:"•";position:absolute;left:2px;color:var(--dsw-alias-label-tertiary,#98a1ad)}.dsh-mobile-control__frp-step>.dsh-mobile-control__frp-requirement{padding:8px 9px;border-radius:9px;background:var(--dsw-alias-bg-layer-1,#f3f5f8);color:var(--dsw-alias-label-primary,#384152);font-size:11px}.dsh-mobile-control__frp-fields{display:grid;grid-template-columns:minmax(0,1fr) 96px;gap:8px}.dsh-mobile-control__field{display:flex;min-width:0;flex-direction:column;gap:5px;color:var(--dsw-alias-label-secondary,#606873);font-size:11px}.dsh-mobile-control__field:nth-child(3),.dsh-mobile-control__field:nth-child(4){grid-column:1/-1}.dsh-mobile-control__field input{box-sizing:border-box;width:100%;min-height:44px;padding:9px 10px;border:1px solid var(--dsw-alias-border-normal,#cfd5dd);border-radius:10px;background:var(--dsw-alias-bg-layer-2,#fff);color:var(--dsw-alias-label-primary,#16181d);font:16px/1.4 system-ui}.dsh-mobile-control__frp-action{box-sizing:border-box;width:100%;min-height:44px;padding:9px 12px;border-radius:10px;font:650 12px/1.3 system-ui;cursor:pointer}.dsh-mobile-control__frp-action:disabled{cursor:not-allowed;opacity:.5}.dsh-mobile-control__remote-workspace{margin:0;padding:12px;border:1px solid var(--dsw-alias-border-subtle,#dbe1e8);border-radius:15px;background:var(--dsw-alias-bg-layer-1,#f7f8fa)}.dsh-mobile-control__stage-header{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:9px}.dsh-mobile-control__stage-header .dsh-mobile-control__section-title{margin:0}.dsh-mobile-control__stage-meta{display:flex;min-width:0;align-items:center;justify-content:flex-end;gap:5px}.dsh-mobile-control__stage-value{max-width:115px;overflow:hidden;color:var(--dsw-alias-label-primary,#16181d);font:650 10px/1.3 system-ui;text-overflow:ellipsis;white-space:nowrap}.dsh-mobile-control__state-badge{flex:none;padding:3px 7px;border-radius:999px;background:var(--dsw-alias-bg-layer-2,#fff);color:var(--dsw-alias-label-secondary,#606873);font:650 9px/1.25 system-ui}.dsh-mobile-control__state-badge.is-ready{background:#e6f7f0;color:#087454}.dsh-mobile-control__state-badge.is-busy{background:#e8f0ff;color:#1d4ed8}.dsh-mobile-control__state-badge.is-attention{background:#fff4dc;color:#935100}.dsh-mobile-control__remote-workspace>.dsh-mobile-control__status{box-sizing:border-box;margin:0 0 10px;padding:9px 10px;border-radius:10px;background:var(--dsw-alias-bg-layer-2,#fff);font-size:11px;line-height:1.45}.dsh-mobile-control__provider-setup-body{margin:0 0 10px}.dsh-mobile-control__provider-setup-body>.dsh-mobile-control__cpolar-setup{margin:0}.dsh-mobile-control__provider-setup-body>.dsh-mobile-control__cpolar-setup>.dsh-mobile-control__section-title,.dsh-mobile-control__provider-setup-body>.dsh-mobile-control__frp-setup>.dsh-mobile-control__section-title{display:none}.dsh-mobile-control__provider-setup-body>.dsh-mobile-control__details{margin:0;padding:9px 10px;border:1px solid var(--dsw-alias-border-subtle,#dbe1e8);border-radius:11px;background:var(--dsw-alias-bg-layer-2,#fff)}.dsh-mobile-control__remote-workspace>.dsh-mobile-control__actions{margin-top:2px}.dsh-mobile-control__remote-workspace>.dsh-mobile-control__qr{margin:10px 0 0}.dsh-mobile-control__remote-workspace>.dsh-mobile-control__manage-row{margin-top:10px;padding-top:10px;border-top:1px solid var(--dsw-alias-border-subtle,#e1e5eb)}
5322
6018
  .dsh-mobile-control__frp-overview{display:grid;grid-template-columns:32px minmax(0,1fr);align-items:center;gap:10px;margin:0 0 10px;padding:10px;border:1px solid #a9dfc9;border-radius:11px;background:#edf9f4}.dsh-mobile-control__frp-overview[hidden]{display:none}.dsh-mobile-control__frp-overview-mark{display:grid;width:32px;height:32px;place-items:center;border-radius:50%;background:#087454;color:#fff;font:700 15px/1 system-ui}.dsh-mobile-control__frp-overview-body{display:flex;min-width:0;flex-direction:column;gap:2px}.dsh-mobile-control__frp-overview-body strong{color:#075d46;font-size:12px;line-height:1.35}.dsh-mobile-control__frp-overview-body span{overflow:hidden;color:#357061;font:10px/1.4 ui-monospace,SFMono-Regular,Consolas,monospace;text-overflow:ellipsis;white-space:nowrap}.dsh-mobile-control__frp-group{margin:8px 0 0;overflow:hidden;border:1px solid var(--dsw-alias-border-subtle,#dbe1e8);border-radius:11px;background:var(--dsw-alias-bg-layer-1,#f8fafc)}.dsh-mobile-control__frp-group>summary{box-sizing:border-box;min-height:44px;padding:12px 34px 10px 12px;color:var(--dsw-alias-label-primary,#16181d);font:650 12px/1.4 system-ui;cursor:pointer}.dsh-mobile-control__frp-group[open]>summary{border-bottom:1px solid var(--dsw-alias-border-subtle,#e1e5eb);background:var(--dsw-alias-bg-layer-2,#fff)}.dsh-mobile-control__frp-group>.dsh-mobile-control__frp-step{padding:12px}.dsh-mobile-control__frp-group>.dsh-mobile-control__frp-step>strong:first-child{display:none}.dsh-mobile-control__frp-group .dsh-mobile-control__frp-step{border-top:0}.dsh-mobile-control__frp-setup>.dsh-mobile-control__frp-step{margin-bottom:8px;padding:10px;border:1px solid var(--dsw-alias-border-subtle,#dbe1e8);border-radius:11px;background:var(--dsw-alias-bg-layer-1,#f8fafc)}
6019
+ .dsh-mobile-control__lan-setup{box-sizing:border-box;margin:0 0 12px;padding:14px;border:1px solid #6f96db;border-radius:15px;background:var(--dsw-alias-bg-layer-1,#f5f8ff)}.dsh-mobile-control__lan-setup[hidden],.dsh-mobile-control__lan-setup-form[hidden]{display:none}.dsh-mobile-control__lan-setup>.dsh-mobile-control__section-title{margin-bottom:5px}.dsh-mobile-control__lan-setup>.dsh-mobile-control__intro{margin-bottom:12px}.dsh-mobile-control__lan-setup-form{display:grid;gap:9px}.dsh-mobile-control__lan-setup .dsh-mobile-control__field select{box-sizing:border-box;width:100%;min-height:44px;padding:9px 32px 9px 10px;border:1px solid var(--dsw-alias-border-normal,#cfd5dd);border-radius:10px;background:var(--dsw-alias-bg-layer-2,#fff);color:var(--dsw-alias-label-primary,#16181d);font:14px/1.4 system-ui}.dsh-mobile-control__lan-setup .dsh-mobile-control__component-note{margin:0}.dsh-mobile-control__lan-setup-actions{display:grid;grid-template-columns:1fr 1.35fr;gap:8px}.dsh-mobile-control__lan-setup-actions button{box-sizing:border-box;min-width:0;min-height:44px;padding:9px 10px;border-radius:10px;font:650 12px/1.3 system-ui;cursor:pointer}.dsh-mobile-control__lan-setup-actions button:disabled{cursor:wait;opacity:.55}.dsh-mobile-control__lan-setup>.dsh-mobile-control__status{margin:10px 0 0;line-height:1.5}.dsh-mobile-control__lan-setup>.dsh-mobile-control__status:empty{display:none}
5323
6020
  .dsh-mobile-control__access{display:flex;align-items:baseline;gap:6px;min-width:0;margin:0 0 12px}.dsh-mobile-control__access[hidden]{display:none}.dsh-mobile-control__access-label{flex:none;color:var(--dsw-alias-label-secondary,#606873);white-space:nowrap}.dsh-mobile-control__access-label::after{content:":"}.dsh-mobile-control__access-link{min-width:0;overflow:hidden;color:#2563eb;text-decoration:none;text-overflow:ellipsis;white-space:nowrap}.dsh-mobile-control__access-link:hover{text-decoration:underline}.dsh-mobile-control__qr{display:flex;justify-content:center;margin:0 0 12px}.dsh-mobile-control__qr[hidden]{display:none}.dsh-mobile-control__qr img{border-radius:12px;background:#fff;padding:8px}
5324
6021
  .dsh-mobile-control__status{margin:0 0 14px;overflow-wrap:anywhere;color:var(--dsw-alias-label-secondary,#606873)}.dsh-mobile-control__status::before{display:inline-block;width:8px;height:8px;margin-right:7px;border-radius:50%;background:#98a1ad;content:""}.dsh-mobile-control__status.is-running::before{background:#16a36a}.dsh-mobile-control__status.is-key{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:12px;word-break:break-all}
5325
6022
  .dsh-mobile-control__guide{margin:0 0 14px;padding:12px;border:1px solid #6f96db;border-radius:12px;background:var(--dsw-alias-bg-layer-1,#eff6ff)}.dsh-mobile-control__guide[hidden]{display:none}.dsh-mobile-control__guide-title{margin:0;color:var(--dsw-alias-label-primary,#172554);font:650 13px/1.45 system-ui}.dsh-mobile-control__guide-summary,.dsh-mobile-control__guide-note{margin:4px 0 0;color:var(--dsw-alias-label-secondary,#475569);font-size:12px;line-height:1.5}.dsh-mobile-control__guide-steps{margin:8px 0 0;padding-left:20px;color:var(--dsw-alias-label-primary,#1e293b);font-size:12px;line-height:1.6}.dsh-mobile-control__guide-note{color:var(--dsw-alias-label-secondary,#64748b)}.dsh-mobile-control__guide-actions{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:10px}.dsh-mobile-control__guide-actions button{min-width:0;min-height:44px;padding:8px;border-radius:10px;font:12px/1.25 system-ui;cursor:pointer}.dsh-mobile-control__guide-actions button:disabled{cursor:not-allowed;opacity:.45}
@@ -5330,8 +6027,8 @@ window.__ModuleLoader__.load({
5330
6027
  .dsh-mobile-control__diagnostic-checks{display:grid;gap:12px;margin-top:12px;animation:dsh-diagnostic-reveal 160ms ease-out both}.dsh-mobile-control__diagnostic-checks[hidden]{display:none}.dsh-mobile-control__diagnostic-group{overflow:hidden;border:1px solid var(--dsw-alias-border-subtle,#dbe1e8);border-radius:13px;background:var(--dsw-alias-bg-layer-2,#fff)}.dsh-mobile-control__diagnostic-group-header{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:8px 11px;border-bottom:1px solid var(--dsw-alias-border-subtle,#e1e5eb);background:var(--dsw-alias-bg-layer-1,#f8fafc)}.dsh-mobile-control__diagnostic-group-header h3{margin:0;font:650 11px/1.4 system-ui}.dsh-mobile-control__diagnostic-group-header span{color:var(--dsw-alias-label-secondary,#606873);font-size:10px}.dsh-mobile-control__diagnostic-list{display:flex;flex-direction:column}.dsh-mobile-control__diagnostic-check{display:grid;grid-template-columns:26px minmax(0,1fr);gap:9px;padding:11px;background:var(--dsw-alias-bg-layer-2,#fff)}.dsh-mobile-control__diagnostic-check + .dsh-mobile-control__diagnostic-check{border-top:1px solid var(--dsw-alias-border-subtle,#e1e5eb)}.dsh-mobile-control__diagnostic-marker{position:relative;width:26px;height:26px;border-radius:50%;background:#edf1f5;color:var(--dsh-diagnostic-info)}.dsh-mobile-control__diagnostic-marker::before,.dsh-mobile-control__diagnostic-marker::after{position:absolute;content:""}.dsh-mobile-control__diagnostic-check.is-ok .dsh-mobile-control__diagnostic-marker{background:#e6f7f0;color:var(--dsh-diagnostic-ok)}.dsh-mobile-control__diagnostic-check.is-ok .dsh-mobile-control__diagnostic-marker::before{top:7px;left:7px;width:9px;height:5px;border-bottom:1.8px solid currentColor;border-left:1.8px solid currentColor;transform:rotate(-45deg)}.dsh-mobile-control__diagnostic-check.is-warning .dsh-mobile-control__diagnostic-marker{background:#fff4dc;color:var(--dsh-diagnostic-warning)}.dsh-mobile-control__diagnostic-check.is-error .dsh-mobile-control__diagnostic-marker{background:#fdecec;color:var(--dsh-diagnostic-error)}.dsh-mobile-control__diagnostic-check.is-warning .dsh-mobile-control__diagnostic-marker::before,.dsh-mobile-control__diagnostic-check.is-error .dsh-mobile-control__diagnostic-marker::before{top:6px;left:12px;width:2px;height:9px;border-radius:2px;background:currentColor}.dsh-mobile-control__diagnostic-check.is-warning .dsh-mobile-control__diagnostic-marker::after,.dsh-mobile-control__diagnostic-check.is-error .dsh-mobile-control__diagnostic-marker::after{bottom:6px;left:12px;width:2px;height:2px;border-radius:50%;background:currentColor}.dsh-mobile-control__diagnostic-check.is-info .dsh-mobile-control__diagnostic-marker::before{top:6px;left:12px;width:2px;height:2px;border-radius:50%;background:currentColor}.dsh-mobile-control__diagnostic-check.is-info .dsh-mobile-control__diagnostic-marker::after{top:10px;left:12px;width:2px;height:9px;border-radius:2px;background:currentColor}.dsh-mobile-control__diagnostic-check-body{min-width:0}.dsh-mobile-control__diagnostic-check-header{display:flex;align-items:flex-start;justify-content:space-between;gap:8px}.dsh-mobile-control__diagnostic-check-header strong{min-width:0;font-size:12px;line-height:1.4}.dsh-mobile-control__diagnostic-badge{flex:none;padding:2px 6px;border-radius:999px;background:#edf1f5;color:var(--dsh-diagnostic-info);font:650 10px/1.3 system-ui}.dsh-mobile-control__diagnostic-check.is-ok .dsh-mobile-control__diagnostic-badge{background:#e6f7f0;color:var(--dsh-diagnostic-ok)}.dsh-mobile-control__diagnostic-check.is-warning .dsh-mobile-control__diagnostic-badge{background:#fff4dc;color:var(--dsh-diagnostic-warning)}.dsh-mobile-control__diagnostic-check.is-error .dsh-mobile-control__diagnostic-badge{background:#fdecec;color:var(--dsh-diagnostic-error)}.dsh-mobile-control__diagnostic-check p{margin:4px 0 0;color:var(--dsw-alias-label-secondary,#606873);font-size:11px;line-height:1.5;overflow-wrap:anywhere}.dsh-mobile-control__diagnostic-check .dsh-mobile-control__diagnostic-action{margin-top:7px;padding:7px 8px;border-radius:8px;background:var(--dsw-alias-bg-layer-1,#f8fafc);color:var(--dsw-alias-label-primary,#16181d)}.dsh-mobile-control__diagnostic-action span{display:inline-block;margin-right:6px;color:#2563eb;font-weight:700}.dsh-mobile-control__diagnostic-details{margin-top:12px}.dsh-mobile-control__diagnostic-details[hidden]{display:none}.dsh-mobile-control__diagnostic-details>summary{box-sizing:border-box;min-height:44px;line-height:44px}.dsh-mobile-control__diagnostic-report{box-sizing:border-box;max-height:220px;margin:4px 0 0;overflow:auto;padding:10px;border:1px solid var(--dsw-alias-border-subtle,#dbe1e8);border-radius:10px;background:var(--dsw-alias-bg-layer-1,#f3f5f8);color:var(--dsw-alias-label-secondary,#606873);font:10px/1.55 ui-monospace,SFMono-Regular,Consolas,monospace;white-space:pre-wrap;overflow-wrap:anywhere}
5331
6028
  .dsh-mobile-control__diagnostic-checks{transition:opacity 150ms ease}.dsh-mobile-control__diagnostic-checks.is-refreshing{opacity:.52}
5332
6029
  @keyframes dsh-diagnostic-spin{to{transform:rotate(360deg)}}@keyframes dsh-diagnostic-reveal{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}
5333
- .dsh-mobile-control__actions{display:flex;flex-wrap:nowrap;gap:6px}.dsh-mobile-control__actions button{flex:1 1 0;min-width:0;min-height:40px;padding:8px 4px;border-radius:10px;font:12px/1.2 system-ui;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dsh-mobile-control__secondary{border:1px solid var(--dsw-alias-border-normal,#cfd5dd);background:transparent;color:inherit}.dsh-mobile-control__primary{border:1px solid #2563eb;background:#2563eb;color:#fff}.dsh-mobile-control__actions button:disabled{cursor:not-allowed;opacity:.45}
5334
- .dsh-mobile-control button:focus-visible,.dsh-mobile-control a:focus-visible,.dsh-mobile-control input:focus-visible,.dsh-mobile-control summary:focus-visible{outline:3px solid rgb(37 99 235 / 28%);outline-offset:2px}
6030
+ .dsh-mobile-control__actions{display:flex;flex-wrap:nowrap;gap:6px}.dsh-mobile-control__actions[hidden],.dsh-mobile-control__manage-row[hidden]{display:none}.dsh-mobile-control__actions button{flex:1 1 0;min-width:0;min-height:40px;padding:8px 4px;border-radius:10px;font:12px/1.2 system-ui;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dsh-mobile-control__secondary{border:1px solid var(--dsw-alias-border-normal,#cfd5dd);background:transparent;color:inherit}.dsh-mobile-control__primary{border:1px solid #2563eb;background:#2563eb;color:#fff}.dsh-mobile-control__actions button:disabled{cursor:not-allowed;opacity:.45}
6031
+ .dsh-mobile-control button:focus-visible,.dsh-mobile-control a:focus-visible,.dsh-mobile-control input:focus-visible,.dsh-mobile-control select:focus-visible,.dsh-mobile-control summary:focus-visible{outline:3px solid rgb(37 99 235 / 28%);outline-offset:2px}
5335
6032
  .dsh-mobile-control__trigger{box-sizing:border-box;flex:1 1 auto;display:flex;align-items:center;gap:8px;width:100%;height:42px;margin:4px 0;padding:0 10px 0 8px;border:0;border-radius:12px;background:transparent;color:var(--dsw-alias-label-primary,#16181d);font-family:inherit;font-size:14px;line-height:22px;cursor:pointer;overflow:hidden}.dsh-mobile-control__trigger:hover{background:var(--dsw-alias-interactive-bg-hover,#f1f3f6)}.dsh-mobile-control__trigger:active,.dsh-mobile-control__trigger[aria-expanded="true"]{background:var(--dsw-alias-interactive-bg-active,#e8ebf0)}.dsh-mobile-control__trigger:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,currentColor);outline-offset:2px}.dsh-mobile-control__trigger.is-rail{flex:0 0 auto;width:36px;height:36px;margin:8px 0 10px;padding:0;justify-content:center;gap:0;border-radius:50%}.dsh-mobile-control__trigger-icon{display:block;flex:none}.dsh-mobile-control__trigger-label{min-width:0;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}
5336
6033
  .dsh-mobile-control__manage-row{display:flex;justify-content:space-between;gap:8px;margin-top:10px}.dsh-mobile-control__manage{flex:1 1 0;min-width:0;min-height:34px;padding:6px 8px;border:1px solid var(--dsw-alias-border-normal,#cfd5dd);border-radius:10px;background:transparent;color:inherit;font:12px/1.3 system-ui;cursor:pointer}.dsh-mobile-control__devices{margin-top:10px;border:1px solid var(--dsw-alias-border-subtle,#e1e5eb);border-radius:10px;padding:8px;max-height:220px;overflow-y:auto}.dsh-mobile-control__device-empty{color:var(--dsw-alias-label-secondary,#606873);font-size:12px;margin:0}.dsh-mobile-control__device{display:flex;align-items:center;gap:8px;padding:6px 2px}.dsh-mobile-control__device + .dsh-mobile-control__device{border-top:1px solid var(--dsw-alias-border-subtle,#e1e5eb)}.dsh-mobile-control__device-label{flex:1 1 0;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12px}.dsh-mobile-control__device-meta{flex:none;color:var(--dsw-alias-label-secondary,#606873);font-size:11px;white-space:nowrap}.dsh-mobile-control__device-revoke{flex:none;min-height:28px;padding:4px 8px;border:1px solid #dc2626;border-radius:8px;background:transparent;color:#dc2626;font:12px/1.2 system-ui;cursor:pointer}.dsh-mobile-control__ws-paths{margin:10px 0 0;padding:12px;border:1px solid var(--dsw-alias-border-subtle,#dbe1e8);border-radius:13px;background:var(--dsw-alias-bg-layer-2,#fff)}.dsh-mobile-control__ws-paths-list{margin:0 0 8px;padding:0;list-style:none}.dsh-mobile-control__ws-paths-empty{color:var(--dsw-alias-label-secondary,#606873);font-size:12px}.dsh-mobile-control__ws-paths-item{display:flex;align-items:center;gap:8px;padding:6px 0}.dsh-mobile-control__ws-paths-item + .dsh-mobile-control__ws-paths-item{border-top:1px solid var(--dsw-alias-border-subtle,#e1e5eb)}.dsh-mobile-control__ws-paths-item code{flex:1 1 0;min-width:0;overflow:hidden;font:11px/1.4 ui-monospace,SFMono-Regular,Consolas,monospace;text-overflow:ellipsis;white-space:nowrap}.dsh-mobile-control__ws-paths-remove{flex:none;min-height:28px;padding:4px 8px;border:1px solid var(--dsw-alias-border-normal,#cfd5dd);border-radius:8px;background:transparent;color:inherit;font:12px/1.2 system-ui;cursor:pointer}.dsh-mobile-control__trigger,.dsh-mobile-control__diagnostic-entry{position:relative}.dsh-mobile-control__ws-dot{position:absolute;top:2px;right:2px;z-index:1;min-width:16px;height:16px;padding:0 4px;border-radius:999px;background:#dc2626;color:#fff;font:650 10px/16px system-ui;text-align:center;cursor:pointer}.dsh-mobile-control__ws-dot::after{content:'';position:absolute;inset:-5px}.dsh-mobile-control__ws-sr-status{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}.dsh-mobile-control__ws-paths-row{display:flex;gap:8px}.dsh-mobile-control__ws-paths-row .dsh-mobile-control__primary{width:auto;flex:none;padding:9px 14px;border-radius:10px}.dsh-mobile-control__ws-paths-group{padding:6px 0}.dsh-mobile-control__ws-paths-group + .dsh-mobile-control__ws-paths-group{border-top:1px solid var(--dsw-alias-border-subtle,#e1e5eb)}.dsh-mobile-control__ws-paths-group-head{display:flex;align-items:center;gap:8px}.dsh-mobile-control__ws-count{flex:none;padding:1px 7px;border-radius:999px;background:#e8f0ff;color:#1d4ed8;font:650 10px/1.6 system-ui}.dsh-mobile-control__ws-paths-item.is-child{padding-left:12px}.dsh-mobile-control__ws-paths-group-head .dsh-mobile-control__primary,.dsh-mobile-control__ws-paths-item .dsh-mobile-control__primary{width:auto;flex:none;min-height:34px;padding:6px 12px;border-radius:10px}.dsh-mobile-control__ws-paths-row input{flex:1 1 0;min-width:0;box-sizing:border-box;min-height:44px;padding:9px 10px;border:1px solid var(--dsw-alias-border-normal,#cfd5dd);border-radius:10px;background:var(--dsw-alias-bg-layer-2,#fff);color:var(--dsw-alias-label-primary,#16181d);font:16px/1.4 system-ui}.dsh-mobile-control__ws-paths-row button{flex:none}.dsh-mobile-control__ws-paths-detected-title{margin:0 0 2px;font:600 12px/1.4 system-ui}.dsh-mobile-control__ws-paths-advanced{margin-top:8px}.dsh-mobile-control__ws-paths-advanced summary{cursor:pointer;color:var(--dsw-alias-label-secondary,#606873);font:12px/1.4 system-ui;user-select:none}.dsh-mobile-control__ws-paths-advanced summary:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,currentColor);outline-offset:2px;border-radius:4px}.dsh-mobile-control__ws-paths-advanced[open] summary{margin-bottom:6px}.dsh-mobile-control__ws-paths-advanced .dsh-mobile-control__status{margin:6px 0 0}
5337
6034
  @media (max-width:359px){.dsh-mobile-control__provider-choices{grid-template-columns:1fr}.dsh-mobile-control__provider{min-height:68px}}@media (prefers-reduced-motion:reduce){.dsh-mobile-control__provider,.dsh-mobile-control__cpolar-connect{transition:none}.dsh-mobile-control__diagnostic-summary.is-running .dsh-mobile-control__diagnostic-summary-icon::before,.dsh-mobile-control__diagnostic-checks{animation:none}}
@@ -5343,15 +6040,21 @@ window.__ModuleLoader__.load({
5343
6040
  return trustAuthenticatedGatewayConnection(ctx.get("connection"));
5344
6041
  }, "dsh-mobile: authenticated gateway client trust");
5345
6042
  ctx.effect(() => {
5346
- const loopback = isLoopbackHost(location.hostname) && !new URLSearchParams(location.search).has("dsh-mobile-preview");
6043
+ const desktopAdmin = isDesktopAdminSurface(location.hostname, location.search, window.__DSH_MOBILE_FRONTEND__);
5347
6044
  const style = element("style");
5348
6045
  style.dataset.plugin = "dsh-mobile";
5349
- style.textContent = loopback ? CONTROL_STYLES : NATIVE_MOBILE_STYLES;
6046
+ style.textContent = desktopAdmin ? CONTROL_STYLES : NATIVE_MOBILE_STYLES;
5350
6047
  document.head.append(style);
5351
- if (!loopback) {
6048
+ if (!desktopAdmin) {
6049
+ const removeSettingsAction = ctx.slots.inject("settings.general.item", () => ctx.slots.register({
6050
+ name: "settings.general.item",
6051
+ id: "dsh-mobile-switch-computer",
6052
+ order: 100
6053
+ }, MobileSwitchComputerRow));
5352
6054
  const removeCustom = installCustomAssets();
5353
6055
  const removeSurface = installDshLanguageBoundSurface(installNativeMobileSurface);
5354
6056
  return () => {
6057
+ removeSettingsAction();
5355
6058
  removeCustom();
5356
6059
  removeSurface();
5357
6060
  style.remove();
@@ -5408,6 +6111,7 @@ window.__ModuleLoader__.load({
5408
6111
  //#endregion
5409
6112
  exports.CONTROL_STYLES = CONTROL_STYLES;
5410
6113
  exports.DIAGNOSTIC_REASON_MESSAGES = DIAGNOSTIC_REASON_MESSAGES;
6114
+ exports.LOCALIZED_DIAGNOSTIC_COPY = LOCALIZED_DIAGNOSTIC_COPY;
5411
6115
  exports.MOBILE_CONTROL_MESSAGES = MOBILE_CONTROL_MESSAGES;
5412
6116
  exports.PerIdActivationLifecycle = PerIdActivationLifecycle;
5413
6117
  exports.apply = apply;
@@ -5419,6 +6123,7 @@ window.__ModuleLoader__.load({
5419
6123
  exports.diagnosticEntriesForRender = diagnosticEntriesForRender;
5420
6124
  exports.diagnosticOverallForChecks = diagnosticOverallForChecks;
5421
6125
  exports.diagnosticServerCopy = diagnosticServerCopy;
6126
+ exports.extensionActionRequestInit = extensionActionRequestInit;
5422
6127
  exports.extensionAssetUrl = extensionAssetUrl;
5423
6128
  exports.extensionGenerationHeaders = extensionGenerationHeaders;
5424
6129
  exports.extensionRouteUrl = extensionRouteUrl;