dsh-mobile 0.4.6 → 0.5.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
@@ -513,6 +513,30 @@ window.__ModuleLoader__.load({
513
513
  wsPathsAnnounce: "{n} 条插件连接等待放行",
514
514
  wsPathsAdvancedHint: "通常不需要手动填写:用上方按钮即可。只有当你确切知道所需路径时才填。"
515
515
  });
516
+ Object.assign(MOBILE_CONTROL_MESSAGES.en, {
517
+ cpolarDownloadFailed: "Could not download cpolar from its official site. Check this computer’s network or proxy, then retry.",
518
+ cpolarDownloadTimeout: "The cpolar download timed out. Check this computer’s network or proxy, then retry.",
519
+ cpolarDownloadHttp: "The official cpolar download returned HTTP {status}. Retry later or check the official download page.",
520
+ cpolarIntegrityFailed: "The cpolar download did not match the pinned build, so it was not installed. Retry later or update DSH Mobile; do not bypass verification.",
521
+ cpolarExtractFailed: "The cpolar download completed, but extraction failed. Check system extraction tools and security software, then retry.",
522
+ cpolarStorageFailed: "Could not write the cpolar component in DSH Mobile’s private directory. Check disk space, permissions, and security software, then retry."
523
+ });
524
+ Object.assign(MOBILE_CONTROL_MESSAGES.it, {
525
+ cpolarDownloadFailed: "Impossibile scaricare cpolar dal sito ufficiale. Controlla rete o proxy del computer e riprova.",
526
+ cpolarDownloadTimeout: "Download di cpolar scaduto. Controlla rete o proxy del computer e riprova.",
527
+ cpolarDownloadHttp: "Il download ufficiale di cpolar ha restituito HTTP {status}. Riprova più tardi o controlla la pagina ufficiale.",
528
+ cpolarIntegrityFailed: "Il pacchetto cpolar non corrisponde alla versione verificata e non è stato installato. Riprova più tardi o aggiorna DSH Mobile; non aggirare la verifica.",
529
+ cpolarExtractFailed: "Download di cpolar completato, ma estrazione non riuscita. Controlla gli strumenti di estrazione e il software di sicurezza, poi riprova.",
530
+ cpolarStorageFailed: "Impossibile scrivere cpolar nella directory privata di DSH Mobile. Controlla spazio, permessi e software di sicurezza, poi riprova."
531
+ });
532
+ Object.assign(MOBILE_CONTROL_MESSAGES.zh, {
533
+ cpolarDownloadFailed: "无法从 cpolar 官网下载组件。请检查这台电脑的网络或代理,然后重试。",
534
+ cpolarDownloadTimeout: "下载 cpolar 组件超时。请检查这台电脑的网络或代理,然后重试。",
535
+ cpolarDownloadHttp: "cpolar 官网下载返回 HTTP {status}。请稍后重试,或检查官网下载页。",
536
+ cpolarIntegrityFailed: "下载的 cpolar 组件未通过固定版本校验,因此没有安装。请稍后重试或更新 DSH Mobile,不要跳过校验。",
537
+ cpolarExtractFailed: "cpolar 下载完成,但解包失败。请检查系统解压组件和安全软件,然后重试。",
538
+ cpolarStorageFailed: "无法把 cpolar 写入 DSH Mobile 私有目录。请检查磁盘空间、目录权限和安全软件,然后重试。"
539
+ });
516
540
  Object.assign(MOBILE_CONTROL_MESSAGES.en, {
517
541
  lanSetupTitle: "Finish local network setup",
518
542
  lanSetupIntro: "Choose the network shared by this computer and your phone. DSH Mobile creates private certificates only after you confirm.",
@@ -1402,6 +1426,7 @@ window.__ModuleLoader__.load({
1402
1426
  "remote-needs-login": ["Tailscale is waiting for login to finish.", "Return to Remote access and continue login."],
1403
1427
  "remote-connecting": ["The {provider} remote channel is still connecting.", "Wait briefly, then check again."],
1404
1428
  "remote-controller-error": ["The {provider} controller reported {controllerCode}.", "Return to Remote access and reconnect."],
1429
+ "competing-remote-channel": ["Another remote plugin is injecting its own request channel; the phone may show that plugin’s pairing screen.", "Turn off dsh-remote-web-ui remote access on the computer, refresh, and pair using the current DSH Mobile QR code. Do not mix pairing codes."],
1405
1430
  "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."]
1406
1431
  },
1407
1432
  it: {
@@ -1426,6 +1451,7 @@ window.__ModuleLoader__.load({
1426
1451
  "remote-needs-login": ["Tailscale attende il completamento dell’accesso.", "Torna ad Accesso remoto e continua l’accesso."],
1427
1452
  "remote-connecting": ["Il canale remoto {provider} è ancora in connessione.", "Attendi qualche istante e ripeti il controllo."],
1428
1453
  "remote-controller-error": ["Il controller {provider} ha segnalato {controllerCode}.", "Torna ad Accesso remoto e riconnettiti."],
1454
+ "competing-remote-channel": ["Un altro plugin remoto sta inserendo il proprio canale di richieste; sul telefono potrebbe apparire la sua schermata di abbinamento.", "Disattiva l’accesso remoto di dsh-remote-web-ui sul computer, aggiorna la pagina e usa il codice QR corrente di DSH Mobile. Non mescolare i codici."],
1429
1455
  "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."]
1430
1456
  },
1431
1457
  zh: {
@@ -1450,6 +1476,7 @@ window.__ModuleLoader__.load({
1450
1476
  "remote-needs-login": ["Tailscale 正在等待完成登录。", "返回远程访问并继续登录。"],
1451
1477
  "remote-connecting": ["{provider} 远程通道仍在连接。", "等待片刻后重新检查。"],
1452
1478
  "remote-controller-error": ["{provider} 控制器报告 {controllerCode}。", "返回远程访问并重新连接。"],
1479
+ "competing-remote-channel": ["检测到另一个远程插件注入自己的请求通道;手机可能显示它的配对页面。", "在电脑端关闭 dsh-remote-web-ui 的远程访问并刷新,再用 DSH Mobile 当前二维码配对;不要混用配对码。"],
1453
1480
  "phone-network-unknown": ["电脑无法判断路由器是否隔离了手机。", "确认手机与电脑使用同一网络,并关闭访客网络或 AP 隔离。"]
1454
1481
  }
1455
1482
  };
@@ -2599,12 +2626,13 @@ window.__ModuleLoader__.load({
2599
2626
  [data-dsh-mobile-center] { grid-column:2 !important; width:100vw !important; min-width:0 !important; }
2600
2627
  [data-dsh-mobile-center] > * { min-width:0 !important; }
2601
2628
  [data-dsh-mobile-header] { box-sizing:border-box !important; width:calc(100% - 16px) !important; margin:0 8px !important; min-width:0; padding-top:max(4px,env(safe-area-inset-top)) !important; padding-right:8px !important; padding-left:42px !important; }
2629
+ [data-dsh-mobile-header] :is([class*="_titleRow"],[class*="_headerLeading"]) { touch-action:pan-y; }
2602
2630
  [data-dsh-mobile-header] [class*="_titleRow"] { box-sizing:border-box !important; display:flex !important; align-items:center !important; min-width:0; min-height:32px !important; height:32px !important; gap:6px !important; padding:0 6px !important; }
2603
2631
  [data-dsh-mobile-header] [class*="_titleCluster"] { min-width:0; }
2604
2632
  [data-dsh-mobile-header] [class*="_crumbs"] { min-width:0; overflow:hidden; }
2605
2633
  [data-dsh-mobile-header] [class*="_crumb"] { max-width:46vw; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
2606
- [data-dsh-mobile-header] [class*="_headerActions"] { min-width:0; overflow-x:auto; scrollbar-width:none; }
2607
- [data-dsh-mobile-header] [class*="_headerActions"]::-webkit-scrollbar { display:none; }
2634
+ /* Nested header actions (including the Jobs menu) must paint outside the row. */
2635
+ [data-dsh-mobile-header] [class*="_headerActions"] { flex:none; min-width:max-content; overflow:visible; }
2608
2636
  [data-dsh-mobile-header] [class*="_headerUtilities"] { gap:2px !important; }
2609
2637
  [data-dsh-mobile-header] [class*="_sessionLogButton"] { width:40px; min-width:40px; padding:0 !important; overflow:hidden; color:transparent; font-size:0 !important; }
2610
2638
  [data-dsh-mobile-header] [class*="_sessionLogButton"] > * { display:none !important; }
@@ -2691,6 +2719,15 @@ window.__ModuleLoader__.load({
2691
2719
  [data-dsh-mobile-center] [data-composer-card] ~ * [class*="_root"] [class*="_sep"] { margin:0 6px !important; }
2692
2720
  /* Composer dock stats strip (turns/steps/tokens) reads small on phones. */
2693
2721
  [data-dsh-mobile-center] [data-slot="conversation.composer.dock"] [class*="_root"] { font-size:10px !important; line-height:16px !important; }
2722
+ /* The context ring is a composer-metadata _root too, so the metadata rule
2723
+ above stretched it across the whole dock. It cannot shrink, which left the
2724
+ session statistics beside it a few pixels wide, every number clipped to an
2725
+ ellipsis. Put the numbers in a wrapping left column and the ring in a right
2726
+ column it cannot grow out of: a phone then shows every number in full, and
2727
+ a wide screen keeps the ring beside them on one line. */
2728
+ [data-dsh-mobile-center] [data-composer-card] ~ * [class*="_root"]:has(> [class*="_trigger"][aria-haspopup="dialog"]) { width:auto !important; max-width:none !important; flex:0 0 auto !important; }
2729
+ [data-dsh-mobile-center] [class*="_dock"]:has([data-composer-stats]) { display:grid !important; grid-template-columns:minmax(0,1fr) max-content !important; align-items:center !important; column-gap:12px !important; }
2730
+ [data-dsh-mobile-center] [data-composer-card] ~ * [class*="_root"][data-composer-stats] { box-sizing:border-box !important; width:auto !important; max-width:none !important; flex:0 1 auto !important; flex-wrap:wrap !important; justify-content:flex-start !important; row-gap:2px !important; }
2694
2731
  /* Message runtime details are inline on desktop. Give the clock/runtime
2695
2732
  label its own wrapping row on narrow screens so TTFT and throughput do
2696
2733
  not push the action buttons or clip at the viewport edge. */
@@ -2739,7 +2776,6 @@ window.__ModuleLoader__.load({
2739
2776
  @keyframes dsh-mobile-panel-in { from { opacity:.72; transform:translateY(6px); } }
2740
2777
  @keyframes dsh-mobile-view-in { from { opacity:.58; transform:translateY(5px); } }
2741
2778
  @media (max-width:420px) {
2742
- [data-dsh-mobile-header] [class*="_headerActions"] { max-width:42vw; }
2743
2779
  [data-dsh-mobile-settings-options] [data-slot="settings.general.item"] [class*="_selector"] { align-self:stretch !important; width:100% !important; }
2744
2780
  [data-dsh-mobile-message-column] [data-context-fields] > * { grid-template-columns:1fr !important; }
2745
2781
  }
@@ -2782,6 +2818,93 @@ window.__ModuleLoader__.load({
2782
2818
  return marked;
2783
2819
  }
2784
2820
  const AUTO_HISTORY_THRESHOLD_PX = 64;
2821
+ /** Travel, in CSS pixels, that separates a header strip pan from a tap on a chip. */
2822
+ const STRIP_DRAG_THRESHOLD_PX = 8;
2823
+ /** Measure flowing title controls without counting an open absolute-positioned menu. */
2824
+ function measureHeaderStripOverflow(row) {
2825
+ const left = row.getBoundingClientRect().left;
2826
+ const controls = Array.from(row.children);
2827
+ const actions = row.querySelector(":scope > [class*=\"_titleCluster\"] [class*=\"_headerActions\"]");
2828
+ if (actions !== null) controls.push(actions);
2829
+ const right = Math.max(left, ...controls.map((control) => control.getBoundingClientRect().right));
2830
+ return Math.max(0, right - left - row.clientWidth);
2831
+ }
2832
+ /** Touch-only pan state; browser pointer cancellation does not interrupt the touch stream. */
2833
+ function createHeaderStripPanController(options) {
2834
+ let offset = 0;
2835
+ let drag;
2836
+ let suppressUntil = 0;
2837
+ const clamp = (value) => Math.min(Math.max(0, options.range()), Math.max(0, value));
2838
+ const setOffset = (value) => {
2839
+ const next = clamp(value);
2840
+ if (next === offset) return;
2841
+ offset = next;
2842
+ options.render(offset);
2843
+ };
2844
+ const end = () => {
2845
+ if (drag === void 0) return;
2846
+ if (drag.claimed) suppressUntil = options.now() + 400;
2847
+ drag = void 0;
2848
+ };
2849
+ const cancel = () => {
2850
+ if (drag?.claimed) suppressUntil = options.now() + 400;
2851
+ drag = void 0;
2852
+ };
2853
+ return {
2854
+ offset: () => offset,
2855
+ start: (x, y) => {
2856
+ drag = {
2857
+ x,
2858
+ y,
2859
+ offset,
2860
+ claimed: false
2861
+ };
2862
+ suppressUntil = 0;
2863
+ },
2864
+ move: (x, y) => {
2865
+ if (drag === void 0) return false;
2866
+ const travel = x - drag.x;
2867
+ if (!drag.claimed) {
2868
+ const drift = y - drag.y;
2869
+ if (Math.abs(travel) < STRIP_DRAG_THRESHOLD_PX && Math.abs(drift) < STRIP_DRAG_THRESHOLD_PX) return false;
2870
+ if (Math.abs(drift) > Math.abs(travel)) {
2871
+ drag = void 0;
2872
+ return false;
2873
+ }
2874
+ drag.claimed = true;
2875
+ }
2876
+ setOffset(drag.offset - travel);
2877
+ return true;
2878
+ },
2879
+ end,
2880
+ cancel,
2881
+ sync: () => {
2882
+ if (options.range() === 0) {
2883
+ drag = void 0;
2884
+ suppressUntil = 0;
2885
+ }
2886
+ setOffset(offset);
2887
+ },
2888
+ reveal: (left, right, visibleLeft, visibleRight) => {
2889
+ if (left < visibleLeft) setOffset(offset - (visibleLeft - left));
2890
+ else if (right > visibleRight) setOffset(offset + right - visibleRight);
2891
+ },
2892
+ resetClickSuppression: () => {
2893
+ suppressUntil = 0;
2894
+ },
2895
+ suppressClick: (inHeader, detail) => {
2896
+ if (suppressUntil === 0 || detail === 0) return false;
2897
+ const suppress = inHeader && options.now() <= suppressUntil;
2898
+ suppressUntil = 0;
2899
+ return suppress;
2900
+ },
2901
+ dispose: () => {
2902
+ drag = void 0;
2903
+ suppressUntil = 0;
2904
+ setOffset(0);
2905
+ }
2906
+ };
2907
+ }
2785
2908
  function controlledFileDragEvent(type, files, initialDropEffect) {
2786
2909
  const event = new Event(type, {
2787
2910
  bubbles: true,
@@ -2839,6 +2962,48 @@ window.__ModuleLoader__.load({
2839
2962
  function drawerScrimVisible(sidebarCollapsed, overlayActive) {
2840
2963
  return !sidebarCollapsed && overlayActive;
2841
2964
  }
2965
+ /** Only a known on-screen keyboard in an active App composer may change Enter into a line break. */
2966
+ function isSoftKeyboardEnterLineBreak(event, context) {
2967
+ if (context.nativeState?.imeVisible !== true || context.nativeState.noHardwareKeyboard !== true || !context.editable || !context.activeSession || context.commandMenuOpen || context.recentlyComposing) return false;
2968
+ if (!event.isTrusted || event.key !== "Enter" || event.isComposing || event.keyCode === 229 || event.repeat) return false;
2969
+ return !event.shiftKey && !event.altKey && !event.ctrlKey && !event.metaKey && !event.getModifierState("AltGraph");
2970
+ }
2971
+ /** Attach the App-only Enter translation while retaining DSH's original physical-key semantics. */
2972
+ function bindComposerSoftEnter(editor, context) {
2973
+ let composing = false;
2974
+ let composingUntil = 0;
2975
+ const onCompositionStart = () => {
2976
+ composing = true;
2977
+ };
2978
+ const onCompositionEnd = () => {
2979
+ composing = false;
2980
+ composingUntil = performance.now() + 10;
2981
+ };
2982
+ const onKeyDown = (event) => {
2983
+ if (!isSoftKeyboardEnterLineBreak(event, {
2984
+ ...context(),
2985
+ recentlyComposing: composing || editor.hasAttribute("data-composer-composing") || performance.now() < composingUntil
2986
+ })) return;
2987
+ event.preventDefault();
2988
+ event.stopImmediatePropagation();
2989
+ editor.dispatchEvent(new KeyboardEvent("keydown", {
2990
+ key: "Enter",
2991
+ code: "Enter",
2992
+ shiftKey: true,
2993
+ bubbles: true,
2994
+ cancelable: true,
2995
+ composed: true
2996
+ }));
2997
+ };
2998
+ editor.addEventListener("compositionstart", onCompositionStart);
2999
+ editor.addEventListener("compositionend", onCompositionEnd);
3000
+ editor.addEventListener("keydown", onKeyDown, { capture: true });
3001
+ return () => {
3002
+ editor.removeEventListener("compositionstart", onCompositionStart);
3003
+ editor.removeEventListener("compositionend", onCompositionEnd);
3004
+ editor.removeEventListener("keydown", onKeyDown, { capture: true });
3005
+ };
3006
+ }
2842
3007
  /** Check that an asynchronous picker result still belongs to its originating session and composer. */
2843
3008
  function isComposerMediaOriginCurrent(origin, current) {
2844
3009
  return !current.disposed && origin.generation === current.generation && origin.href === current.href && origin.composer !== null && current.composerConnected && origin.composer === current.composer && origin.sessionRoot !== null && origin.sessionRoot === current.sessionRoot && origin.sessionId !== null && origin.sessionId === current.sessionId;
@@ -2935,6 +3100,9 @@ window.__ModuleLoader__.load({
2935
3100
  const canAcceptComposerDrop = () => preflightComposerImageDrop(document, [preflightFile]);
2936
3101
  const mediaPickerAbortController = new AbortController();
2937
3102
  const browserPickerCleanups = /* @__PURE__ */ new Set();
3103
+ let boundEnterEditor = null;
3104
+ let boundEnterCard = null;
3105
+ let disposeEnterBinding;
2938
3106
  const sessionTokens = /* @__PURE__ */ new WeakMap();
2939
3107
  let nextSessionToken = 0;
2940
3108
  const currentSessionOrigin = () => {
@@ -3135,6 +3303,101 @@ window.__ModuleLoader__.load({
3135
3303
  if (branch === null || branch.hasAttribute("disabled") || branch.getAttribute("aria-disabled") === "true") return;
3136
3304
  window.setTimeout(showBranchToast, 80);
3137
3305
  };
3306
+ let stripHeader;
3307
+ const stripParts = () => {
3308
+ const row = stripHeader?.querySelector("[class*=\"_titleRow\"]");
3309
+ if (row === void 0 || row === null) return void 0;
3310
+ return {
3311
+ row,
3312
+ lead: stripHeader?.querySelector("[class*=\"_headerLeading\"]") ?? void 0
3313
+ };
3314
+ };
3315
+ const stripRange = () => {
3316
+ const parts = stripParts();
3317
+ if (!overlayQuery.matches || parts === void 0) return 0;
3318
+ const overflow = measureHeaderStripOverflow(parts.row);
3319
+ return overflow > 1 ? overflow : 0;
3320
+ };
3321
+ const applyStrip = (offset) => {
3322
+ const parts = stripParts();
3323
+ if (parts === void 0) return;
3324
+ const value = offset === 0 ? "" : `translateX(${-offset}px)`;
3325
+ if (parts.row.style.transform !== value) parts.row.style.transform = value;
3326
+ if (parts.lead !== void 0 && parts.lead.style.transform !== value) parts.lead.style.transform = value;
3327
+ };
3328
+ const pan = createHeaderStripPanController({
3329
+ range: stripRange,
3330
+ render: applyStrip,
3331
+ now: () => performance.now()
3332
+ });
3333
+ const syncPanHeader = (header) => {
3334
+ if (header !== stripHeader) {
3335
+ pan.dispose();
3336
+ stripHeader = header;
3337
+ }
3338
+ pan.sync();
3339
+ applyStrip(pan.offset());
3340
+ };
3341
+ let activeTouchId;
3342
+ const onStripTouchStart = (event) => {
3343
+ if (activeTouchId !== void 0) pan.cancel();
3344
+ activeTouchId = void 0;
3345
+ pan.resetClickSuppression();
3346
+ if (event.touches.length !== 1) return;
3347
+ if (!overlayQuery.matches) return;
3348
+ if (!(event.target instanceof Element)) return;
3349
+ if (event.target.closest("[data-dsh-mobile-header]") !== stripHeader) return;
3350
+ if (event.target.closest("[data-dsh-mobile-header] [class*=\"_titleRow\"],[data-dsh-mobile-header] [class*=\"_headerLeading\"]") === null) return;
3351
+ if (stripRange() === 0) return;
3352
+ const touch = event.touches[0];
3353
+ if (touch === void 0) return;
3354
+ activeTouchId = touch.identifier;
3355
+ pan.start(touch.clientX, touch.clientY);
3356
+ };
3357
+ const onStripTouchMove = (event) => {
3358
+ if (activeTouchId === void 0) return;
3359
+ if (event.touches.length !== 1) {
3360
+ activeTouchId = void 0;
3361
+ pan.cancel();
3362
+ return;
3363
+ }
3364
+ const touch = event.touches[0];
3365
+ if (touch === void 0 || touch.identifier !== activeTouchId) return;
3366
+ if (pan.move(touch.clientX, touch.clientY)) event.preventDefault();
3367
+ };
3368
+ const onStripTouchEnd = (event) => {
3369
+ if (activeTouchId === void 0 || !Array.from(event.changedTouches).some((touch) => touch.identifier === activeTouchId)) return;
3370
+ activeTouchId = void 0;
3371
+ pan.end();
3372
+ };
3373
+ const onStripTouchCancel = (event) => {
3374
+ if (activeTouchId === void 0 || !Array.from(event.changedTouches).some((touch) => touch.identifier === activeTouchId)) return;
3375
+ activeTouchId = void 0;
3376
+ pan.cancel();
3377
+ };
3378
+ const onStripFocus = (event) => {
3379
+ if (!(event.target instanceof HTMLElement)) return;
3380
+ const header = event.target.closest("[data-dsh-mobile-header]");
3381
+ if (header === null || header !== stripHeader || stripRange() === 0 || event.target.closest("[class*=\"_titleRow\"],[class*=\"_headerLeading\"]") === null) return;
3382
+ const focused = event.target.getBoundingClientRect();
3383
+ const visible = header.getBoundingClientRect();
3384
+ pan.reveal(focused.left, focused.right, visible.left + 42, visible.right - 8);
3385
+ };
3386
+ const onStripClickCapture = (event) => {
3387
+ const inHeader = event.target instanceof Element && event.target.closest("[data-dsh-mobile-header]") !== null;
3388
+ if (!pan.suppressClick(inHeader, event.detail)) return;
3389
+ event.preventDefault();
3390
+ event.stopPropagation();
3391
+ };
3392
+ document.addEventListener("touchstart", onStripTouchStart, true);
3393
+ document.addEventListener("touchmove", onStripTouchMove, {
3394
+ capture: true,
3395
+ passive: false
3396
+ });
3397
+ document.addEventListener("touchend", onStripTouchEnd, true);
3398
+ document.addEventListener("touchcancel", onStripTouchCancel, true);
3399
+ document.addEventListener("focusin", onStripFocus, true);
3400
+ document.addEventListener("click", onStripClickCapture, true);
3138
3401
  document.addEventListener("click", onBranchClick, true);
3139
3402
  let frame;
3140
3403
  let sidebar;
@@ -3200,6 +3463,19 @@ window.__ModuleLoader__.load({
3200
3463
  });
3201
3464
  };
3202
3465
  document.addEventListener("click", animateNavigation);
3466
+ const syncComposerEnterNewline = (composerCard) => {
3467
+ const editor = composerCard?.querySelector("[data-composer-input][contenteditable=\"true\"]") ?? null;
3468
+ if (editor === boundEnterEditor && composerCard === boundEnterCard) return;
3469
+ disposeEnterBinding?.();
3470
+ boundEnterEditor = editor;
3471
+ boundEnterCard = composerCard;
3472
+ disposeEnterBinding = editor === null || composerCard === null ? void 0 : bindComposerSoftEnter(editor, () => ({
3473
+ nativeState: window.__DSH_MOBILE_NATIVE__ === void 0 ? null : window.__DSH_MOBILE_KEYBOARD_STATE__,
3474
+ editable: editor.isConnected && editor.getAttribute("contenteditable") === "true" && editor.getAttribute("aria-disabled") !== "true" && editor.getAttribute("aria-haspopup") !== "menu" && composerCard.getAttribute("aria-busy") !== "true",
3475
+ activeSession: currentSessionOrigin().sessionId !== null,
3476
+ commandMenuOpen: composerCard.querySelector("[data-trigger-menu],button[aria-haspopup=\"listbox\"][aria-expanded=\"true\"]") !== null
3477
+ }));
3478
+ };
3203
3479
  const syncMediaBinding = (composer) => {
3204
3480
  const previousComposer = boundComposer;
3205
3481
  boundComposer = composer;
@@ -3222,6 +3498,8 @@ window.__ModuleLoader__.load({
3222
3498
  const handle = frame === void 0 ? void 0 : firstByClassSuffix(frame, "_handle");
3223
3499
  markNativeMobileSettings(document);
3224
3500
  if (center === void 0) {
3501
+ syncPanHeader(void 0);
3502
+ syncComposerEnterNewline(null);
3225
3503
  bindHistoryScroller(void 0);
3226
3504
  syncMediaBinding(null);
3227
3505
  setCameraActionDisabled(true, label("Apri prima una sessione", "Open a session first", "请先打开会话"));
@@ -3230,7 +3508,9 @@ window.__ModuleLoader__.load({
3230
3508
  }
3231
3509
  if (center !== void 0) {
3232
3510
  center.dataset.dshMobileCenter = "true";
3233
- center.querySelector("header")?.setAttribute("data-dsh-mobile-header", "true");
3511
+ const header = center.querySelector("header") ?? void 0;
3512
+ header?.setAttribute("data-dsh-mobile-header", "true");
3513
+ syncPanHeader(header);
3234
3514
  viewArea = firstByClassSuffix(center, "_viewArea");
3235
3515
  if (viewArea !== void 0) viewArea.dataset.dshMobileView = "true";
3236
3516
  const conversation = center.querySelector("[data-conversation-scroll]");
@@ -3262,6 +3542,7 @@ window.__ModuleLoader__.load({
3262
3542
  const composerCard = center.querySelector("[data-composer-card]");
3263
3543
  const composerRow = composerCard?.querySelector(":scope > [data-input-scroll]")?.nextElementSibling;
3264
3544
  if (!(composerRow instanceof HTMLElement)) {
3545
+ syncComposerEnterNewline(null);
3265
3546
  syncMediaBinding(null);
3266
3547
  setCameraActionDisabled(true, label("Apri prima una sessione", "Open a session first", "请先打开会话"));
3267
3548
  cameraButton.remove();
@@ -3274,6 +3555,7 @@ window.__ModuleLoader__.load({
3274
3555
  if (composerTools !== void 0) {
3275
3556
  composerTools.dataset.dshMobileComposerTools = "true";
3276
3557
  syncMediaBinding(composerCard ?? null);
3558
+ syncComposerEnterNewline(composerCard ?? null);
3277
3559
  const composerInput = composerCard?.querySelector("textarea");
3278
3560
  const composerEditor = composerCard?.querySelector("[contenteditable=\"true\"],[contenteditable=\"plaintext-only\"]");
3279
3561
  const composerBusy = composerCard?.getAttribute("aria-busy") === "true" || composerInput?.disabled === true || composerInput?.readOnly === true || composerEditor?.getAttribute("aria-disabled") === "true";
@@ -3284,7 +3566,7 @@ window.__ModuleLoader__.load({
3284
3566
  const commandMenu = composerCard?.querySelector("[data-trigger-menu]") ?? null;
3285
3567
  if (commandMenu !== null) placeCameraAction(commandMenu);
3286
3568
  else cameraButton.remove();
3287
- }
3569
+ } else syncComposerEnterNewline(null);
3288
3570
  if (composerTrailing !== void 0 && composerTrailing !== composerTools) {
3289
3571
  composerTrailing.dataset.dshMobileComposerTrailing = "true";
3290
3572
  const modelTrigger = composerTrailing.querySelector("button[aria-label^=\"选择模型\"],button[aria-label^=\"Select model\"],button[aria-label^=\"Seleziona modello\"]");
@@ -3356,9 +3638,18 @@ window.__ModuleLoader__.load({
3356
3638
  mediaPickerAbortController.abort();
3357
3639
  restoreLanguageMarker();
3358
3640
  for (const cleanup of [...browserPickerCleanups]) cleanup();
3641
+ disposeEnterBinding?.();
3359
3642
  observer.disconnect();
3360
3643
  overlayQuery.removeEventListener("change", schedule);
3361
3644
  document.removeEventListener("click", onBranchClick, true);
3645
+ document.removeEventListener("touchstart", onStripTouchStart, true);
3646
+ document.removeEventListener("touchmove", onStripTouchMove, true);
3647
+ document.removeEventListener("touchend", onStripTouchEnd, true);
3648
+ document.removeEventListener("touchcancel", onStripTouchCancel, true);
3649
+ document.removeEventListener("focusin", onStripFocus, true);
3650
+ document.removeEventListener("click", onStripClickCapture, true);
3651
+ pan.dispose();
3652
+ applyStrip(0);
3362
3653
  cameraButton.removeEventListener("pointerdown", quietMediaPointer);
3363
3654
  cameraButton.removeEventListener("click", takePhoto);
3364
3655
  if (branchToastTimer !== 0) window.clearTimeout(branchToastTimer);
@@ -3383,6 +3674,16 @@ window.__ModuleLoader__.load({
3383
3674
  //#endregion
3384
3675
  //#region src/local-admin-host.ts
3385
3676
  /** Browser-safe desktop-admin Host checks. Do not import Node APIs here. */
3677
+ const DESKTOP_ADMIN_HEADER = "x-dsh-mobile-desktop-admin";
3678
+ const DESKTOP_ADMIN_MARKER = "dsh-app://app";
3679
+ /** Add the forwarded Desktop marker only to this plugin's mutating admin requests. */
3680
+ function localAdminRequestHeaders(init, location) {
3681
+ const headers = new Headers(init?.headers);
3682
+ if (!headers.has("content-type")) headers.set("content-type", "application/json");
3683
+ headers.delete(DESKTOP_ADMIN_HEADER);
3684
+ if (init?.method?.toUpperCase() === "POST" && location.protocol === "dsh-app:" && location.hostname === "app") headers.set(DESKTOP_ADMIN_HEADER, DESKTOP_ADMIN_MARKER);
3685
+ return headers;
3686
+ }
3386
3687
  function unwrapBrackets(hostname) {
3387
3688
  return hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
3388
3689
  }
@@ -3423,11 +3724,12 @@ window.__ModuleLoader__.load({
3423
3724
  /**
3424
3725
  * Whether the current browser document should mount the desktop Mobile access
3425
3726
  * control. Dedicated Mobile HTTPS (the phone surface) stays native even when
3426
- * the Host is a private LAN address.
3727
+ * the Host is a private LAN address. DSH Desktop (dsh-app://app) has neither,
3728
+ * so its protocol gated on the fixed hostname "app" counts too.
3427
3729
  */
3428
- function isDesktopAdminSurface(hostname, search = "", frontend) {
3730
+ function isDesktopAdminSurface(hostname, search = "", frontend, protocol = "") {
3429
3731
  const query = search.startsWith("?") ? search.slice(1) : search;
3430
- return isLocalAdminHostname(hostname) && frontend !== "dedicated" && !new URLSearchParams(query).has("dsh-mobile-preview");
3732
+ return (protocol === "dsh-app:" ? hostname === "app" : (protocol === "" || protocol === "http:" || protocol === "https:") && isLocalAdminHostname(hostname)) && frontend !== "dedicated" && !new URLSearchParams(query).has("dsh-mobile-preview");
3431
3733
  }
3432
3734
  //#endregion
3433
3735
  //#region src/client.ts
@@ -3748,10 +4050,7 @@ window.__ModuleLoader__.load({
3748
4050
  const response = await fetch(path, {
3749
4051
  ...init,
3750
4052
  signal: controller.signal,
3751
- headers: {
3752
- "content-type": "application/json",
3753
- ...init?.headers
3754
- }
4053
+ headers: localAdminRequestHeaders(init, location)
3755
4054
  });
3756
4055
  const body = await response.json();
3757
4056
  if (!response.ok) throw new Error(typeof body.error === "string" ? body.error : `HTTP ${String(response.status)}`);
@@ -3851,6 +4150,18 @@ window.__ModuleLoader__.load({
3851
4150
  control_channel_failed: "controlChannelFailed",
3852
4151
  cpolar_component_missing: "cpolarMissing",
3853
4152
  cpolar_component_invalid: "cpolarInvalid",
4153
+ cpolar_download_failed: "cpolarDownloadFailed",
4154
+ cpolar_download_timeout: "cpolarDownloadTimeout",
4155
+ cpolar_download_size_mismatch: "cpolarIntegrityFailed",
4156
+ cpolar_download_hash_mismatch: "cpolarIntegrityFailed",
4157
+ cpolar_executable_hash_mismatch: "cpolarIntegrityFailed",
4158
+ cpolar_download_redirect_missing: "cpolarDownloadFailed",
4159
+ cpolar_download_redirect_invalid: "cpolarDownloadFailed",
4160
+ cpolar_download_redirect_rejected: "cpolarDownloadFailed",
4161
+ cpolar_installer_missing: "cpolarExtractFailed",
4162
+ cpolar_executable_missing: "cpolarExtractFailed",
4163
+ cpolar_extract_failed: "cpolarExtractFailed",
4164
+ cpolar_storage_failed: "cpolarStorageFailed",
3854
4165
  cpolar_config_missing: "cpolarConfigMissing",
3855
4166
  cpolar_config_invalid: "cpolarConfigInvalid",
3856
4167
  cpolar_port_unavailable: "cpolarPortUnavailable",
@@ -3986,6 +4297,8 @@ window.__ModuleLoader__.load({
3986
4297
  */
3987
4298
  const remoteFailureTextFor = (error, fallbackKey) => {
3988
4299
  const message = error instanceof Error ? error.message : String(error);
4300
+ const cpolarHttpStatus = /^cpolar_download_http_(\d{3})$/u.exec(message)?.[1];
4301
+ if (cpolarHttpStatus !== void 0) return t("cpolarDownloadHttp", { status: cpolarHttpStatus });
3989
4302
  const key = REMOTE_ERROR_MESSAGE_KEYS[message];
3990
4303
  return key === void 0 ? t(fallbackKey, { error: message }) : t(key);
3991
4304
  };
@@ -8224,7 +8537,7 @@ window.__ModuleLoader__.load({
8224
8537
  return trustAuthenticatedGatewayConnection(ctx.get("connection"));
8225
8538
  }, "dsh-mobile: authenticated gateway client trust");
8226
8539
  ctx.effect(() => {
8227
- const desktopAdmin = isDesktopAdminSurface(location.hostname, location.search, window.__DSH_MOBILE_FRONTEND__);
8540
+ const desktopAdmin = isDesktopAdminSurface(location.hostname, location.search, window.__DSH_MOBILE_FRONTEND__, location.protocol);
8228
8541
  const style = element("style");
8229
8542
  style.dataset.plugin = "dsh-mobile";
8230
8543
  style.textContent = desktopAdmin ? CONTROL_STYLES : NATIVE_MOBILE_STYLES;