dsh-mobile 0.3.15 → 0.4.0

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
@@ -83,6 +83,7 @@ window.__ModuleLoader__.load({
83
83
  continueLogin: "Continue login",
84
84
  reconnect: "Reconnect",
85
85
  generateRemoteQr: "Generate remote pairing QR code",
86
+ copyRemoteLink: "Copy remote pairing link",
86
87
  manageRemoteDevices: "Manage remote devices",
87
88
  resetRemoteLogin: "Sign out and clear remote login",
88
89
  resetRemoteDevices: "Disable and clear remote devices",
@@ -250,6 +251,7 @@ window.__ModuleLoader__.load({
250
251
  continueLogin: "Continua accesso",
251
252
  reconnect: "Riconnetti",
252
253
  generateRemoteQr: "Genera QR di abbinamento remoto",
254
+ copyRemoteLink: "Copia link di abbinamento remoto",
253
255
  manageRemoteDevices: "Gestisci dispositivi remoti",
254
256
  resetRemoteLogin: "Esci e rimuovi accesso remoto",
255
257
  resetRemoteDevices: "Disattiva e rimuovi dispositivi remoti",
@@ -419,6 +421,7 @@ window.__ModuleLoader__.load({
419
421
  continueLogin: "继续登录",
420
422
  reconnect: "重新连接",
421
423
  generateRemoteQr: "生成远程配对二维码",
424
+ copyRemoteLink: "复制远程配对链接",
422
425
  manageRemoteDevices: "管理远程设备",
423
426
  resetRemoteLogin: "退出并清除远程登录",
424
427
  resetRemoteDevices: "关闭并清除远程设备",
@@ -510,6 +513,51 @@ window.__ModuleLoader__.load({
510
513
  wsPathsAnnounce: "{n} 条插件连接等待放行",
511
514
  wsPathsAdvancedHint: "通常不需要手动填写:用上方按钮即可。只有当你确切知道所需路径时才填。"
512
515
  });
516
+ Object.assign(MOBILE_CONTROL_MESSAGES.en, {
517
+ lanSetupTitle: "Finish local network setup",
518
+ lanSetupIntro: "Choose the network shared by this computer and your phone. DSH Mobile creates private certificates only after you confirm.",
519
+ lanSetupNetwork: "Computer network",
520
+ lanSetupChooseNetwork: "Choose a network",
521
+ lanSetupRecommended: "Recommended",
522
+ lanSetupNoNetworks: "No private Wi-Fi or Ethernet address was found. Connect this computer to the local network, then scan again.",
523
+ lanSetupRefresh: "Scan again",
524
+ lanSetupFirewallNote: "Windows may request administrator approval for two inbound rules limited to the local subnet.",
525
+ lanSetupConfigure: "Configure local network",
526
+ lanSetupConfiguring: "Creating private certificates and local network rules…",
527
+ lanSetupRequired: "Local network setup has not been completed. Choose a network above to continue.",
528
+ lanSetupRestart: "Setup complete for {origin}. Restart DSH once to start local network access.",
529
+ lanSetupFailed: "Setup was not completed. Confirm the network selection and approve the Windows prompt, then try again."
530
+ });
531
+ Object.assign(MOBILE_CONTROL_MESSAGES.it, {
532
+ lanSetupTitle: "Completa la configurazione della rete locale",
533
+ lanSetupIntro: "Scegli la rete condivisa da questo computer e dal telefono. DSH Mobile crea certificati privati solo dopo la conferma.",
534
+ lanSetupNetwork: "Rete del computer",
535
+ lanSetupChooseNetwork: "Scegli una rete",
536
+ lanSetupRecommended: "Consigliata",
537
+ lanSetupNoNetworks: "Nessun indirizzo Wi-Fi o Ethernet privato trovato. Collega il computer alla rete locale e ripeti la ricerca.",
538
+ lanSetupRefresh: "Cerca di nuovo",
539
+ lanSetupFirewallNote: "Windows può richiedere l’approvazione amministratore per due regole in ingresso limitate alla sottorete locale.",
540
+ lanSetupConfigure: "Configura rete locale",
541
+ lanSetupConfiguring: "Creazione dei certificati privati e delle regole di rete locale…",
542
+ lanSetupRequired: "La configurazione della rete locale non è completa. Scegli una rete qui sopra per continuare.",
543
+ lanSetupRestart: "Configurazione completata per {origin}. Riavvia DSH una volta per attivare l’accesso locale.",
544
+ lanSetupFailed: "Configurazione non completata. Controlla la rete e approva la richiesta di Windows, quindi riprova."
545
+ });
546
+ Object.assign(MOBILE_CONTROL_MESSAGES.zh, {
547
+ lanSetupTitle: "完成局域网配置",
548
+ lanSetupIntro: "选择电脑与手机共同使用的网络。确认后才会生成私有证书,不会在安装时自动开放端口。",
549
+ lanSetupNetwork: "电脑当前网络",
550
+ lanSetupChooseNetwork: "请选择网络",
551
+ lanSetupRecommended: "推荐",
552
+ lanSetupNoNetworks: "未找到可用的私有 Wi-Fi 或以太网地址。请先让电脑接入局域网,然后重新扫描。",
553
+ lanSetupRefresh: "重新扫描",
554
+ lanSetupFirewallNote: "Windows 可能请求管理员授权,仅创建两条限本地子网访问的入站规则。",
555
+ lanSetupConfigure: "完成配置",
556
+ lanSetupConfiguring: "正在生成私有证书并配置局域网规则…",
557
+ lanSetupRequired: "尚未完成局域网配置,请选择上方网络继续。",
558
+ lanSetupRestart: "已为 {origin} 完成配置。请重启一次 DSH,局域网访问会自动开启。",
559
+ lanSetupFailed: "配置未完成。请确认网卡选择并允许 Windows 授权提示,然后重试。"
560
+ });
513
561
  Object.assign(MOBILE_CONTROL_MESSAGES.en, {
514
562
  selfHostedConnections: "Self-hosted connection",
515
563
  selfHostedDescription: "For users with a VPS and domain. It does not depend on public tunnel bandwidth.",
@@ -966,12 +1014,32 @@ window.__ModuleLoader__.load({
966
1014
  generated: "Generato"
967
1015
  },
968
1016
  zh: {
1017
+ versions: "已显示插件、DSH 和 Android App 最低版本。",
1018
+ networkOk: "已配置的局域网网卡可用。",
1019
+ networkError: "已保存的局域网网卡不可用。",
1020
+ networkInfo: "当前使用固定局域网配置。",
1021
+ networkAction: "重新运行 dsh-mobile setup。",
1022
+ lanOk: "局域网网关正在监听,配对入口可用。",
1023
+ lanInfo: "局域网访问当前未开启。",
1024
+ lanAction: "需要手机直连时开启局域网访问。",
1025
+ firewallOk: "局域网 TCP 与发现防火墙规则已启用。",
1026
+ firewallWarning: "未找到完整的局域网防火墙规则。",
1027
+ firewallInfo: "系统未允许插件读取防火墙状态。",
1028
+ firewallAction: "以管理员身份运行 dsh-mobile setup 并检查防火墙规则。",
1029
+ remoteOk: "远程公网地址已通过可达性检查。",
1030
+ remoteWarning: "远程通道需要处理或仍在连接。",
1031
+ remoteError: "远程连接当前不可达。",
1032
+ remoteInfo: "远程访问当前未开启。",
1033
+ remoteAction: "返回远程访问,按提供方提示操作并重新连接。",
1034
+ phone: "电脑无法判断路由器是否隔离了手机。",
1035
+ phoneAction: "确认手机与电脑使用同一网络,并关闭访客网络或 AP 隔离。",
969
1036
  reportTitle: "DSH Mobile 诊断报告",
970
1037
  generated: "生成时间"
971
1038
  }
972
1039
  };
973
1040
  const DIAGNOSTIC_REASON_MESSAGES = {
974
1041
  en: {
1042
+ "lan-setup-required": ["Local network setup has not been completed.", "Return to Local network and choose a network to finish setup."],
975
1043
  "versions-current": ["Installed plugin, DSH, and minimum Android app versions are shown.", ""],
976
1044
  "network-unavailable": ["The saved local network adapter is unavailable.", "Run dsh-mobile setup again."],
977
1045
  "network-interface": ["Using network interface {interfaceName}.", ""],
@@ -992,6 +1060,7 @@ window.__ModuleLoader__.load({
992
1060
  "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
1061
  },
994
1062
  it: {
1063
+ "lan-setup-required": ["La configurazione della rete locale non è completa.", "Torna a Rete locale e scegli una rete per completare la configurazione."],
995
1064
  "versions-current": ["Sono indicate le versioni installate di plugin e DSH e la versione minima dell’app Android.", ""],
996
1065
  "network-unavailable": ["La scheda di rete locale salvata non è disponibile.", "Esegui di nuovo dsh-mobile setup."],
997
1066
  "network-interface": ["È in uso l’interfaccia di rete {interfaceName}.", ""],
@@ -1012,6 +1081,7 @@ window.__ModuleLoader__.load({
1012
1081
  "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
1082
  },
1014
1083
  zh: {
1084
+ "lan-setup-required": ["尚未完成局域网初始化。", "返回局域网页选择网卡并完成配置。"],
1015
1085
  "versions-current": ["已显示插件、DSH 和 Android App 最低版本。", ""],
1016
1086
  "network-unavailable": ["已保存的局域网网卡当前不可用。", "重新运行 dsh-mobile setup。"],
1017
1087
  "network-interface": ["正在使用网卡 {interfaceName}。", ""],
@@ -1129,6 +1199,304 @@ window.__ModuleLoader__.load({
1129
1199
  if (publicIpv4Address(publicHost)) lines.push(manualIpCertificateGuide(publicHost), "");
1130
1200
  return lines.join("\n");
1131
1201
  }
1202
+ /** Wire bounds mirrored by the Android notification policy. */
1203
+ const TASK_NOTIFY_LIMITS = Object.freeze({
1204
+ title: 80,
1205
+ body: 200,
1206
+ tag: 64
1207
+ });
1208
+ const MAX_SEEN_QUESTION_KEYS = 20;
1209
+ function sanitizeTagFragment(value) {
1210
+ return value.toLowerCase().replaceAll(/[^a-z0-9-]+/gu, "-").replaceAll(/^-+|-+$/gu, "").slice(0, TASK_NOTIFY_LIMITS.tag);
1211
+ }
1212
+ /** Stable per-turn tag so concurrent sessions keep separate notifications. */
1213
+ function taskCompletionTag(sessionId, turn) {
1214
+ return `dsh-task-done-${sanitizeTagFragment(`${sessionId}-${String(turn)}`) || "task"}`.slice(0, TASK_NOTIFY_LIMITS.tag);
1215
+ }
1216
+ /** Decide whether the current snapshot deserves a system notification. */
1217
+ var TaskNotifyTracker = class {
1218
+ lastActivityAt;
1219
+ notifiedDoneAt;
1220
+ wasBusy = false;
1221
+ busySince;
1222
+ idleAnchoredAt;
1223
+ seenQuestionKeys = /* @__PURE__ */ new Set();
1224
+ quietMs;
1225
+ graceMs;
1226
+ minBusyMs;
1227
+ now;
1228
+ format;
1229
+ constructor(options) {
1230
+ this.quietMs = options.quietMs ?? 9e4;
1231
+ this.graceMs = options.graceMs ?? 1e4;
1232
+ this.minBusyMs = options.minBusyMs ?? 2e4;
1233
+ this.now = options.now ?? Date.now;
1234
+ this.format = options.format;
1235
+ }
1236
+ /** Record assistant-side activity; re-arms completion after a notification. */
1237
+ markActivity() {
1238
+ const now = this.now();
1239
+ this.lastActivityAt = now;
1240
+ if (this.notifiedDoneAt !== void 0 && this.notifiedDoneAt <= now) this.notifiedDoneAt = void 0;
1241
+ }
1242
+ /**
1243
+ * Milliseconds until a pending completion anchor fires, or undefined when
1244
+ * no anchor exists. Lets the watcher schedule one exact timer instead of
1245
+ * waiting for the next coarse poll tick.
1246
+ */
1247
+ pendingAnchorDelayMs() {
1248
+ if (this.idleAnchoredAt === void 0) return void 0;
1249
+ return Math.max(0, this.graceMs - (this.now() - this.idleAnchoredAt));
1250
+ }
1251
+ evaluate(snapshot) {
1252
+ const now = this.now();
1253
+ if (!this.wasBusy && snapshot.composerBusy) this.busySince = now;
1254
+ if (this.wasBusy && !snapshot.composerBusy) {
1255
+ const busyFor = this.busySince === void 0 ? 0 : now - this.busySince;
1256
+ this.busySince = void 0;
1257
+ if (snapshot.pageHidden && !snapshot.pendingQuestion && busyFor >= this.minBusyMs) {
1258
+ this.idleAnchoredAt = void 0;
1259
+ this.notifiedDoneAt = now;
1260
+ return this.event("done", snapshot.sessionLabel, void 0);
1261
+ }
1262
+ this.idleAnchoredAt = snapshot.pageHidden ? now : void 0;
1263
+ if (!snapshot.pageHidden) this.notifiedDoneAt = now;
1264
+ }
1265
+ this.wasBusy = snapshot.composerBusy;
1266
+ if (snapshot.composerBusy) this.idleAnchoredAt = void 0;
1267
+ else if (this.idleAnchoredAt !== void 0 && !snapshot.pageHidden) {
1268
+ this.idleAnchoredAt = void 0;
1269
+ this.notifiedDoneAt = now;
1270
+ }
1271
+ if (!snapshot.pageHidden) {
1272
+ if (snapshot.pendingQuestion && snapshot.questionKey !== void 0) this.rememberQuestionKey(snapshot.questionKey);
1273
+ return;
1274
+ }
1275
+ if (snapshot.pendingQuestion) {
1276
+ if (snapshot.questionKey === void 0 || this.seenQuestionKeys.has(snapshot.questionKey)) return void 0;
1277
+ this.rememberQuestionKey(snapshot.questionKey);
1278
+ return this.event("question", snapshot.sessionLabel, snapshot.questionKey);
1279
+ }
1280
+ if (this.idleAnchoredAt !== void 0) {
1281
+ if (now - this.idleAnchoredAt < this.graceMs) return void 0;
1282
+ this.idleAnchoredAt = void 0;
1283
+ this.notifiedDoneAt = now;
1284
+ return this.event("done", snapshot.sessionLabel, void 0);
1285
+ }
1286
+ if (this.lastActivityAt === void 0) return void 0;
1287
+ if (now - this.lastActivityAt < this.quietMs) return void 0;
1288
+ if (now - this.lastActivityAt > 18e5) {
1289
+ this.lastActivityAt = void 0;
1290
+ return;
1291
+ }
1292
+ if (this.notifiedDoneAt !== void 0 && this.notifiedDoneAt >= this.lastActivityAt) return void 0;
1293
+ this.notifiedDoneAt = now;
1294
+ return this.event("done", snapshot.sessionLabel, void 0);
1295
+ }
1296
+ rememberQuestionKey(key) {
1297
+ this.seenQuestionKeys.add(key);
1298
+ if (this.seenQuestionKeys.size > MAX_SEEN_QUESTION_KEYS) {
1299
+ const oldest = this.seenQuestionKeys.values().next();
1300
+ if (!oldest.done) this.seenQuestionKeys.delete(oldest.value);
1301
+ }
1302
+ }
1303
+ event(kind, sessionLabel, questionKey) {
1304
+ const text = this.format(kind, sessionLabel);
1305
+ const tag = kind === "question" && questionKey !== void 0 ? `dsh-task-question-${sanitizeTagFragment(questionKey) || "pending"}` : "dsh-task-done";
1306
+ return Object.freeze({
1307
+ kind,
1308
+ title: text.title,
1309
+ body: text.body,
1310
+ tag
1311
+ });
1312
+ }
1313
+ };
1314
+ /**
1315
+ * Mirror of the composer busy signal used for media actions: the run is busy
1316
+ * while the composer is aria-busy, disabled, read-only, or non-interactive.
1317
+ */
1318
+ function readComposerBusyState(root) {
1319
+ if (typeof root.querySelector !== "function") return false;
1320
+ const card = root.querySelector("[data-composer-card]");
1321
+ if (card === null || typeof card.getAttribute !== "function" || typeof card.querySelector !== "function") return false;
1322
+ if (card.getAttribute("aria-busy") === "true") return true;
1323
+ const field = card.querySelector("textarea,input,[contenteditable=\"true\"],[contenteditable=\"plaintext-only\"]");
1324
+ if (field === null) return false;
1325
+ return field.disabled === true || field.readOnly === true || field.getAttribute?.("aria-disabled") === "true";
1326
+ }
1327
+ /**
1328
+ * Best-effort session label for the notification body, bounded for the wire.
1329
+ * Callers accept that titles may name the active session on the lock screen.
1330
+ */
1331
+ function readSessionLabel(title) {
1332
+ if (typeof title !== "string") return "";
1333
+ return title.normalize("NFC").replace(/[\u0000-\u001f\u007f]+/gu, " ").trim().slice(0, 80);
1334
+ }
1335
+ /** Whether the document currently shows a card that waits for user input. */
1336
+ function hasPendingInputQuestion(root) {
1337
+ if (typeof root.querySelector !== "function") return false;
1338
+ return root.querySelector("[data-question-key],[data-plan-review-key]") !== null;
1339
+ }
1340
+ /** Stable key of the waiting input card, when the DOM exposes one. */
1341
+ function pendingInputQuestionKey(root) {
1342
+ if (typeof root.querySelector !== "function") return void 0;
1343
+ const card = root.querySelector("[data-question-key],[data-plan-review-key]");
1344
+ const getAttribute = card?.getAttribute;
1345
+ if (typeof getAttribute !== "function") return void 0;
1346
+ const key = getAttribute.call(card, "data-question-key") ?? getAttribute.call(card, "data-plan-review-key");
1347
+ if (key === null || key === "") return void 0;
1348
+ return key;
1349
+ }
1350
+ /** Observe assistant-side conversation mutations; returns a disposer. */
1351
+ function observeTaskActivity(target, onActivity, observe = typeof MutationObserver === "function" ? MutationObserver : void 0) {
1352
+ if (observe === void 0) return () => void 0;
1353
+ const observer = new observe(() => {
1354
+ onActivity();
1355
+ });
1356
+ try {
1357
+ observer.observe(target, {
1358
+ childList: true,
1359
+ characterData: true,
1360
+ subtree: true
1361
+ });
1362
+ } catch {
1363
+ return () => void 0;
1364
+ }
1365
+ return () => {
1366
+ observer.disconnect();
1367
+ };
1368
+ }
1369
+ function readNativeBridge() {
1370
+ if (typeof window === "undefined") return void 0;
1371
+ const bridge = window.__DSH_MOBILE_NATIVE__;
1372
+ if (bridge === void 0 || typeof bridge.capabilities !== "function" || typeof bridge.invoke !== "function") return void 0;
1373
+ return bridge;
1374
+ }
1375
+ /** Parse a `task-notify` SSE payload; rejects anything malformed or empty. */
1376
+ function parseTaskNotifyPayload(value) {
1377
+ let record;
1378
+ if (typeof value === "string") try {
1379
+ const parsed = JSON.parse(value);
1380
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return void 0;
1381
+ record = parsed;
1382
+ } catch {
1383
+ return;
1384
+ }
1385
+ else if (value !== null && typeof value === "object" && !Array.isArray(value)) record = value;
1386
+ else return void 0;
1387
+ if (typeof record.sessionId !== "string" || record.sessionId.trim() === "") return void 0;
1388
+ const turn = typeof record.turn === "number" && Number.isSafeInteger(record.turn) && record.turn >= 0 ? record.turn : 0;
1389
+ return Object.freeze({
1390
+ sessionId: record.sessionId.slice(0, 128),
1391
+ turn
1392
+ });
1393
+ }
1394
+ /** Parse the small authenticated event used when the Host revokes this device. */
1395
+ function isDeviceRevokedPayload(value) {
1396
+ let record;
1397
+ if (typeof value === "string") try {
1398
+ const parsed = JSON.parse(value);
1399
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return false;
1400
+ record = parsed;
1401
+ } catch {
1402
+ return false;
1403
+ }
1404
+ else if (value !== null && typeof value === "object" && !Array.isArray(value)) record = value;
1405
+ else return false;
1406
+ return record.reason === "device_revoked";
1407
+ }
1408
+ /** Tell the Android shell to transition the active record to the revoked state. */
1409
+ function fireDeviceRevoked() {
1410
+ const bridge = readNativeBridge();
1411
+ if (bridge === void 0) return false;
1412
+ bridge.invoke("device.revoked", {}).catch(() => void 0);
1413
+ return true;
1414
+ }
1415
+ /**
1416
+ * Deliver one notification through the native bridge when available.
1417
+ * Returns false when there is no bridge (desktop browsers keep their own
1418
+ * UI) so callers can fall back; a denial flips the module sticky flag via
1419
+ * onDenied for the session-lifetime nag guard.
1420
+ */
1421
+ function fireTaskNotifyEvent(event, onDenied) {
1422
+ const bridge = readNativeBridge();
1423
+ if (bridge === void 0) return false;
1424
+ Promise.resolve().then(() => bridge.capabilities()).then((capabilities) => {
1425
+ if (!capabilities.includes("notification.notify")) return;
1426
+ return bridge.invoke("notification.notify", {
1427
+ title: event.title,
1428
+ body: event.body,
1429
+ tag: event.tag
1430
+ });
1431
+ }).then(() => void 0, (error) => {
1432
+ const code = typeof error === "object" && error !== null && "code" in error ? String(error.code) : "";
1433
+ if ((code === "permission_denied" || code === "denied") && onDenied !== void 0) onDenied();
1434
+ });
1435
+ return true;
1436
+ }
1437
+ /**
1438
+ * Watch the rendered conversation for finished runs and waiting questions,
1439
+ * notifying through the native bridge while the page is hidden. Returns a
1440
+ * disposer. Silent when no native bridge exists (desktop browsers keep their
1441
+ * own UI) or after the user denies the permission.
1442
+ */
1443
+ function installTaskCompletionWatcher(options) {
1444
+ if (typeof document === "undefined" || typeof window === "undefined") return () => void 0;
1445
+ const tracker = new TaskNotifyTracker({ format: options.label });
1446
+ const completionFallback = options.completionFallback ?? true;
1447
+ let notifyBlocked = false;
1448
+ let timer = 0;
1449
+ let anchorTimer = 0;
1450
+ const fire = (event) => {
1451
+ if (notifyBlocked) return;
1452
+ if (!fireTaskNotifyEvent(event, () => {
1453
+ notifyBlocked = true;
1454
+ })) return;
1455
+ };
1456
+ const evaluateNow = () => {
1457
+ const busy = completionFallback && readComposerBusyState(document);
1458
+ const event = tracker.evaluate({
1459
+ pageHidden: document.hidden,
1460
+ pendingQuestion: hasPendingInputQuestion(document),
1461
+ questionKey: pendingInputQuestionKey(document),
1462
+ sessionLabel: readSessionLabel(document.title),
1463
+ composerBusy: busy
1464
+ });
1465
+ if (event !== void 0) fire(event);
1466
+ scheduleAnchorTimer();
1467
+ };
1468
+ const scheduleAnchorTimer = () => {
1469
+ const delay = tracker.pendingAnchorDelayMs();
1470
+ if (delay === void 0) {
1471
+ if (anchorTimer !== 0) {
1472
+ window.clearTimeout(anchorTimer);
1473
+ anchorTimer = 0;
1474
+ }
1475
+ return;
1476
+ }
1477
+ if (anchorTimer !== 0) return;
1478
+ anchorTimer = window.setTimeout(() => {
1479
+ anchorTimer = 0;
1480
+ evaluateNow();
1481
+ }, delay);
1482
+ };
1483
+ const disposeObservation = observeTaskActivity(document.documentElement, () => {
1484
+ if (completionFallback) tracker.markActivity();
1485
+ evaluateNow();
1486
+ });
1487
+ const onVisibility = () => {
1488
+ evaluateNow();
1489
+ };
1490
+ document.addEventListener("visibilitychange", onVisibility);
1491
+ if (typeof window.setInterval === "function") timer = window.setInterval(evaluateNow, 15e3);
1492
+ evaluateNow();
1493
+ return () => {
1494
+ disposeObservation();
1495
+ document.removeEventListener("visibilitychange", onVisibility);
1496
+ if (timer !== 0) window.clearInterval(timer);
1497
+ if (anchorTimer !== 0) window.clearTimeout(anchorTimer);
1498
+ };
1499
+ }
1132
1500
  //#endregion
1133
1501
  //#region src/native-mobile.ts
1134
1502
  /**
@@ -1148,7 +1516,14 @@ window.__ModuleLoader__.load({
1148
1516
  still collapsed the sidebar. Keep the neutral state explicitly invisible —
1149
1517
  the query restores the fixed scrim, and its more specific [hidden] rule
1150
1518
  keeps winning there. */
1151
- .dsh-native-mobile-backdrop,.dsh-mobile-branch-toast,.dsh-mobile-media-toast { display:none; }
1519
+ .dsh-native-mobile-backdrop,.dsh-mobile-branch-toast,.dsh-mobile-media-toast { display:none; }
1520
+ .dsh-mobile-settings_row { display:flex; align-items:center; gap:8px; padding:16px 0; border-bottom:0.5px solid var(--dsw-alias-border-l2); }
1521
+ .dsh-mobile-settings_rowText { flex:1; min-width:0; display:flex; flex-direction:column; gap:4px; padding-right:48px; }
1522
+ .dsh-mobile-settings_title { color:var(--dsw-alias-label-primary); font-size:14px; font-weight:400; line-height:22px; }
1523
+ .dsh-mobile-settings_desc { color:var(--dsw-alias-label-tertiary); font-size:12px; font-weight:400; line-height:18px; }
1524
+ .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; }
1525
+ .dsh-mobile-settings_selector:hover:not(:disabled) { background:var(--dsw-alias-interactive-bg-hover); }
1526
+ .dsh-mobile-settings_selector:disabled { cursor:wait; opacity:.55; }
1152
1527
  @media ${NATIVE_MOBILE_OVERLAY_QUERY} {
1153
1528
  html.dsh-native-mobile-active,html.dsh-native-mobile-active body { width:100%; height:100%; overflow:hidden; }
1154
1529
  html.dsh-native-mobile-active { --dsh-mobile-motion-duration:200ms; --dsh-mobile-motion-ease:cubic-bezier(.22,1,.36,1); }
@@ -1191,8 +1566,12 @@ window.__ModuleLoader__.load({
1191
1566
  [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
1567
  [data-dsh-mobile-sidebar][data-open="false"] [data-dsh-mobile-sidebar-root] > :not(:has([data-dsh-mobile-toggle])) { display:none !important; }
1193
1568
  [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; }
1569
+ [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; }
1570
+ /* The stock settings shell is mounted under the collapsed sidebar. Let
1571
+ its fixed overlay escape the zero-width drawer while it is open. */
1572
+ [data-dsh-mobile-sidebar]:has([data-dsh-mobile-settings]) { width:100vw !important; inset:0 !important; }
1573
+ [data-dsh-mobile-sidebar]:has([data-dsh-mobile-settings]) > * { width:100vw !important; overflow:visible !important; }
1574
+ [data-dsh-mobile-toggle] { width:44px !important; height:44px !important; min-width:44px !important; min-height:44px !important; }
1196
1575
  [data-dsh-mobile-sidebar][data-open="false"] [data-dsh-mobile-toggle] > svg[class*="_railFish"] { transform:translateY(-4px) !important; }
1197
1576
  .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
1577
  .dsh-native-mobile-backdrop:not([hidden]) { animation:dsh-mobile-fade-in var(--dsh-mobile-motion-duration) ease-out; }
@@ -1208,11 +1587,11 @@ window.__ModuleLoader__.load({
1208
1587
  [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
1588
  [data-dsh-mobile-settings-list] [aria-current="true"] { border-color:transparent !important; outline:0 !important; box-shadow:none !important; }
1210
1589
  [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; }
1590
+ [data-dsh-mobile-settings-header] { height:48px !important; min-height:48px !important; padding:10px 12px 6px !important; }
1591
+ [data-dsh-mobile-settings-header] [class*="_close"] { width:36px !important; height:36px !important; }
1592
+ [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; }
1593
+ [data-dsh-mobile-settings-options] > * { width:100% !important; min-width:0 !important; }
1594
+ [data-dsh-mobile-settings-options] [data-slot="settings.general.item"] > [class*="_row"] { flex-direction:column !important; align-items:stretch !important; gap:12px !important; }
1216
1595
  [data-dsh-mobile-settings-options] [data-slot="settings.general.item"] [class*="_rowText"] { width:100% !important; padding-right:0 !important; }
1217
1596
  [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
1597
  [data-dsh-mobile-settings-options] :is(input,select,textarea,button) { max-width:100%; }
@@ -1281,8 +1660,9 @@ window.__ModuleLoader__.load({
1281
1660
  [data-dsh-mobile-center] [role="dialog"][aria-label*="Context"] :is(dt,dd) { white-space:nowrap !important; word-break:keep-all !important; }
1282
1661
  .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
1662
  .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; }
1663
+ /* Fallback only: when the native Add group has no item to clone, keep the
1664
+ camera action on the same row metrics and tokens as the native menu. */
1665
+ .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
1666
  .dsh-mobile-media-action:active { opacity:.72; }
1287
1667
  .dsh-mobile-media-action:focus-visible { outline:2px solid var(--dsw-alias-interactive-border-focus,#4c82f7); outline-offset:1px; }
1288
1668
  .dsh-mobile-media-action:disabled { cursor:default; opacity:.38; }
@@ -1416,19 +1796,12 @@ window.__ModuleLoader__.load({
1416
1796
  const language = resolveNativeMobileLanguage(document.documentElement.lang, browserLanguages);
1417
1797
  const restoreLanguageMarker = applyNativeMobileLanguageMarker(document.documentElement, language);
1418
1798
  const label = (italian, english, chinese) => language === "it" ? italian : language === "zh" ? chinese : english;
1419
- const mediaIcon = (kind) => {
1799
+ const mediaIcon = () => {
1420
1800
  const namespace = "http://www.w3.org/2000/svg";
1421
1801
  const icon = document.createElementNS(namespace, "svg");
1422
1802
  icon.setAttribute("viewBox", "0 0 16 16");
1423
1803
  icon.setAttribute("fill", "none");
1424
1804
  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
1805
  const body = document.createElementNS(namespace, "path");
1433
1806
  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
1807
  body.setAttribute("fill", "currentColor");
@@ -1441,13 +1814,15 @@ window.__ModuleLoader__.load({
1441
1814
  icon.append(body, lens);
1442
1815
  return icon;
1443
1816
  };
1444
- const createMediaAction = (kind, text) => {
1817
+ const createMediaAction = (text) => {
1445
1818
  const button = document.createElement("button");
1446
1819
  button.type = "button";
1447
1820
  button.className = "dsh-mobile-media-action";
1448
1821
  button.lang = language;
1449
- button.dataset.dshMobileMediaAction = kind;
1450
- button.append(mediaIcon(kind));
1822
+ button.dataset.dshMobileMediaAction = "camera";
1823
+ const icon = document.createElement("span");
1824
+ icon.append(mediaIcon());
1825
+ button.append(icon);
1451
1826
  const caption = document.createElement("span");
1452
1827
  caption.textContent = text;
1453
1828
  button.append(caption);
@@ -1484,17 +1859,8 @@ window.__ModuleLoader__.load({
1484
1859
  mediaToast.setAttribute("role", "status");
1485
1860
  mediaToast.setAttribute("aria-live", "polite");
1486
1861
  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;
1862
+ const cameraButton = createMediaAction(label("Scatta foto", "Take photo", "拍照"));
1496
1863
  cameraButton.disabled = true;
1497
- mediaActions.append(fileButton, cameraButton);
1498
1864
  let branchToastTimer = 0;
1499
1865
  let mediaToastTimer = 0;
1500
1866
  const showMediaToast = (message) => {
@@ -1566,11 +1932,11 @@ window.__ModuleLoader__.load({
1566
1932
  if (files.length === 0 || !mediaRequestIsCurrent(context)) return;
1567
1933
  dispatchComposerImageDrop(document, files);
1568
1934
  };
1569
- const launchBrowserPicker = (camera, context) => {
1935
+ const launchBrowserCamera = (context) => {
1570
1936
  const input = document.createElement("input");
1571
1937
  input.type = "file";
1572
- input.accept = "image/png,image/jpeg,image/webp,image/gif";
1573
- if (camera) input.capture = "environment";
1938
+ input.accept = "image/*";
1939
+ input.capture = "environment";
1574
1940
  input.hidden = true;
1575
1941
  const signal = mediaPickerAbortController.signal;
1576
1942
  let cleanupTimer = 0;
@@ -1619,17 +1985,51 @@ window.__ModuleLoader__.load({
1619
1985
  input.click();
1620
1986
  } catch {
1621
1987
  cleanup();
1622
- if (mediaRequestIsCurrent(context)) showMediaToast(label("Impossibile aprire il selettore immagini", "Could not open the image picker", "无法打开图片选择器"));
1988
+ if (mediaRequestIsCurrent(context)) showMediaToast(label("Impossibile aprire la fotocamera", "Could not open the camera", "无法打开相机"));
1623
1989
  }
1624
1990
  };
1625
1991
  const dismissCommandMenu = () => {
1626
1992
  (boundComposer?.querySelector("button[aria-haspopup=\"listbox\"][aria-expanded=\"true\"]"))?.click();
1627
1993
  };
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;
1994
+ const addGroupLabel = label("Aggiungi", "Add", "添加");
1995
+ const placeCameraAction = (commandMenu) => {
1996
+ const sectionTitle = Array.from(commandMenu.querySelectorAll("[role=\"listbox\"] [role=\"presentation\"]")).find((candidate) => !candidate.hasAttribute("data-source") && candidate.textContent?.trim() === addGroupLabel);
1997
+ if (sectionTitle === void 0) {
1998
+ cameraButton.remove();
1999
+ return false;
2000
+ }
2001
+ cameraButton.className = "dsh-mobile-media-action";
2002
+ const nativeItem = (() => {
2003
+ let sibling = sectionTitle.nextElementSibling;
2004
+ while (sibling instanceof HTMLElement && sibling.getAttribute("role") !== "presentation") {
2005
+ if (sibling.matches("button[role=\"option\"]:not([aria-selected=\"true\"])")) return sibling;
2006
+ sibling = sibling.nextElementSibling;
2007
+ }
2008
+ })();
2009
+ if (nativeItem !== void 0) {
2010
+ cameraButton.className = nativeItem.className;
2011
+ const cameraIcon = cameraButton.firstElementChild;
2012
+ const cameraLabel = cameraButton.lastElementChild;
2013
+ const nativeIcon = firstByClassSuffix(nativeItem, "_itemIcon");
2014
+ const nativeLabel = firstByClassSuffix(nativeItem, "_itemName");
2015
+ if (cameraIcon instanceof HTMLElement && nativeIcon !== void 0) cameraIcon.className = nativeIcon.className;
2016
+ if (cameraLabel instanceof HTMLElement && nativeLabel !== void 0) cameraLabel.className = nativeLabel.className;
2017
+ }
2018
+ cameraButton.setAttribute("role", "option");
2019
+ cameraButton.setAttribute("aria-selected", "false");
2020
+ cameraButton.setAttribute("aria-label", label("Scatta foto", "Take photo", "拍照"));
2021
+ let lastItem = sectionTitle;
2022
+ let sibling = sectionTitle.nextElementSibling;
2023
+ while (sibling instanceof HTMLElement && sibling.getAttribute("role") !== "presentation") {
2024
+ if (sibling !== cameraButton) lastItem = sibling;
2025
+ sibling = sibling.nextElementSibling;
1632
2026
  }
2027
+ if (cameraButton.parentElement !== sectionTitle.parentElement || cameraButton.previousElementSibling !== lastItem) lastItem.after(cameraButton);
2028
+ return true;
2029
+ };
2030
+ const setCameraActionDisabled = (disabled, title) => {
2031
+ if (cameraButton.disabled !== disabled) cameraButton.disabled = disabled;
2032
+ if (cameraButton.title !== title) cameraButton.title = title;
1633
2033
  };
1634
2034
  const quietMediaPointer = (event) => {
1635
2035
  event.preventDefault();
@@ -1637,9 +2037,9 @@ window.__ModuleLoader__.load({
1637
2037
  const active = document.activeElement;
1638
2038
  if (active instanceof HTMLElement && (active.matches("input,textarea") || active.isContentEditable)) active.blur();
1639
2039
  };
1640
- const pickImage = (camera) => {
2040
+ const takePhoto = () => {
1641
2041
  if (!canAcceptComposerDrop()) {
1642
- setMediaActionsDisabled(true, label("Allegati immagine non disponibili", "Image attachments are unavailable", "图片附件不可用"));
2042
+ setCameraActionDisabled(true, label("Allegati immagine non disponibili", "Image attachments are unavailable", "图片附件不可用"));
1643
2043
  dismissCommandMenu();
1644
2044
  return;
1645
2045
  }
@@ -1647,36 +2047,21 @@ window.__ModuleLoader__.load({
1647
2047
  dismissCommandMenu();
1648
2048
  const bridge = window.__DSH_MOBILE_NATIVE__;
1649
2049
  if (bridge === void 0) {
1650
- launchBrowserPicker(camera, context);
2050
+ launchBrowserCamera(context);
1651
2051
  return;
1652
2052
  }
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) => {
2053
+ Promise.resolve().then(() => bridge.invoke("camera.capture", {})).then((value) => {
1661
2054
  if (!mediaRequestIsCurrent(context)) return;
1662
2055
  if (value instanceof File) deliverImages([value], context);
1663
- else showMediaToast(label("Il file selezionato non è utilizzabile", "The selected file is unavailable", "所选文件不可用"));
2056
+ else showMediaToast(label("La foto scattata non è utilizzabile", "The captured photo is unavailable", "所拍照片不可用"));
1664
2057
  }).catch((error) => {
1665
2058
  if (!mediaRequestIsCurrent(context)) return;
1666
2059
  const code = typeof error === "object" && error !== null && "code" in error ? String(error.code) : "";
1667
2060
  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", "无法附加图片"));
2061
+ 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
2062
  });
1670
2063
  };
1671
- const chooseImage = () => {
1672
- pickImage(false);
1673
- };
1674
- const takePhoto = () => {
1675
- pickImage(true);
1676
- };
1677
- fileButton.addEventListener("pointerdown", quietMediaPointer);
1678
2064
  cameraButton.addEventListener("pointerdown", quietMediaPointer);
1679
- fileButton.addEventListener("click", chooseImage);
1680
2065
  cameraButton.addEventListener("click", takePhoto);
1681
2066
  const showBranchToast = () => {
1682
2067
  const header = document.querySelector("[data-dsh-mobile-header]");
@@ -1785,8 +2170,8 @@ window.__ModuleLoader__.load({
1785
2170
  if (center === void 0) {
1786
2171
  bindHistoryScroller(void 0);
1787
2172
  syncMediaBinding(null);
1788
- setMediaActionsDisabled(true, label("Apri prima una sessione", "Open a session first", "请先打开会话"));
1789
- mediaActions.remove();
2173
+ setCameraActionDisabled(true, label("Apri prima una sessione", "Open a session first", "请先打开会话"));
2174
+ cameraButton.remove();
1790
2175
  return;
1791
2176
  }
1792
2177
  if (center !== void 0) {
@@ -1824,8 +2209,8 @@ window.__ModuleLoader__.load({
1824
2209
  const composerRow = composerCard?.querySelector(":scope > [data-input-scroll]")?.nextElementSibling;
1825
2210
  if (!(composerRow instanceof HTMLElement)) {
1826
2211
  syncMediaBinding(null);
1827
- setMediaActionsDisabled(true, label("Apri prima una sessione", "Open a session first", "请先打开会话"));
1828
- mediaActions.remove();
2212
+ setCameraActionDisabled(true, label("Apri prima una sessione", "Open a session first", "请先打开会话"));
2213
+ cameraButton.remove();
1829
2214
  }
1830
2215
  if (composerRow instanceof HTMLElement) {
1831
2216
  composerRow.dataset.dshMobileComposerRow = "true";
@@ -1840,10 +2225,11 @@ window.__ModuleLoader__.load({
1840
2225
  const composerBusy = composerCard?.getAttribute("aria-busy") === "true" || composerInput?.disabled === true || composerInput?.readOnly === true || composerEditor?.getAttribute("aria-disabled") === "true";
1841
2226
  const attachmentBlocked = !canAcceptComposerDrop();
1842
2227
  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);
2228
+ 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", "拍照并附加到当前消息");
2229
+ setCameraActionDisabled(mediaDisabled, mediaTitle);
1845
2230
  const commandMenu = composerCard?.querySelector("[data-trigger-menu]") ?? null;
1846
- if (commandMenu !== null && (mediaActions.parentElement !== commandMenu || commandMenu.firstElementChild !== mediaActions)) commandMenu.prepend(mediaActions);
2231
+ if (commandMenu !== null) placeCameraAction(commandMenu);
2232
+ else cameraButton.remove();
1847
2233
  }
1848
2234
  if (composerTrailing !== void 0 && composerTrailing !== composerTools) {
1849
2235
  composerTrailing.dataset.dshMobileComposerTrailing = "true";
@@ -1899,8 +2285,19 @@ window.__ModuleLoader__.load({
1899
2285
  if (sidebar?.dataset.open === "true") toggle?.click();
1900
2286
  });
1901
2287
  sync();
2288
+ const disposeTaskWatcher = installTaskCompletionWatcher({
2289
+ completionFallback: false,
2290
+ label: (kind, sessionLabel) => kind === "done" ? {
2291
+ title: label("Attività completata", "Task finished", "任务已完成"),
2292
+ 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}`)
2293
+ } : {
2294
+ title: label("È richiesto un input", "Input needed", "需要你确认"),
2295
+ body: label("DSH attende una tua scelta", "DSH waits for your choice", "DSH 等待你的选择")
2296
+ }
2297
+ });
1902
2298
  return () => {
1903
2299
  disposed = true;
2300
+ disposeTaskWatcher();
1904
2301
  mediaRequestGeneration += 1;
1905
2302
  mediaPickerAbortController.abort();
1906
2303
  restoreLanguageMarker();
@@ -1908,15 +2305,13 @@ window.__ModuleLoader__.load({
1908
2305
  observer.disconnect();
1909
2306
  overlayQuery.removeEventListener("change", schedule);
1910
2307
  document.removeEventListener("click", onBranchClick, true);
1911
- fileButton.removeEventListener("pointerdown", quietMediaPointer);
1912
2308
  cameraButton.removeEventListener("pointerdown", quietMediaPointer);
1913
- fileButton.removeEventListener("click", chooseImage);
1914
2309
  cameraButton.removeEventListener("click", takePhoto);
1915
2310
  if (branchToastTimer !== 0) window.clearTimeout(branchToastTimer);
1916
2311
  if (mediaToastTimer !== 0) window.clearTimeout(mediaToastTimer);
1917
2312
  branchToast.remove();
1918
2313
  mediaToast.remove();
1919
- mediaActions.remove();
2314
+ cameraButton.remove();
1920
2315
  if (scheduled !== 0) cancelAnimationFrame(scheduled);
1921
2316
  if (transitionFrame !== 0) cancelAnimationFrame(transitionFrame);
1922
2317
  if (transitionRestartFrame !== 0) cancelAnimationFrame(transitionRestartFrame);
@@ -2053,6 +2448,82 @@ window.__ModuleLoader__.load({
2053
2448
  if (className !== void 0) node.className = className;
2054
2449
  return node;
2055
2450
  }
2451
+ function mobileSwitchComputerCopy(locale) {
2452
+ if (locale === "zh") return {
2453
+ title: "切换电脑",
2454
+ description: "返回已配对设备列表,选择要打开的电脑。",
2455
+ action: "切换电脑",
2456
+ busy: "正在切换…",
2457
+ retry: "重试",
2458
+ error: "切换失败,请重试。"
2459
+ };
2460
+ if (locale === "it") return {
2461
+ title: "Cambia computer",
2462
+ description: "Torna all’elenco dei computer associati e scegli quale aprire.",
2463
+ action: "Cambia computer",
2464
+ busy: "Cambio in corso…",
2465
+ retry: "Riprova",
2466
+ error: "Cambio non riuscito. Riprova."
2467
+ };
2468
+ return {
2469
+ title: "Switch computer",
2470
+ description: "Return to the paired-device list and choose a computer to open.",
2471
+ action: "Switch computer",
2472
+ busy: "Switching…",
2473
+ retry: "Retry",
2474
+ error: "Could not switch computers. Try again."
2475
+ };
2476
+ }
2477
+ /** Render the native-app-only action in DSH's General settings row style. */
2478
+ function MobileSwitchComputerRow() {
2479
+ const [nativeReady, setNativeReady] = (0, react.useState)(false);
2480
+ const [state, setState] = (0, react.useState)("idle");
2481
+ (0, react.useEffect)(() => {
2482
+ let disposed = false;
2483
+ const checkNativeAction = () => {
2484
+ const bridge = window.__DSH_MOBILE_NATIVE__;
2485
+ if (bridge === void 0) {
2486
+ setNativeReady(false);
2487
+ return;
2488
+ }
2489
+ Promise.resolve().then(() => bridge.capabilities()).then((capabilities) => {
2490
+ if (!disposed) setNativeReady(capabilities.includes("mobile.switch-computer"));
2491
+ }, () => {
2492
+ if (!disposed) setNativeReady(false);
2493
+ });
2494
+ };
2495
+ checkNativeAction();
2496
+ window.addEventListener("dsh-mobile-native-ready", checkNativeAction);
2497
+ return () => {
2498
+ disposed = true;
2499
+ window.removeEventListener("dsh-mobile-native-ready", checkNativeAction);
2500
+ };
2501
+ }, []);
2502
+ if (!nativeReady) return null;
2503
+ const locale = selectedMobileControlLocale();
2504
+ const copy = mobileSwitchComputerCopy(locale);
2505
+ const invoke = () => {
2506
+ const bridge = window.__DSH_MOBILE_NATIVE__;
2507
+ if (bridge === void 0 || state === "busy") return;
2508
+ setState("busy");
2509
+ bridge.invoke("mobile.switch-computer", {}).catch(() => {
2510
+ setState("error");
2511
+ });
2512
+ };
2513
+ return (0, react.createElement)("div", {
2514
+ className: "dsh-mobile-settings_row",
2515
+ lang: locale
2516
+ }, (0, react.createElement)("div", { className: "dsh-mobile-settings_rowText" }, (0, react.createElement)("div", { className: "dsh-mobile-settings_title" }, copy.title), (0, react.createElement)("div", {
2517
+ className: "dsh-mobile-settings_desc",
2518
+ "aria-live": "polite"
2519
+ }, state === "error" ? copy.error : copy.description)), (0, react.createElement)("button", {
2520
+ type: "button",
2521
+ className: "dsh-mobile-settings_selector",
2522
+ disabled: state === "busy",
2523
+ "aria-busy": state === "busy" ? "true" : void 0,
2524
+ onClick: invoke
2525
+ }, state === "busy" ? copy.busy : state === "error" ? copy.retry : copy.action));
2526
+ }
2056
2527
  const CONTROL_REQUEST_TIMEOUT_MS = 15e3;
2057
2528
  const LONG_CONTROL_REQUEST_TIMEOUT_MS = 21e4;
2058
2529
  const GITHUB_RELEASES_URL = "https://github.com/saya-ch/dsh-mobile/releases";
@@ -2193,6 +2664,33 @@ window.__ModuleLoader__.load({
2193
2664
  remoteTab.setAttribute("aria-pressed", "false");
2194
2665
  switcher.append(lanTab, remoteTab);
2195
2666
  const lanView = element("div", "dsh-mobile-control__view");
2667
+ const lanSetup = element("section", "dsh-mobile-control__lan-setup");
2668
+ lanSetup.hidden = true;
2669
+ const lanSetupTitle = element("h3", "dsh-mobile-control__section-title");
2670
+ lanSetupTitle.textContent = t("lanSetupTitle");
2671
+ const lanSetupIntro = element("p", "dsh-mobile-control__intro");
2672
+ lanSetupIntro.textContent = t("lanSetupIntro");
2673
+ const lanSetupForm = element("div", "dsh-mobile-control__lan-setup-form");
2674
+ const lanSetupLabel = element("label", "dsh-mobile-control__field");
2675
+ lanSetupLabel.textContent = t("lanSetupNetwork");
2676
+ const lanSetupSelect = element("select");
2677
+ lanSetupSelect.setAttribute("aria-label", t("lanSetupNetwork"));
2678
+ const lanSetupNote = element("p", "dsh-mobile-control__component-note");
2679
+ lanSetupNote.textContent = t("lanSetupFirewallNote");
2680
+ const lanSetupActions = element("div", "dsh-mobile-control__lan-setup-actions");
2681
+ const lanSetupRefresh = element("button", "dsh-mobile-control__secondary");
2682
+ lanSetupRefresh.type = "button";
2683
+ lanSetupRefresh.textContent = t("lanSetupRefresh");
2684
+ const lanSetupConfigure = element("button", "dsh-mobile-control__primary");
2685
+ lanSetupConfigure.type = "button";
2686
+ lanSetupConfigure.textContent = t("lanSetupConfigure");
2687
+ const lanSetupStatus = element("p", "dsh-mobile-control__status");
2688
+ lanSetupStatus.setAttribute("role", "status");
2689
+ lanSetupStatus.setAttribute("aria-live", "polite");
2690
+ lanSetupLabel.append(lanSetupSelect);
2691
+ lanSetupActions.append(lanSetupRefresh, lanSetupConfigure);
2692
+ lanSetupForm.append(lanSetupLabel, lanSetupNote, lanSetupActions);
2693
+ lanSetup.append(lanSetupTitle, lanSetupIntro, lanSetupForm, lanSetupStatus);
2196
2694
  const access = element("div", "dsh-mobile-control__access");
2197
2695
  access.hidden = true;
2198
2696
  const accessLabel = element("span", "dsh-mobile-control__access-label");
@@ -2625,7 +3123,15 @@ window.__ModuleLoader__.load({
2625
3123
  remotePair.type = "button";
2626
3124
  remotePair.textContent = t("generateRemoteQr");
2627
3125
  remotePair.disabled = true;
2628
- remoteActions.append(remoteToggle, remoteLogin, remoteReconnect, remotePair);
3126
+ const remoteCopyLink = element("button", "dsh-mobile-control__secondary");
3127
+ remoteCopyLink.type = "button";
3128
+ remoteCopyLink.textContent = t("copyRemoteLink");
3129
+ remoteCopyLink.disabled = true;
3130
+ const remotePairLink = element("p", "dsh-mobile-control__status");
3131
+ remotePairLink.hidden = true;
3132
+ let activeRemotePairUrl = "";
3133
+ let activeRemotePairExpiresAt = 0;
3134
+ remoteActions.append(remoteToggle, remoteLogin, remoteReconnect, remotePair, remoteCopyLink);
2629
3135
  const remoteManageRow = element("div", "dsh-mobile-control__manage-row");
2630
3136
  const remoteDevices = element("button", "dsh-mobile-control__manage");
2631
3137
  remoteDevices.type = "button";
@@ -2821,7 +3327,7 @@ window.__ModuleLoader__.load({
2821
3327
  saveWsPaths([...wsPathsCurrent, value]);
2822
3328
  });
2823
3329
  const remoteWorkspace = element("section", "dsh-mobile-control__remote-workspace");
2824
- remoteWorkspace.append(providerSetupHeader, remoteStatus, remoteAccess, remoteGuide, providerSetupBody, remoteActions, remoteQr, remoteManageRow, remoteDevicePanel);
3330
+ remoteWorkspace.append(providerSetupHeader, remoteStatus, remoteAccess, remoteGuide, providerSetupBody, remoteActions, remoteQr, remotePairLink, remoteManageRow, remoteDevicePanel);
2825
3331
  const diagnosticsView = element("div", "dsh-mobile-control__view is-diagnostics");
2826
3332
  diagnosticsView.hidden = true;
2827
3333
  const diagnosticsIntro = element("p", "dsh-mobile-control__intro");
@@ -2865,7 +3371,7 @@ window.__ModuleLoader__.load({
2865
3371
  diagnosticsDetails.append(diagnosticsDetailsSummary, diagnosticsReport);
2866
3372
  header.append(title, headerActions);
2867
3373
  actions.append(toggle, pair, linkPair);
2868
- lanView.append(access, qrBox, status, extensionStatus, actions, manageRow, devicePanel);
3374
+ lanView.append(lanSetup, access, qrBox, status, extensionStatus, actions, manageRow, devicePanel);
2869
3375
  remoteView.append(remoteIntro, providerSection, remoteWorkspace);
2870
3376
  diagnosticsView.append(diagnosticsIntro, diagnosticsSummary, diagnosticsToolbar, diagnosticsFeedback, diagnosticsChecks, wsPathsSection, diagnosticsDetails);
2871
3377
  panel.append(header, releaseNotice, updateCard, appDownload, switcher, lanView, remoteView, diagnosticsView);
@@ -2873,6 +3379,9 @@ window.__ModuleLoader__.load({
2873
3379
  document.body.append(root);
2874
3380
  let running = false;
2875
3381
  let origin = "";
3382
+ let lanConfigured = true;
3383
+ let lanRestartRequired = false;
3384
+ let lanSetupLoaded = false;
2876
3385
  let remoteRunning = false;
2877
3386
  let remoteReady = false;
2878
3387
  let remoteProvider = "tailscale";
@@ -2976,14 +3485,25 @@ window.__ModuleLoader__.load({
2976
3485
  for (const trigger of document.querySelectorAll(".dsh-mobile-control__trigger")) trigger.setAttribute("aria-expanded", String(open));
2977
3486
  };
2978
3487
  const render = (data) => {
3488
+ lanConfigured = data.configured !== false;
3489
+ lanRestartRequired = data.restartRequired === true;
2979
3490
  running = data.running === true;
2980
3491
  origin = running && typeof data.origin === "string" ? data.origin : "";
3492
+ lanSetup.hidden = lanConfigured && !lanRestartRequired;
3493
+ lanSetupForm.hidden = lanRestartRequired;
3494
+ lanSetupIntro.hidden = lanRestartRequired;
3495
+ status.hidden = !lanConfigured || lanRestartRequired;
3496
+ if (!lanConfigured && !lanSetupLoaded) loadLanSetup();
3497
+ if (lanRestartRequired) {
3498
+ const pendingOrigin = typeof data.pendingOrigin === "string" ? data.pendingOrigin : "";
3499
+ lanSetupStatus.textContent = t("lanSetupRestart", { origin: pendingOrigin });
3500
+ } else if (!lanConfigured && lanSetupStatus.textContent === "") lanSetupStatus.textContent = t("lanSetupRequired");
2981
3501
  access.hidden = origin === "";
2982
3502
  accessLink.href = origin;
2983
3503
  accessLink.textContent = origin;
2984
3504
  accessLink.title = origin;
2985
3505
  status.classList.toggle("is-running", running);
2986
- status.textContent = running ? t("lanOn") : t("lanOff");
3506
+ status.textContent = !lanConfigured ? t("lanSetupRequired") : lanRestartRequired ? t("lanSetupRestart", { origin: typeof data.pendingOrigin === "string" ? data.pendingOrigin : "" }) : running ? t("lanOn") : t("lanOff");
2987
3507
  const extensionData = data.extensions;
2988
3508
  if (extensionData !== null && typeof extensionData === "object") {
2989
3509
  const loaded = typeof extensionData.loaded === "number" ? extensionData.loaded : 0;
@@ -2996,11 +3516,78 @@ window.__ModuleLoader__.load({
2996
3516
  } else extensionStatus.hidden = true;
2997
3517
  if (!running) qrBox.hidden = true;
2998
3518
  toggle.textContent = running ? t("disableLan") : t("enableLan");
3519
+ actions.hidden = !lanConfigured || lanRestartRequired;
3520
+ manageRow.hidden = !lanConfigured || lanRestartRequired;
2999
3521
  pair.disabled = !running;
3000
3522
  linkPair.disabled = !running;
3001
3523
  manageDevices.disabled = !running;
3002
3524
  resetAll.disabled = !running;
3003
3525
  };
3526
+ const renderLanSetup = (data) => {
3527
+ if (data.restartRequired === true || data.configured === true) {
3528
+ render(data);
3529
+ return;
3530
+ }
3531
+ const networks = Array.isArray(data.networks) ? data.networks.filter((entry) => typeof entry === "object" && entry !== null) : [];
3532
+ lanSetupSelect.replaceChildren();
3533
+ const hasRecommended = networks.some((network) => network.recommended === true);
3534
+ if (!hasRecommended && networks.length > 1) {
3535
+ const placeholder = element("option");
3536
+ placeholder.value = "";
3537
+ placeholder.textContent = t("lanSetupChooseNetwork");
3538
+ placeholder.disabled = true;
3539
+ placeholder.selected = true;
3540
+ lanSetupSelect.append(placeholder);
3541
+ }
3542
+ for (const network of networks) {
3543
+ if (typeof network.address !== "string" || typeof network.name !== "string") continue;
3544
+ const option = element("option");
3545
+ option.value = network.address;
3546
+ option.textContent = `${network.name} · ${network.address}${network.recommended === true ? ` · ${t("lanSetupRecommended")}` : ""}`;
3547
+ option.selected = network.recommended === true || !hasRecommended && networks.length === 1;
3548
+ lanSetupSelect.append(option);
3549
+ }
3550
+ const available = networks.some((network) => typeof network.address === "string" && typeof network.name === "string");
3551
+ lanSetupConfigure.disabled = lanSetupSelect.value === "";
3552
+ lanSetupStatus.textContent = available ? t("lanSetupRequired") : t("lanSetupNoNetworks");
3553
+ lanSetupNote.hidden = data.windowsFirewall !== true;
3554
+ };
3555
+ lanSetupSelect.addEventListener("change", () => {
3556
+ lanSetupConfigure.disabled = lanSetupSelect.value === "";
3557
+ });
3558
+ const loadLanSetup = () => {
3559
+ lanSetupLoaded = true;
3560
+ lanSetupRefresh.disabled = true;
3561
+ controlRequestJson("/api/mobile-access/lan/setup").then(renderLanSetup, () => {
3562
+ lanSetupStatus.textContent = t("lanSetupFailed");
3563
+ }).finally(() => {
3564
+ lanSetupRefresh.disabled = false;
3565
+ });
3566
+ };
3567
+ lanSetupRefresh.addEventListener("click", () => {
3568
+ loadLanSetup();
3569
+ });
3570
+ lanSetupConfigure.addEventListener("click", () => {
3571
+ if (lanSetupSelect.value === "") return;
3572
+ lanSetupConfigure.disabled = true;
3573
+ lanSetupRefresh.disabled = true;
3574
+ lanSetupConfigure.textContent = t("lanSetupConfiguring");
3575
+ lanSetupStatus.textContent = t("lanSetupConfiguring");
3576
+ controlRequestJson("/api/mobile-access/lan/setup", {
3577
+ method: "POST",
3578
+ body: JSON.stringify({
3579
+ confirm: true,
3580
+ address: lanSetupSelect.value,
3581
+ configureFirewall: true
3582
+ })
3583
+ }, LONG_CONTROL_REQUEST_TIMEOUT_MS).then(renderLanSetup, () => {
3584
+ lanSetupStatus.textContent = t("lanSetupFailed");
3585
+ }).finally(() => {
3586
+ lanSetupConfigure.textContent = t("lanSetupConfigure");
3587
+ lanSetupRefresh.disabled = false;
3588
+ lanSetupConfigure.disabled = lanRestartRequired || lanSetupSelect.options.length === 0;
3589
+ });
3590
+ });
3004
3591
  const showQr = (svg, target = qrBox) => {
3005
3592
  target.replaceChildren();
3006
3593
  if (svg === "") {
@@ -3263,8 +3850,14 @@ window.__ModuleLoader__.load({
3263
3850
  remoteReconnect.hidden = needsFunnelSetup || state !== "error" && state !== "unavailable" || !providerPrepared;
3264
3851
  remoteActions.hidden = !providerPrepared;
3265
3852
  remotePair.disabled = !remoteReady;
3853
+ remoteCopyLink.disabled = !remoteReady;
3266
3854
  remoteDevices.disabled = !remoteReady;
3267
- if (!remoteReady) remoteQr.hidden = true;
3855
+ if (!remoteReady) {
3856
+ remoteQr.hidden = true;
3857
+ remotePairLink.hidden = true;
3858
+ activeRemotePairUrl = "";
3859
+ activeRemotePairExpiresAt = 0;
3860
+ }
3268
3861
  if (!needsFunnelSetup) remoteSetupPending = false;
3269
3862
  };
3270
3863
  let remoteLoadInFlight = false;
@@ -3765,24 +4358,48 @@ window.__ModuleLoader__.load({
3765
4358
  };
3766
4359
  window.addEventListener("focus", retryAfterSetup);
3767
4360
  document.addEventListener("visibilitychange", retryAfterSetup);
4361
+ const copyRemotePairUrl = async () => {
4362
+ try {
4363
+ await navigator.clipboard.writeText(activeRemotePairUrl);
4364
+ remoteStatus.textContent = t("linkCopied");
4365
+ } catch {
4366
+ remoteStatus.textContent = t("remoteQrReady");
4367
+ }
4368
+ };
4369
+ const openRemotePairing = () => controlRequestJson("/api/mobile-access/remote/pairing/open", {
4370
+ method: "POST",
4371
+ body: "{}"
4372
+ }).then(async (data) => {
4373
+ const pairUrl = typeof data.pairUrl === "string" ? data.pairUrl : "";
4374
+ const expiresAt = typeof data.expiresAt === "number" && Number.isFinite(data.expiresAt) ? data.expiresAt : 0;
4375
+ showQr(typeof data.qrSvg === "string" ? data.qrSvg : "", remoteQr);
4376
+ if (pairUrl === "") {
4377
+ remoteStatus.textContent = t("keyGenerationFailed");
4378
+ return;
4379
+ }
4380
+ activeRemotePairUrl = pairUrl;
4381
+ activeRemotePairExpiresAt = expiresAt;
4382
+ remotePairLink.textContent = pairUrl;
4383
+ remotePairLink.classList.add("is-key");
4384
+ remotePairLink.hidden = false;
4385
+ await copyRemotePairUrl();
4386
+ });
3768
4387
  remotePair.addEventListener("click", () => {
3769
4388
  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) => {
4389
+ openRemotePairing().catch((error) => {
3781
4390
  remoteStatus.textContent = t("requestFailed", { error: String(error) });
3782
4391
  }).finally(() => {
3783
4392
  remotePair.disabled = !remoteReady;
3784
4393
  });
3785
4394
  });
4395
+ remoteCopyLink.addEventListener("click", () => {
4396
+ remoteCopyLink.disabled = true;
4397
+ (activeRemotePairUrl !== "" && activeRemotePairExpiresAt > Date.now() ? copyRemotePairUrl() : openRemotePairing()).catch((error) => {
4398
+ remoteStatus.textContent = t("requestFailed", { error: String(error) });
4399
+ }).finally(() => {
4400
+ remoteCopyLink.disabled = !remoteReady;
4401
+ });
4402
+ });
3786
4403
  const renderRemoteDevices = (data) => {
3787
4404
  const devices = Array.isArray(data.devices) ? data.devices : [];
3788
4405
  remoteDevicePanel.replaceChildren();
@@ -4473,7 +5090,7 @@ window.__ModuleLoader__.load({
4473
5090
  function startExtensionChangeStream(changed, runtime = {
4474
5091
  window,
4475
5092
  create: (url) => new EventSource(url, { withCredentials: true })
4476
- }) {
5093
+ }, onTaskEvent, onDeviceRevoked) {
4477
5094
  let source;
4478
5095
  let timer;
4479
5096
  let disposed = false;
@@ -4492,6 +5109,14 @@ window.__ModuleLoader__.load({
4492
5109
  retryMs = 1e3;
4493
5110
  };
4494
5111
  next.addEventListener("extensions-changed", changed);
5112
+ if (onTaskEvent !== void 0) next.addEventListener("task-notify", (message) => {
5113
+ const data = message.data;
5114
+ onTaskEvent(data);
5115
+ });
5116
+ if (onDeviceRevoked !== void 0) next.addEventListener("device-revoked", (message) => {
5117
+ const data = message.data;
5118
+ if (isDeviceRevokedPayload(data)) onDeviceRevoked();
5119
+ });
4495
5120
  next.onerror = () => {
4496
5121
  if (source !== next) return;
4497
5122
  next.close();
@@ -5252,6 +5877,20 @@ window.__ModuleLoader__.load({
5252
5877
  });
5253
5878
  const stopEvents = startExtensionChangeStream(() => {
5254
5879
  stopRefresh.refresh();
5880
+ }, void 0, (payload) => {
5881
+ if (document.hidden !== true) return;
5882
+ const parsed = parseTaskNotifyPayload(payload);
5883
+ if (parsed === void 0) return;
5884
+ const language = resolveNativeMobileLanguage(document.documentElement.lang, navigator.languages.length > 0 ? [...navigator.languages] : [navigator.language]);
5885
+ const label = (italian, english, chinese) => language === "it" ? italian : language === "zh" ? chinese : english;
5886
+ fireTaskNotifyEvent({
5887
+ kind: "done",
5888
+ title: label("Attività completata", "Task finished", "任务已完成"),
5889
+ body: label("Il tuo task DSH è terminato", "Your DSH task finished", "你的 DSH 任务已完成"),
5890
+ tag: taskCompletionTag(parsed.sessionId, parsed.turn)
5891
+ });
5892
+ }, () => {
5893
+ fireDeviceRevoked();
5255
5894
  });
5256
5895
  return () => {
5257
5896
  disposed = true;
@@ -5320,6 +5959,7 @@ window.__ModuleLoader__.load({
5320
5959
  .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
5960
  .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
5961
  .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)}
5962
+ .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
5963
  .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
5964
  .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
5965
  .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 +5970,8 @@ window.__ModuleLoader__.load({
5330
5970
  .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
5971
  .dsh-mobile-control__diagnostic-checks{transition:opacity 150ms ease}.dsh-mobile-control__diagnostic-checks.is-refreshing{opacity:.52}
5332
5972
  @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}
5973
+ .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}
5974
+ .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
5975
  .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
5976
  .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
5977
  @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}}
@@ -5349,9 +5989,15 @@ window.__ModuleLoader__.load({
5349
5989
  style.textContent = loopback ? CONTROL_STYLES : NATIVE_MOBILE_STYLES;
5350
5990
  document.head.append(style);
5351
5991
  if (!loopback) {
5992
+ const removeSettingsAction = ctx.slots.inject("settings.general.item", () => ctx.slots.register({
5993
+ name: "settings.general.item",
5994
+ id: "dsh-mobile-switch-computer",
5995
+ order: 100
5996
+ }, MobileSwitchComputerRow));
5352
5997
  const removeCustom = installCustomAssets();
5353
5998
  const removeSurface = installDshLanguageBoundSurface(installNativeMobileSurface);
5354
5999
  return () => {
6000
+ removeSettingsAction();
5355
6001
  removeCustom();
5356
6002
  removeSurface();
5357
6003
  style.remove();
@@ -5408,6 +6054,7 @@ window.__ModuleLoader__.load({
5408
6054
  //#endregion
5409
6055
  exports.CONTROL_STYLES = CONTROL_STYLES;
5410
6056
  exports.DIAGNOSTIC_REASON_MESSAGES = DIAGNOSTIC_REASON_MESSAGES;
6057
+ exports.LOCALIZED_DIAGNOSTIC_COPY = LOCALIZED_DIAGNOSTIC_COPY;
5411
6058
  exports.MOBILE_CONTROL_MESSAGES = MOBILE_CONTROL_MESSAGES;
5412
6059
  exports.PerIdActivationLifecycle = PerIdActivationLifecycle;
5413
6060
  exports.apply = apply;