dsh-bridge-gateway 0.2.2 → 0.3.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/README.md +17 -3
  3. package/README.zh.md +18 -2
  4. package/assets/apple-touch-icon-180.png +0 -0
  5. package/assets/icon-192.png +0 -0
  6. package/assets/icon-512.png +0 -0
  7. package/assets/icon-maskable-192.png +0 -0
  8. package/assets/icon-maskable-512.png +0 -0
  9. package/client/client.js +302 -41
  10. package/client/index.js +438 -42
  11. package/docs/banner.jpg +0 -0
  12. package/docs/custom-tunnel.md +5 -5
  13. package/docs/platform-abstraction-design.md +1 -1
  14. package/docs/qq-usage.md +6 -6
  15. package/docs/screenshots/remote-auth-login-light.jpg +0 -0
  16. package/docs/screenshots/remote-auth-login.jpg +0 -0
  17. package/docs/wechat-usage.md +1 -1
  18. package/lib/auth/brand-assets.js +131 -0
  19. package/lib/auth/login-template.js +503 -361
  20. package/lib/auth/manager.js +47 -12
  21. package/lib/auth/theme-tokens.js +142 -0
  22. package/lib/bridge-rpc.js +2 -2
  23. package/lib/cloudflared-manager.mjs +1 -1
  24. package/lib/feishu/gateway.js +17 -17
  25. package/lib/feishu/index.js +3 -3
  26. package/lib/feishu/lark-bundled.mjs +288 -469
  27. package/lib/feishu/node.js +7 -7
  28. package/lib/gateway/cert.js +1 -1
  29. package/lib/index.js +210 -80
  30. package/lib/paths.js +43 -14
  31. package/lib/platform/base.js +2 -2
  32. package/lib/platform/conversation-bridge.js +17 -17
  33. package/lib/platform/index.js +1 -1
  34. package/lib/platform/manager.js +4 -4
  35. package/lib/qq/gateway.js +10 -10
  36. package/lib/qq/index.js +11 -11
  37. package/lib/qq/node.js +17 -17
  38. package/lib/telegram/gateway.js +10 -10
  39. package/lib/telegram/index.js +3 -3
  40. package/lib/telegram/node.js +5 -5
  41. package/lib/wechat/gateway.js +6 -6
  42. package/lib/wechat/index.js +4 -4
  43. package/lib/wechat/media.js +1 -1
  44. package/lib/wechat/node.js +13 -13
  45. package/package.json +6 -3
package/CHANGELOG.md CHANGED
@@ -2,6 +2,80 @@
2
2
 
3
3
  本项目 `dsh-bridge-gateway` 是基于 [dsh-bridge](https://github.com/wenbin-wb/dsh-bridge) 的移植增强分支,在保留原版能力之外,重点新增「公网直连网关」。
4
4
 
5
+ ## [0.3.0] - 2026-10-17
6
+
7
+ ### 新增:PWA 主屏图标(真 PNG + DSH 业务蓝)
8
+
9
+ 此前 manifest 只声明了 `image/svg+xml` 图标,且 `/apple-touch-icon.png` 也返回 SVG。
10
+ **华为 HarmonyOS 与多数 Android 启动器不支持 SVG 图标**,于是「添加到桌面」后得到的是一块
11
+ 空白图标。底色也由近黑 `#151517` 改为 DSH 业务蓝 `#4176e6` —— 近黑底在深色桌面上会糊成
12
+ 一块「光板」,这也是「图标是空白的」观感来源之一。
13
+
14
+ - 新增 `scripts/generate-icons.mjs`:用无头 Chrome 渲染 192/512 的 `any` 与 `maskable` 两版,
15
+ 外加 iOS 的 180;产物入库 `assets/`
16
+ - manifest 声明 4 个 PNG;`/apple-touch-icon.png` 改为返回真 PNG
17
+ - `maskable` 版满幅不留圆角 —— 启动器会按自己的形状裁切,自带圆角会被裁掉一半
18
+
19
+ ### 新增:远程登录页改版(DSH 元素)
20
+
21
+ 登录页重做为门厅式设计,与 DSH 本体同源同一枚鲸鱼。
22
+
23
+ - `lib/auth/brand-assets.js`:品牌资产单一来源,附 sha256 前缀校验
24
+ - `lib/auth/theme-tokens.js`:18 个 `--dsw-*` token 的静态十六进制回落 —— 登录页在 DSH
25
+ 之前渲染、加载不到 token,**每个 token 都必须有回落**
26
+ - 响应头:`no-store` + CSP。其中 `connect-src 'self'` 是登录 `fetch` 的必需项,漏掉会登录失败
27
+
28
+ ### 修复:移动端 8 项问题
29
+
30
+ | # | 症状 | 根因 |
31
+ |---|---|---|
32
+ | 1 | 主屏图标空白 | 见上:HarmonyOS 不支持 SVG 图标 |
33
+ | 2 | 打开渠道中心时面板标题被顶栏盖住 | 插件顶栏 `position:fixed` + `z-index:9998` 不感知面板状态(面板 `z-index:80`,其标题落在 y19,正被顶栏的 y0-40 覆盖) |
34
+ | 3 | 会话名恒为「新会话」 | DSH 0.1.7 起 sessions list 快照不再提供 `current` 字段,旧实现读它 → 恒 `undefined` → **静默回落**默认文案(不报错,很难发现)。改读 `retainedBy.mainView > 0` 的那一个(与 `dsh-client-ui-session` 的判据一致) |
35
+ | 4 | 轨迹视图内容被裁 | `table` 元素的 `overflow` 在 CSS 里**是被忽略的**,只写 `overflow-x:auto` 无效,必须配 `display:block`。另:工具调用参数列内容宽 3049px 而可见仅 180px(17×),横滑读不了,改为换行 |
36
+ | 5 | 输入框压住内容 | 见下「已知问题」(两次尝试均已回退) |
37
+ | 6 | 顶部两层头过高、挤压对话区 | **DSH 自带 `min-height:76px`**,只改 padding 压不下去;显式清零后内容区 +51px |
38
+ | 7 | 「对话」视图下拉被内容盖住 | 祖先链 `titleRow` 与 `wSkVaW_root` 都是 `overflow:hidden`,下拉落在头部下方被**整块裁掉**(z-index 再高也没用,裁剪发生在层叠之前)。改为 body 级 portal |
39
+ | 8 | **滚不到最新会话**(只渲染约 4 条消息) | 插件给 `_frame` 注入的 `height:100dvh` + `overflow:hidden` 组合使 DSH 的布局测量算错,驻留窗口塌缩成 4 条。去掉这两个属性组后 `scrollHeight` 由 996 恢复到 11295 |
40
+
41
+ ### 已知问题:#5 输入框压住内容(未修)
42
+
43
+ `composerSeat` 是 `position:sticky` 且为 `scrollBody` 的直接 flex 子项,吸住时会盖在滚动内容
44
+ 之上。**两次尝试修复(改 `fixed` + 补 padding;再用 `visualViewport` 算键盘高度)都破坏了 DSH
45
+ 的滚动锚定,均已回退。**
46
+
47
+ DSH 另有「覆盖层模式」(`:has([data-conversation-composer-overlay])`:`composerSeat` 转
48
+ `absolute`、`viewArea` 自己滚),但那是 `dsh-client-ui-trajectory`(轨迹视图)在用的,对话
49
+ 视图没有开。**要根治得走 DSH 侧,插件 CSS 补不了。**
50
+
51
+ ### 新增:真机诊断能力(长期保留)
52
+
53
+ 真机上稳定复现、桌面视口复现不出的问题,可以不改代码直接二分:
54
+
55
+ | 参数 | 作用 |
56
+ |---|---|
57
+ | `?dsh-diag=1` | 只读指标叠加层(滚动容器几何、`scrollHeight` / `clientHeight`、输入框位置、视口三件套)。与 `dsh-mobile` 独立,因此可对比「移动层开/关」两态 |
58
+ | `?dsh-mobile=off` | 完全禁用移动端增强层(CSS + JS) |
59
+ | `?dsh-mobile=no-new` | 保留承重布局,只关掉最近新增的那批规则 |
60
+ | `?dsh-mobile=no-<分组>` | 关掉某一组规则(`no-frameLayout` / `no-centercol` / `no-pad` / `no-observer`),逗号分隔可叠加 |
61
+ | `?dsh-mobile=on` | 恢复正常 |
62
+
63
+ 选择记进 `localStorage`,因此在没有地址栏的 PWA 里同样生效。
64
+
65
+ ### 变更:品牌名统一为 dsh-bridge-gateway
66
+
67
+ 插件自身标识、日志前缀、数据目录、cloudflared 缓存目录全部改名,并对旧名做**两级链自动迁移**
68
+ (`~/.dsh-bridge/` → `~/.dsh-bridge-cloudflared/` → `~/.dsh-bridge-gateway-cloudflared/`),
69
+ 用户无需重下 cloudflared。
70
+
71
+ 上游仓库名 `wenbin-wb/dsh-bridge` 与兼容性标识(`LEGACY_DATA_DIR_NAME`、
72
+ `BRIDGE_RPC_CHANNEL_LEGACY`、CHANGELOG 历史)按约定保留。
73
+
74
+ ### 测试
75
+
76
+ 269 项全绿;新增 12 条护栏,逐条用注入 bug 验证过会变红。每条护栏都写了「为什么」(踩过的坑)。
77
+ 中英 README 同步(章节数 / 标题层级 / 逐章节条目数 / 表格行数零差异)。
78
+
5
79
  ## [0.2.2] - 2026-10-11
6
80
 
7
81
  ### 修复:旧内核浏览器打开后工作区永远加载不了
package/README.md CHANGED
@@ -14,7 +14,7 @@ This project's **upstream is [wenbin-wb/dsh-bridge](https://github.com/wenbin-wb
14
14
 
15
15
  ### Who upstream is
16
16
 
17
- | | |
17
+ | Field | Value |
18
18
  |---|---|
19
19
  | Repository | [wenbin-wb/dsh-bridge](https://github.com/wenbin-wb/dsh-bridge) |
20
20
  | Role | Multi-channel remote access and security plugin for DSH |
@@ -74,7 +74,7 @@ The plugin was renamed from `dsh-bridge` to **`dsh-bridge-gateway`** (reflecting
74
74
  | Path | Change |
75
75
  |---|---|
76
76
  | `<DSH_HOME>/dsh-bridge/` | → `<DSH_HOME>/dsh-bridge-gateway/` (migrated automatically at startup) |
77
- | `~/.dsh-bridge/` | → `~/.dsh-bridge-cloudflared/` (cloudflared cache; migrated automatically, no re-download) |
77
+ | `~/.dsh-bridge/` | → `~/.dsh-bridge-gateway-cloudflared/` (cloudflared cache; **two-hop auto-migration**, no 39MB re-download) |
78
78
  | RPC channel `/dsh-bridge` | → `/dsh-bridge-gateway`, with **both channels served**, so stale cached pages keep working |
79
79
 
80
80
  ## Features
@@ -89,7 +89,7 @@ The plugin was renamed from `dsh-bridge` to **`dsh-bridge-gateway`** (reflecting
89
89
  - **Visitor management.** See live who is currently connected (LAN / direct gateway / tunnel, with the real source IP behind a tunnel) plus an IP blacklist. **Viewing is open to every visitor**; disconnecting and blacklisting require admin rights.
90
90
  - **IM bots.** WeChat / QQ / Feishu / Telegram bots connect through their own gateway links — no public IP needed. Per-platform workspace / agent preset / model selection is configurable. Guides live in [docs/](./docs).
91
91
  - **Collapsible cards.** Cards in the Access and Security tabs collapse; only the first in each group is expanded by default, and a collapsed card keeps its status tag plus one line of key information.
92
- - **Mobile-friendly web.** The remote web UI is deeply adapted for phone screens (drawer sidebar, ~44px touch targets, bottom-stuck composer, and more).
92
+ - **Mobile-friendly web.** The remote web UI is deeply adapted for phone screens (drawer sidebar, ~44px touch targets, bottom-stuck composer, and more). The gateway also serves real **PNG** home-screen icons (DSH business blue + white whale, in both `any` and `maskable` variants) together with a Web App Manifest, so "Add to Home Screen" yields a proper icon rather than a blank tile.
93
93
  - **All configuration in the UI.** Every feature above is configured from DSH Web's remote-access panel; nothing is edited by hand.
94
94
 
95
95
  ## Interface
@@ -216,6 +216,20 @@ dsh plugin --profile web add .
216
216
 
217
217
  Tests point `DSH_HOME` at a throwaway temporary directory via `test/isolate-home.mjs`, so they never touch your real environment.
218
218
 
219
+ ### Diagnosing phone-only layout problems
220
+
221
+ Bugs that reproduce on a phone but never in a desktop viewport can be bisected without touching code. Append a query parameter to the remote URL; the choice is remembered in `localStorage`, so it also works inside an installed PWA (which has no address bar).
222
+
223
+ | Parameter | Effect |
224
+ |---|---|
225
+ | `?dsh-diag=1` | Overlays a read-only metrics panel (scroll-container geometry, `scrollHeight` / `clientHeight`, composer position, viewport triple). It is independent of `dsh-mobile`, so the layer can be compared on against off. |
226
+ | `?dsh-mobile=off` | Disables the whole mobile enhancement layer (CSS and JS). |
227
+ | `?dsh-mobile=no-new` | Keeps the load-bearing layout and drops only the most recently added rules. |
228
+ | `?dsh-mobile=no-<group>` | Drops one rule group: `no-frameLayout`, `no-centercol`, `no-pad`, `no-observer`. Comma-separate to combine. |
229
+ | `?dsh-mobile=on` | Restores normal behaviour. |
230
+
231
+ Start by capturing `?dsh-diag=1` twice — once as-is and once with `?dsh-mobile=off` — because the delta between those two states is usually enough to name the culprit.
232
+
219
233
  This project is forked from [wenbin-wb/dsh-bridge](https://github.com/wenbin-wb/dsh-bridge). Report general issues upstream and direct-gateway issues to this repository.
220
234
 
221
235
  ## Release notes
package/README.zh.md CHANGED
@@ -97,7 +97,7 @@ resolve、远程流层直接抛错 —— 于是外壳渲染得出来,工作
97
97
  | 路径 | 变化 |
98
98
  |---|---|
99
99
  | `<DSH_HOME>/dsh-bridge/` | → `<DSH_HOME>/dsh-bridge-gateway/`(启动时自动迁移) |
100
- | `~/.dsh-bridge/` | → `~/.dsh-bridge-cloudflared/`(cloudflared 缓存,自动迁移,免重下) |
100
+ | `~/.dsh-bridge/` | → `~/.dsh-bridge-gateway-cloudflared/`(cloudflared 缓存,**两级链自动迁移**,免重下 39MB) |
101
101
  | RPC 通道 `/dsh-bridge` | → `/dsh-bridge-gateway`,**新旧双通道并存**,旧缓存页面仍可用 |
102
102
 
103
103
  ## 功能
@@ -123,7 +123,8 @@ resolve、远程流层直接抛错 —— 于是外壳渲染得出来,工作
123
123
  - **卡片折叠。** 访问配置与安全认证两个 Tab 的卡片都可折叠,默认只展开首张,
124
124
  折叠后仍保留状态标签与一行关键信息。
125
125
  - **移动端适配。** 远程网页 UI 针对手机屏幕深度适配(抽屉化侧边栏、点击热区扩至约 44px、
126
- 输入框贴底等)。
126
+ 输入框贴底等)。网关另外提供真实的 **PNG** 主屏图标(DSH 业务蓝 + 白色鲸鱼,含 `any`
127
+ 与 `maskable` 两版)与 Web App Manifest,"添加到桌面"得到的是正常图标而非空白方块。
127
128
  - **全界面配置。** 以上全部功能都在 DSH Web 远程访问面板里配置,无需手改任何文件。
128
129
 
129
130
  ## 界面
@@ -277,6 +278,21 @@ dsh plugin --profile web add .
277
278
  测试会通过 `test/isolate-home.mjs` 把 `DSH_HOME` 指向一次性临时目录,不会污染你本机的
278
279
  真实环境。
279
280
 
281
+ ### 排查只在手机上出现的问题
282
+
283
+ 桌面视口复现不出、真机稳定复现的问题,可以不改代码直接二分。在远程地址后加查询参数即可;
284
+ 选择会记进 `localStorage`,所以在没有地址栏的 PWA 里同样生效。
285
+
286
+ | 参数 | 作用 |
287
+ |---|---|
288
+ | `?dsh-diag=1` | 叠加只读指标面板(滚动容器几何、`scrollHeight` / `clientHeight`、输入框位置、视口三件套)。它与 `dsh-mobile` 相互独立,因此能对比移动层「开」与「关」两种状态。 |
289
+ | `?dsh-mobile=off` | 完全禁用移动端增强层(CSS 与 JS)。 |
290
+ | `?dsh-mobile=no-new` | 保留承重布局,只关掉最近新增的那批规则。 |
291
+ | `?dsh-mobile=no-<分组>` | 关掉某一组规则:`no-frameLayout`、`no-centercol`、`no-pad`、`no-observer`,逗号分隔可叠加。 |
292
+ | `?dsh-mobile=on` | 恢复正常。 |
293
+
294
+ 第一步先取两次 `?dsh-diag=1` —— 一次原样、一次带 `?dsh-mobile=off` —— 两者差值通常就足以定位元凶。
295
+
280
296
  本项目 fork 自 [wenbin-wb/dsh-bridge](https://github.com/wenbin-wb/dsh-bridge)。
281
297
  向上游反馈通用问题、向本仓库反馈直连网关相关问题。
282
298
 
Binary file
Binary file
Binary file
Binary file
Binary file
package/client/client.js CHANGED
@@ -2639,7 +2639,7 @@ function BackupRestoreWidget({ rpcCall, onUpdate }) {
2639
2639
  const now = /* @__PURE__ */ new Date();
2640
2640
  const dateStr = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, "0")}${String(now.getDate()).padStart(2, "0")}`;
2641
2641
  a.href = url;
2642
- a.download = `dsh-bridge-backup-${dateStr}.json`;
2642
+ a.download = `dsh-bridge-gateway-backup-${dateStr}.json`;
2643
2643
  document.body.appendChild(a);
2644
2644
  a.click();
2645
2645
  document.body.removeChild(a);
@@ -3935,11 +3935,165 @@ function BridgePanel({ rpcCall }) {
3935
3935
  )
3936
3936
  );
3937
3937
  }
3938
+ function setupMobileDiag() {
3939
+ if (document.getElementById("dsh-mobile-diag")) return;
3940
+ const box = document.createElement("div");
3941
+ box.id = "dsh-mobile-diag";
3942
+ box.style.cssText = [
3943
+ "position:fixed",
3944
+ "left:0",
3945
+ "right:0",
3946
+ "top:0",
3947
+ "z-index:2147483647",
3948
+ "background:rgba(0,0,0,.86)",
3949
+ "color:#0f0",
3950
+ "pointer-events:none",
3951
+ "font:11px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace",
3952
+ "padding:5px 7px",
3953
+ "white-space:pre-wrap",
3954
+ "word-break:break-all"
3955
+ ].join(";");
3956
+ document.body.appendChild(box);
3957
+ const tick = () => {
3958
+ const sb = document.querySelector('[class*="wSkVaW_scrollBody"]');
3959
+ const seat = document.querySelector('[class*="wSkVaW_composerSeat"]');
3960
+ const vv = window.visualViewport;
3961
+ const L = [];
3962
+ if (sb) {
3963
+ const r = sb.getBoundingClientRect();
3964
+ const max = sb.scrollHeight - sb.clientHeight;
3965
+ L.push("scrollBody top=" + Math.round(r.top) + " h=" + Math.round(r.height) + " pad=" + getComputedStyle(sb).paddingBottom);
3966
+ L.push(" scrollTop=" + Math.round(sb.scrollTop) + " max=" + max + " atBottom=" + (sb.scrollTop >= max - 2));
3967
+ L.push(" scrollH=" + sb.scrollHeight + " clientH=" + sb.clientHeight + " children=" + sb.children.length);
3968
+ const idx = sb.querySelectorAll("[data-index]");
3969
+ L.push(" virtItems=" + idx.length);
3970
+ } else {
3971
+ L.push("scrollBody: NOT FOUND");
3972
+ }
3973
+ if (seat) {
3974
+ const r = seat.getBoundingClientRect();
3975
+ L.push("composer pos=" + getComputedStyle(seat).position + " top=" + Math.round(r.top) + " h=" + Math.round(r.height));
3976
+ }
3977
+ L.push("inner=" + window.innerHeight + " vv=" + (vv ? Math.round(vv.height) : "-") + " vvTop=" + (vv ? Math.round(vv.offsetTop) : "-") + " dpr=" + window.devicePixelRatio + " w=" + window.innerWidth);
3978
+ L.push("body=" + Array.from(document.body.classList).join("."));
3979
+ box.textContent = L.join("\n");
3980
+ };
3981
+ tick();
3982
+ window.setInterval(tick, 500);
3983
+ window.addEventListener("scroll", tick, true);
3984
+ window.addEventListener("resize", tick);
3985
+ }
3986
+ var SUSPECT_CSS = {
3987
+ // _frame 规则 —— 真机二分结论(2026-10-17):
3988
+ // ?dsh-mobile=no-frameSize (去掉 width:100vw + height:100dvh)→ scrollH 恢复正常
3989
+ // ?dsh-mobile=no-frameOverflow(去掉 overflow:hidden) → scrollH 恢复正常
3990
+ // 两个属性各自都能触发,说明是「固定高度 + 裁剪」这个组合在作怪:
3991
+ // height:100dvh 把主框架钉成固定高、overflow:hidden 又让它成为裁剪容器,
3992
+ // DSH 的布局测量因此算错,导致只渲染约 4 条消息(scrollH≈795,正常应 10 万+)。
3993
+ // 故**这两个属性组已删除**,不再注入。只保留 padding-top(本规则的目的:
3994
+ // 为固定顶栏腾出空间)与 display/flex-direction/position(抽屉化的前提)。
3995
+ // 注意 grid-template-columns:1fr 也已去掉 —— 它是 grid 属性,在 flex 容器里无效。
3996
+ // padding-top / box-sizing 是这条规则的**目的**(为固定顶栏腾空间),任何分组都保留
3997
+ frameBase: `
3998
+ div[class*="_frame"] {
3999
+ padding-top: var(--dsh-mobile-header-h) !important;
4000
+ margin: 0 !important;
4001
+ box-sizing: border-box !important;
4002
+ }`,
4003
+ frameLayout: `
4004
+ div[class*="_frame"] {
4005
+ display: flex !important;
4006
+ flex-direction: column !important;
4007
+ position: relative !important;
4008
+ }`,
4009
+ centercol: ` div[class*="_centerCol"] {
4010
+ flex: 1 1 100% !important;
4011
+ width: 100% !important;
4012
+ min-width: 0 !important;
4013
+ max-width: 100% !important;
4014
+ display: flex !important;
4015
+ height: 100% !important;
4016
+ }`,
4017
+ pad: ` div[class*="wSkVaW_scrollBody"] {
4018
+ padding-bottom: max(16px, env(safe-area-inset-bottom)) !important;
4019
+ }
4020
+ div[class*="wSkVaW_scrollBody"] {
4021
+ padding-bottom: calc(6px + env(safe-area-inset-bottom, 0px)) !important;
4022
+ }`
4023
+ };
4024
+ function injectSuspectStyles(disabled) {
4025
+ const parts = Object.entries(SUSPECT_CSS).filter(([k]) => !disabled.has(k)).map(([, v]) => v);
4026
+ if (parts.length === 0) return;
4027
+ if (document.getElementById("dsh-bridge-gateway-mobile-styles-suspect")) return;
4028
+ const style = document.createElement("style");
4029
+ style.id = "dsh-bridge-gateway-mobile-styles-suspect";
4030
+ style.textContent = "@media (max-width: 768px) {\n" + parts.join("\n") + "\n}";
4031
+ document.head.appendChild(style);
4032
+ }
4033
+ var NEW_MOBILE_CSS = `
4034
+ /* #2 \u9762\u677F\u6253\u5F00\u65F6\u9876\u680F\u8BA9\u4F4D\uFF08body.dsh-panel-open \u7531 setupPanelAwareness \u6253\u4E0A\uFF09\u3002
4035
+ \u5B9E\u6D4B\uFF1A\u6E20\u9053\u4E2D\u5FC3\u9762\u677F\u6807\u9898\u5728 y19\uFF0C\u800C\u9876\u680F\u5360 y0-40 \u2192 \u4E0D\u6539\u5C31\u662F\u6807\u9898\u88AB\u76D6\u4F4F\u3002
4036
+ \u4E0D\u7528 :has() \u662F\u56E0\u4E3A\u6784\u5EFA\u76EE\u6807\u662F chrome100\uFF08:has() \u8981 105+\uFF09\u3002 */
4037
+ body.dsh-panel-open .dsh-mobile-app-header {
4038
+ display: none !important;
4039
+ }
4040
+
4041
+ /* #6 \u53CC\u5C42\u5934\u538B\u7F29\uFF1ADSH \u81EA\u5E26 min-height:76px\uFF0C\u53EA\u6539 padding \u538B\u4E0D\u4E0B\u53BB \u2014\u2014 \u5B9E\u6D4B
4042
+ 76px \u4E00\u76F4\u751F\u6548\u3001titleRow \u88AB\u62C9\u4F38\u586B\u6EE1\uFF0869px\uFF09\uFF0C\u4E24\u5C42\u5934\u5408\u8BA1\u5403\u6389 128px\uFF08\u5C4F\u5E55 15%\uFF09\u3002
4043
+ \u663E\u5F0F\u6E05\u96F6 + \u628A padding \u6536\u5230 2px\uFF0C\u5B9E\u6D4B\u5185\u5BB9\u533A +51px\u3002 */
4044
+ div[class*="_centerCol"] header,
4045
+ header[class*="wSkVaW_header"] {
4046
+ min-height: 0 !important;
4047
+ padding: 2px 16px !important;
4048
+ }
4049
+ div[class*="wSkVaW_titleRow"] {
4050
+ min-height: 0 !important;
4051
+ height: 32px !important;
4052
+ }
4053
+
4054
+ /* #4-C \u8868\u683C\u6A2A\u5411\u6EDA\u52A8\uFF1Atable \u5143\u7D20\u7684 overflow \u5728 CSS \u91CC\u662F**\u88AB\u5FFD\u7565**\u7684\uFF0C\u5FC5\u987B\u914D
4055
+ display:block \u624D\u4F1A\u53D8\u6210\u53EF\u6EDA\u5757\u76D2\uFF08\u63D2\u4EF6\u539F\u6765\u53EA\u5199\u4E86 overflow-x:auto\uFF0C\u6240\u4EE5\u65E0\u6548\uFF09\u3002 */
4056
+ div[class*="wSkVaW_scrollBody"] table {
4057
+ display: block !important;
4058
+ max-width: 100% !important;
4059
+ width: 100% !important;
4060
+ overflow-x: auto !important;
4061
+ -webkit-overflow-scrolling: touch !important;
4062
+ }
4063
+
4064
+ /* #4-E \u8F68\u8FF9\u91CC\u7684\u5DE5\u5177\u8C03\u7528\u53C2\u6570\u6362\u884C\uFF1A\u8BE5\u5217\u5185\u5BB9\u5BBD 3049px \u5374\u53EA\u6709 180px \u53EF\u89C1\uFF0817\xD7\uFF09\uFF0C
4065
+ \u6A2A\u6ED1\u8981\u5212 17 \u5C4F\u3001\u5B9E\u9645\u8BFB\u4E0D\u4E86\u3002overflow-wrap:anywhere \u662F\u5FC5\u987B\u7684 \u2014\u2014 bash \u547D\u4EE4\u91CC
4066
+ \u5E38\u6709\u8D85\u957F\u65E0\u7A7A\u683C token\u3002 */
4067
+ div[class*="wSkVaW_scrollBody"] [class*="resultRequest"] {
4068
+ white-space: normal !important;
4069
+ overflow-wrap: anywhere !important;
4070
+ word-break: break-word !important;
4071
+ text-overflow: clip !important;
4072
+ overflow: visible !important;
4073
+ }
4074
+
4075
+ /* #7 \u4E0B\u62C9\u6539 body \u7EA7 portal\uFF08position:fixed\uFF09\u3002\u4E3A\u4EC0\u4E48\u4E0D\u80FD absolute \u6302\u5728\u6309\u94AE\u91CC\uFF1A
4076
+ \u7956\u5148\u94FE titleRow \u4E0E wSkVaW_root \u90FD\u662F overflow:hidden\uFF08titleRow \u90A3\u6761\u8FD8\u662F\u63D2\u4EF6
4077
+ \u81EA\u5DF1\u4E3A\u4E86\u7701\u7565\u53F7\u8BBE\u7684\uFF09\u2192 \u4E0B\u62C9\u843D\u5728\u5934\u90E8\u4E0B\u65B9 y78 \u4F1A\u88AB**\u6574\u5757\u88C1\u6389**\uFF0C\u5B9E\u6D4B\u4E2D\u5FC3\u70B9\u547D\u4E2D
4078
+ \u7684\u662F\u6D88\u606F\u6587\u672C\u3002z-index \u518D\u9AD8\u4E5F\u6CA1\u7528\uFF0C\u88C1\u526A\u53D1\u751F\u5728\u5C42\u53E0\u4E4B\u524D\u3002
4079
+ \u6CE8\u610F\uFF1A**\u4E0D\u80FD**\u5199 top/left \u7684 !important \u2014\u2014 \u90A3\u4F1A\u538B\u8FC7 positionTabDropdown()
4080
+ \u5199\u7684\u5185\u8054\u5750\u6807\uFF0C\u628A\u4E0B\u62C9\u9489\u5728\u5DE6\u4E0A\u89D2\u3002 */
4081
+ .dsh-mobile-tab-dropdown {
4082
+ position: fixed !important;
4083
+ }
4084
+ `;
4085
+ function injectNewMobileStyles() {
4086
+ if (document.getElementById("dsh-bridge-gateway-mobile-styles-new")) return;
4087
+ const style = document.createElement("style");
4088
+ style.id = "dsh-bridge-gateway-mobile-styles-new";
4089
+ style.textContent = "@media (max-width: 768px) {\n" + NEW_MOBILE_CSS + "\n}";
4090
+ document.head.appendChild(style);
4091
+ }
3938
4092
  function injectMobileStyles() {
3939
4093
  if (typeof document === "undefined") return;
3940
- if (document.getElementById("dsh-bridge-mobile-styles")) return;
4094
+ if (document.getElementById("dsh-bridge-gateway-mobile-styles")) return;
3941
4095
  const style = document.createElement("style");
3942
- style.id = "dsh-bridge-mobile-styles";
4096
+ style.id = "dsh-bridge-gateway-mobile-styles";
3943
4097
  style.textContent = `
3944
4098
  /* DSH Bridge \u9690\u85CF Tab \u680F\u539F\u751F\u6EDA\u52A8\u6761\u5E76\u4FDD\u6301\u5E73\u6ED1\u6ED1\u52A8 */
3945
4099
  .dsh-tabbar-container {
@@ -3954,25 +4108,16 @@ function injectMobileStyles() {
3954
4108
 
3955
4109
  /* DSH Bridge \u79FB\u52A8\u7AEF\u81EA\u9002\u5E94\u4E0E\u89E6\u63A7\u4EA4\u4E92\u589E\u5F3A\u6837\u5F0F */
3956
4110
  :root {
3957
- --dsh-mobile-header-h: 52px;
4111
+ /* \u9876\u680F\u9AD8\u5EA6 52 -> 40\uFF082026-10-16 \u771F\u673A\u53CD\u9988\uFF1A"\u4E0A\u9762\u9AD8\u5EA6\u592A\u9AD8\u4E86\uFF0C\u628A\u5BF9\u8BDD\u6846\u7684\u7A7A\u95F4\u538B\u7F29\u4E86"\uFF09\u3002
4112
+ 40px \u4ECD\u80FD\u5BB9\u7EB3 32px \u7684 \u2630/\u2295 \u6309\u94AE\u4E0E\u4E00\u884C\u4F1A\u8BDD\u540D\u3002 */
4113
+ --dsh-mobile-header-h: 40px;
3958
4114
  --dsh-mobile-safe-top: env(safe-area-inset-top, 0px);
3959
4115
  --dsh-mobile-safe-bottom: env(safe-area-inset-bottom, 0px);
3960
4116
  }
3961
4117
 
3962
4118
  @media (max-width: 768px) {
3963
4119
  /* 1. \u4E3B\u6846\u67B6\u4E3A Header \u817E\u51FA\u9876\u90E8\u7A7A\u95F4 */
3964
- div[class*="_frame"] {
3965
- display: flex !important;
3966
- flex-direction: column !important;
3967
- width: 100vw !important;
3968
- height: 100dvh !important;
3969
- margin: 0 !important;
3970
- padding-top: var(--dsh-mobile-header-h) !important;
3971
- position: relative !important;
3972
- grid-template-columns: 1fr !important;
3973
- overflow: hidden !important;
3974
- box-sizing: border-box !important;
3975
- }
4120
+
3976
4121
 
3977
4122
  /* 2. \u9876\u90E8\u539F\u751F\u5BFC\u822A\u6761\uFF1A100% \u8FD8\u539F DeepSeek App (\u5DE6\u4FA7\u53CC\u6A2A\u7EBF\uFF0C\u53F3\u4FA7(+)\uFF0C\u4E2D\u95F4\u7559\u767D\uFF0C\u65E0\u591A\u4F59\u8BBE\u7F6E\u6309\u94AE) */
3978
4123
  .dsh-mobile-app-header {
@@ -4052,14 +4197,7 @@ function injectMobileStyles() {
4052
4197
  }
4053
4198
 
4054
4199
  /* 3. \u4E2D\u95F4\u4E3B\u5185\u5BB9\u533A\u4E0E\u8F93\u5165\u6846 */
4055
- div[class*="_centerCol"] {
4056
- flex: 1 1 100% !important;
4057
- width: 100% !important;
4058
- min-width: 0 !important;
4059
- max-width: 100% !important;
4060
- display: flex !important;
4061
- height: 100% !important;
4062
- }
4200
+
4063
4201
 
4064
4202
  div[class*="_detailsCol"],
4065
4203
  div[class*="toggleCluster"],
@@ -4450,9 +4588,7 @@ function injectMobileStyles() {
4450
4588
  }
4451
4589
 
4452
4590
  /* \u8F93\u5165\u6846\u5E95\u5EA7\uFF1ADeepSeek App \u5C45\u4E2D\u53CA\u5E95\u90E8\u56FA\u5B9A */
4453
- div[class*="wSkVaW_scrollBody"] {
4454
- padding-bottom: max(16px, env(safe-area-inset-bottom)) !important;
4455
- }
4591
+
4456
4592
 
4457
4593
  /* \u8F93\u5165\u5361\u7247\uFF1ADeepSeek App \u5706\u89D2\u5927\u80F6\u56CA\u9020\u578B */
4458
4594
  div[class*="uV2eYG_card"] {
@@ -4646,11 +4782,20 @@ function injectMobileStyles() {
4646
4782
  }
4647
4783
 
4648
4784
  /* \u8F93\u5165\u6846\u8D34\u5E95\uFF1AscrollBody \u7684 16px \u5E95\u90E8\u886C\u8DDD\u662F\u7A7A\u9699\u6765\u6E90\uFF0C\u6536\u6389\u53EA\u7559\u5B89\u5168\u533A\uFF1B
4649
- \u518D\u57AB 6px \u8BA9\u7EDF\u8BA1\u836F\u4E38\u5B8C\u6574\u53EF\u89C1 */
4650
- div[class*="wSkVaW_scrollBody"] {
4651
- padding-bottom: calc(6px + env(safe-area-inset-bottom, 0px)) !important;
4652
- }
4653
- div[class*="wSkVaW_composerStack"] {
4785
+ \u518D\u57AB 6px \u8BA9\u7EDF\u8BA1\u836F\u4E38\u5B8C\u6574\u53EF\u89C1\u3002
4786
+
4787
+ \u26A0\uFE0F **\u4E0D\u8981**\u518D\u8BD5\u56FE\u5728\u8FD9\u91CC\u52A0 --dsh-composer-height\u3001\u4E5F\u4E0D\u8981\u7ED9 composerSeat \u52A0
4788
+ position:fixed\uFF082026-10-16 \u4E24\u6B21\u5C1D\u8BD5\uFF0C\u5747\u5DF2\u56DE\u9000\uFF09\u3002\u539F\u56E0\uFF1ADSH \u7684\u6EDA\u52A8\u951A\u5B9A
4789
+ \uFF08dsh-client-ui-conversation\uFF09\u662F**\u6309 DOM \u6D41\u5185\u4F4D\u7F6E**\u904D\u5386\u7956\u5148\u7B97\u504F\u79FB\u6765\u4FEE\u6B63
4790
+ scrollTop \u7684\uFF1B\u628A composer \u6458\u51FA\u6587\u6863\u6D41\u3001\u6216\u7ED9\u6EDA\u52A8\u5185\u5BB9\u52A0 padding\uFF0C\u951A\u5B9A\u5C31\u7B97\u9519\uFF0C
4791
+ \u771F\u673A\u75C7\u72B6\u662F\u300C\u8FDB\u53BB\u505C\u5728\u5F88\u4E45\u4EE5\u524D\u7684\u67D0\u6761\uFF0C\u70B9\u300E\u52A0\u8F7D\u66F4\u65E9\u300F\u540E\u518D\u4E5F\u56DE\u4E0D\u5230\u6700\u65B0\u300D\u3002
4792
+ \u8FD9\u4E2A\u906E\u6321\u662F DSH \u5BF9\u8BDD\u89C6\u56FE sticky \u5438\u5E95\u8BBE\u8BA1\u7684\u56FA\u6709\u4EE3\u4EF7\uFF1BDSH \u53E6\u6709"\u8986\u76D6\u5C42\u6A21\u5F0F"
4793
+ \uFF08:has([data-conversation-composer-overlay])\uFF1AcomposerSeat \u8F6C absolute\u3001
4794
+ viewArea \u81EA\u5DF1\u6EDA\uFF09\uFF0C\u4F46\u90A3\u662F **dsh-client-ui-trajectory\uFF08\u8F68\u8FF9\u89C6\u56FE\uFF09** \u5728\u7528\u7684\uFF0C
4795
+ \u5BF9\u8BDD\u89C6\u56FE\u6CA1\u6709\u5F00\u3002\u8981\u6839\u6CBB\u5F97\u8D70 DSH \u4FA7\uFF0C\u4E0D\u662F\u63D2\u4EF6 CSS \u80FD\u8865\u7684\u3002 */
4796
+
4797
+
4798
+ div[class*="wSkVaW_composerStack"] {
4654
4799
  padding-bottom: env(safe-area-inset-bottom, 0px) !important;
4655
4800
  }
4656
4801
 
@@ -4957,15 +5102,15 @@ function injectMobileStyles() {
4957
5102
  box-sizing: border-box !important;
4958
5103
  }
4959
5104
 
4960
- /* 7. \u4EE3\u7801\u5757\u3001\u8868\u683C\u4E0E\u5FBD\u6807\u81EA\u9002\u5E94 */
4961
- pre, code, pre > code, table {
5105
+ /* 7. \u4EE3\u7801\u5757\u4E0E\u5FBD\u6807\u81EA\u9002\u5E94 */
5106
+ pre, code, pre > code {
4962
5107
  max-width: 100% !important;
4963
5108
  overflow-x: auto !important;
4964
5109
  -webkit-overflow-scrolling: touch !important;
4965
5110
  font-size: 12.5px !important;
4966
5111
  }
4967
5112
 
4968
- /* \u72B6\u6001\u5FBD\u6807\u4E0E\u836F\u4E38\u6309\u94AE\u6C38\u4E0D\u6298\u5B57 */
5113
+ /* \u72B6\u6001\u5FBD\u6807\u4E0E\u836F\u4E38\u6309\u94AE\u6C38\u4E0D\u6298\u5B57 */
4969
5114
  span[style*="border-radius: 999"],
4970
5115
  span[style*="border-radius:999"] {
4971
5116
  white-space: nowrap !important;
@@ -5181,10 +5326,82 @@ function createUpstreamBridge(ctx) {
5181
5326
  }
5182
5327
  };
5183
5328
  }
5329
+ function setupPanelAwareness(ctx) {
5330
+ if (typeof document === "undefined") return () => {
5331
+ };
5332
+ const PANEL_SELECTORS = [
5333
+ ".dshc-center-backdrop",
5334
+ // dsh-chanhub 渠道中心
5335
+ "[data-dsh-panel-host]",
5336
+ // dsh-better-sidebar(注意:常驻 DOM,需判可见)
5337
+ "[data-dsh-better-sidebar]"
5338
+ ];
5339
+ const isPanelOpen = () => PANEL_SELECTORS.some((sel) => {
5340
+ const el = document.querySelector(sel);
5341
+ if (!el) return false;
5342
+ const cs = getComputedStyle(el);
5343
+ if (cs.display === "none" || cs.visibility === "hidden") return false;
5344
+ const r = el.getBoundingClientRect();
5345
+ return r.width > 0 && r.height > 0;
5346
+ });
5347
+ const sync = () => {
5348
+ const open = isPanelOpen();
5349
+ if (document.body.classList.contains("dsh-panel-open") !== open) {
5350
+ document.body.classList.toggle("dsh-panel-open", open);
5351
+ }
5352
+ };
5353
+ sync();
5354
+ const observer = new MutationObserver(sync);
5355
+ observer.observe(document.body, { childList: true });
5356
+ const timer = setInterval(sync, 1e3);
5357
+ const dispose = () => {
5358
+ observer.disconnect();
5359
+ clearInterval(timer);
5360
+ };
5361
+ ctx?.effect?.(() => dispose);
5362
+ return dispose;
5363
+ }
5184
5364
  function setupMobileExperience(rpcCall, ctx) {
5185
5365
  if (typeof document === "undefined" || typeof window === "undefined") return;
5186
- injectMobileStyles();
5366
+ const MOBILE_FLAG_KEY = "dsh-bridge-gateway:mobile";
5367
+ const flag = new URLSearchParams(window.location.search).get("dsh-mobile");
5368
+ if (flag !== null) {
5369
+ try {
5370
+ window.localStorage.setItem(MOBILE_FLAG_KEY, flag);
5371
+ } catch {
5372
+ }
5373
+ }
5374
+ let mobileFlag = "on";
5375
+ try {
5376
+ mobileFlag = window.localStorage.getItem(MOBILE_FLAG_KEY) || "on";
5377
+ } catch {
5378
+ }
5379
+ document.documentElement.dataset.dshMobile = mobileFlag;
5380
+ const diagOn = new URLSearchParams(window.location.search).get("dsh-diag") === "1";
5381
+ if (diagOn) window.setTimeout(setupMobileDiag, 1500);
5382
+ const flagSet = new Set(String(mobileFlag).split(",").map((x) => x.trim()).filter(Boolean));
5383
+ const disabledSuspects = new Set(
5384
+ [...flagSet].filter((f) => f.startsWith("no-")).map((f) => f.slice(3))
5385
+ );
5386
+ if (disabledSuspects.has("frame")) {
5387
+ for (const k of ["frameLayout", "frameSize", "frameOverflow"]) disabledSuspects.add(k);
5388
+ }
5389
+ const withCss = !flagSet.has("off") && !flagSet.has("no-css");
5390
+ const withJs = !flagSet.has("off") && !flagSet.has("no-js");
5391
+ const withNew = withCss && !flagSet.has("no-new");
5392
+ const MOBILE_NO_OBSERVER = flagSet.has("no-observer");
5393
+ if (!withCss && !withJs) {
5394
+ if (diagOn) window.setTimeout(setupMobileDiag, 1500);
5395
+ return;
5396
+ }
5397
+ if (withCss) {
5398
+ injectMobileStyles();
5399
+ injectSuspectStyles(disabledSuspects);
5400
+ }
5401
+ if (withNew) injectNewMobileStyles();
5402
+ if (!withJs) return;
5187
5403
  const U = createUpstreamBridge(ctx);
5404
+ setupPanelAwareness(ctx);
5188
5405
  let header = document.querySelector(".dsh-mobile-app-header");
5189
5406
  let titleEl = document.querySelector(".dsh-mobile-header-title");
5190
5407
  if (!header) {
@@ -5229,17 +5446,39 @@ function setupMobileExperience(rpcCall, ctx) {
5229
5446
  header.appendChild(rightBtn);
5230
5447
  document.body.appendChild(header);
5231
5448
  }
5449
+ if (header && !header.dataset.dshVisibilityBound) {
5450
+ header.dataset.dshVisibilityBound = "1";
5451
+ header.style.display = "none";
5452
+ try {
5453
+ const mq = window.matchMedia("(max-width: 768px)");
5454
+ const syncHeaderVisibility = () => {
5455
+ header.style.display = mq.matches ? "" : "none";
5456
+ };
5457
+ syncHeaderVisibility();
5458
+ if (typeof mq.addEventListener === "function") mq.addEventListener("change", syncHeaderVisibility);
5459
+ else if (typeof mq.addListener === "function") mq.addListener(syncHeaderVisibility);
5460
+ } catch {
5461
+ }
5462
+ }
5463
+ const pickCurrentSession = (snap) => {
5464
+ const list = snap?.byId ? Object.values(snap.byId) : [];
5465
+ if (!list.length) return null;
5466
+ const retained = list.find((x) => (x?.retainedBy?.mainView ?? 0) > 0);
5467
+ if (retained) return retained;
5468
+ return list.find((x) => !x?.blank) || null;
5469
+ };
5232
5470
  const syncMobileTitle = () => {
5233
5471
  if (!titleEl) titleEl = document.querySelector(".dsh-mobile-header-title");
5234
5472
  if (!titleEl) return;
5235
5473
  const snap = ctx?.sessions?.list?.getSnapshot?.();
5236
5474
  if (!snap) return;
5237
- const cur = snap.current ? snap.byId?.[snap.current] : null;
5475
+ const cur = pickCurrentSession(snap);
5238
5476
  if (!cur || cur.blank) {
5239
5477
  titleEl.innerText = "\u65B0\u4F1A\u8BDD";
5240
5478
  } else {
5241
5479
  titleEl.innerText = cur.displayTitle || cur.title || "\u4F1A\u8BDD";
5242
5480
  }
5481
+ titleEl.title = titleEl.innerText;
5243
5482
  };
5244
5483
  syncMobileTitle();
5245
5484
  if (typeof ctx?.sessions?.list?.subscribe === "function") {
@@ -5279,9 +5518,21 @@ function setupMobileExperience(rpcCall, ctx) {
5279
5518
  }
5280
5519
  });
5281
5520
  };
5282
- const panelObserver = new MutationObserver(ensurePanelCloseButton);
5283
- panelObserver.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ["class"] });
5284
- window.addEventListener("resize", ensurePanelCloseButton);
5521
+ let panelCloseRaf = 0;
5522
+ const schedulePanelCloseButton = () => {
5523
+ if (panelCloseRaf) return;
5524
+ panelCloseRaf = window.requestAnimationFrame(() => {
5525
+ panelCloseRaf = 0;
5526
+ ensurePanelCloseButton();
5527
+ });
5528
+ };
5529
+ if (MOBILE_NO_OBSERVER) {
5530
+ document.documentElement.dataset.dshMobileObserver = "off";
5531
+ } else {
5532
+ const panelObserver = new MutationObserver(schedulePanelCloseButton);
5533
+ panelObserver.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ["class"] });
5534
+ }
5535
+ window.addEventListener("resize", schedulePanelCloseButton);
5285
5536
  const mobileTabState = { btn: null, dropdown: null };
5286
5537
  const nativeTabs = () => [...document.querySelectorAll('[role="tablist"] [role="tab"]')];
5287
5538
  const closeTabDropdown = () => {
@@ -5290,6 +5541,14 @@ function setupMobileExperience(rpcCall, ctx) {
5290
5541
  mobileTabState.dropdown = null;
5291
5542
  }
5292
5543
  };
5544
+ const positionTabDropdown = () => {
5545
+ const dd = mobileTabState.dropdown;
5546
+ const btn = mobileTabState.btn;
5547
+ if (!dd || !btn || !btn.isConnected) return;
5548
+ const r = btn.getBoundingClientRect();
5549
+ dd.style.top = Math.round(r.bottom + 6) + "px";
5550
+ dd.style.left = Math.round(r.left) + "px";
5551
+ };
5293
5552
  const renderTabDropdown = () => {
5294
5553
  closeTabDropdown();
5295
5554
  if (!mobileTabState.btn) return;
@@ -5326,8 +5585,9 @@ function setupMobileExperience(rpcCall, ctx) {
5326
5585
  };
5327
5586
  dropdown.appendChild(option);
5328
5587
  });
5329
- mobileTabState.btn.appendChild(dropdown);
5588
+ document.body.appendChild(dropdown);
5330
5589
  mobileTabState.dropdown = dropdown;
5590
+ positionTabDropdown();
5331
5591
  };
5332
5592
  const syncTabButtonLabel = () => {
5333
5593
  if (!mobileTabState.btn || !mobileTabState.btn.isConnected) return;
@@ -5390,6 +5650,7 @@ function setupMobileExperience(rpcCall, ctx) {
5390
5650
  titleExtrasObserver.observe(document.body, { childList: true, subtree: true });
5391
5651
  window.addEventListener("resize", () => {
5392
5652
  if (window.innerWidth > 768) closeTabDropdown();
5653
+ else positionTabDropdown();
5393
5654
  });
5394
5655
  document.addEventListener(
5395
5656
  "click",