dsh-mobile-ui 0.2.3 → 0.2.5

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/client.js CHANGED
@@ -244,7 +244,13 @@ window.__ModuleLoader__.load({
244
244
  (退回“切换器被裁”),升级后跑 tests/mobile-layout-probe.py 的 subagent 段复核。 */
245
245
  " body.dsh-mobile-ui [class*=wSkVaW_crumbSeg] [class*=ZKlsPq_root]{flex:0 0 auto !important}",
246
246
  " body.dsh-mobile-ui [class*=wSkVaW_titleRow]{flex-wrap:wrap !important;row-gap:4px !important}",
247
- " body.dsh-mobile-ui [class*=wSkVaW_titleCluster]{flex:1 1 100% !important}",
247
+ " body.dsh-mobile-ui [class*=wSkVaW_titleCluster]{flex:1 1 100% !important;flex-wrap:wrap !important;row-gap:4px !important}",
248
+ /* ⚠️ titleCluster 必须允许换行(2026-09-14 补):会话有后台任务时,headerActions 里会
249
+ 多出一个 150px 的「N 个后台任务」切换器且**不收缩**(flex:0 0 auto)⇒ crumbs 被压到
250
+ 106px,子代理切换器(80px)溢出被裁 8px(实测 crumbs.right=126 < switcher.right=134)。
251
+ 允许换行后 space 不足时 headerActions 自动落到下一行,crumbs 独占整行;
252
+ 没有后台任务时(crumbs+actions 仅 182px)不触发换行,布局维持原样。
253
+ 代价:有后台任务的会话 header 106 → 136px。 */
248
254
  /* 极窄屏(≤360px)面包屑只留当前段:子代理会话页的面包屑是
249
255
  「父会话 / 子代理名 / N 个子代理」,320px 实测当前段标题被挤到 36px
250
256
  (只剩两三个字符,探针会判 ❌),隐藏父级段与分隔符后回到 96px。
@@ -252,6 +258,64 @@ window.__ModuleLoader__.load({
252
258
  " @media (max-width:360px){",
253
259
  " body.dsh-mobile-ui [class*=wSkVaW_crumbSeg] > [class*=wSkVaW_crumb]:not([class*=wSkVaW_crumbCurrent]),body.dsh-mobile-ui [class*=wSkVaW_crumbSeg] > [class*=wSkVaW_crumbSep]{display:none !important}",
254
260
  " }",
261
+ /* ── 移动端浮层不许出视口(2026-09-14,用户报「点后台任务/终端/session 日志显示不全」)──
262
+ 上游两处 popover 按**桌面宽度**做对齐,390px 手机上直接飞到屏幕外(实测):
263
+ · 后台任务菜单 [class*=QsffPG_menu]:absolute left:0 + 固定宽 336,锚在 x=261 的按钮上
264
+ ⇒ 右边界 597 ⇒ **右侧溢出 207px**,只剩最左 129px 可见;
265
+ · 「更多操作」菜单 [role=menu][class*=_list_1nxmc_](内含「下载 Session 日志」
266
+ 「在 GNOME Terminal 中打开工作目录」等项):absolute right:0 + 宽 218,锚在 x=100 的按钮上
267
+ ⇒ 左边界 **-90** ⇒ 左侧溢出 90px(用户就是在这一步看不到终端/日志项)。
268
+ 两处一律 fixed 到视口:左右各留 8px、宽度自适应、顶部落在 header 下方(第一行按钮 42px、
269
+ 第二行按钮 76px,用 CSS 变量区分),并补 max-height + overflow:auto 兜住长菜单。
270
+ ⚠️ 必须 fixed 而不是「用 right:0 相对容器」:absolute 的 left/right 相对**按钮容器**,
271
+ 而容器在 header 里的横坐标随「有无子代理/后台任务」变化 —— 候选方案 right:0 在容器靠右时
272
+ 可用、靠左时左溢出;fixed 与容器位置无关(实测两种场景都在视口内)。
273
+ ⚠️ hash 前缀(QsffPG_ / _1nxmc_)会随 dsh 升级漂移、失效是静默的(退回溢出),
274
+ 升级后跑 tests/live-probe.py 复核(已覆盖后台任务菜单)。 */
275
+ " body.dsh-mobile-ui [class*=QsffPG_menu],body.dsh-mobile-ui [role=menu][class*=_list_1nxmc_]{position:fixed !important;left:8px !important;right:8px !important;width:auto !important;min-width:0 !important;max-width:none !important;max-height:calc(100vh - 130px) !important;overflow:auto !important;top:calc(var(--dsh-mobile-popover-top,42px) + env(safe-area-inset-top,0px)) !important}",
276
+ " body.dsh-mobile-ui [role=menu][class*=_list_1nxmc_]{--dsh-mobile-popover-top:76px}",
277
+ /* ── 手机端 header 瘦身 + 正文占比(2026-09-14,用户反馈「标题栏三行有点乱 / 正文显得窄」)──
278
+ 实测 390px:header 138px(titleRow 三行)+ 输入区 272px ⇒ 正文只剩 434px(占屏 51%)。
279
+ 三行里第三行是「终端打开 / 选择打开方式 / 更多操作 / 打开右侧边栏」——前两个点了是在
280
+ **服务器上**开终端(手机上什么也看不到)、右侧边栏早被本插件隐藏(点了空白),
281
+ 只有「更多操作」(下载 Session 日志)有用。处理:
282
+ ① 整行隐藏(行空掉后 header 少 32px);
283
+ ② 「更多操作」改由**插件注入到 tabs 行空白处的按钮代理**(点击转发给上游按钮,
284
+ 上游按钮仍留在 DOM 里只是不可见,程序化 click 照常触发 React 逻辑);
285
+ ③ tabs 行右侧本来空着 246px,顺带放「折叠输入框」开关。
286
+ 隐藏后 titleRow 只剩 titleCluster 一行组(crumbs 行 + headerActions 行),header ≈ 105px。 */
287
+ /* ⚠️ headerUtilities 用「移出文档流但保留坐标」而不是 display:none:
288
+ 上游「更多操作」按钮的菜单位置由它自己的 getBoundingClientRect 算出,display:none 时
289
+ 该矩形全 0 ⇒ 菜单渲染成 0×0 根本看不见(2026-09-14 实测踩到)。
290
+ 改成 absolute + opacity:0 + pointer-events:none:不占布局高度(header 照样瘦身)、
291
+ 矩形照常可测、人也点不到(点击由 tabs 行的代理按钮转发)。 */
292
+ " body.dsh-mobile-ui [class*=wSkVaW_headerUtilities]{position:absolute !important;top:74px !important;left:40px !important;opacity:0 !important;pointer-events:none !important}",
293
+ " body.dsh-mobile-ui [class*=wSkVaW_headerCorner]{display:none !important}",
294
+ /* 抽屉入口(原右上角悬浮汉堡 top:48/right:12,42px)在 header 从 138 降到 107 后
295
+ 正好压在第二行(后台任务)与 tabs 行上 —— vision 复核截图时抓到。
296
+ 改为并入 tabs 行工具组(#dsh-mobile-tab-tools 第三个按钮,点击仍走 openDrawer()),
297
+ 原悬浮按钮恒定隐藏。 */
298
+ /* ⚠️ 原悬浮汉堡只在「有 tabs 行的会话页」隐藏 —— 首页(hero)没有 tabs 行,
299
+ 若一并隐藏就**没有任何抽屉入口**了(2026-09-14 实测踩到:首页点不开会话列表)。
300
+ :has() 判断 tabs 行是否存在;首页仍走 JS 原有的三/四态显隐逻辑。 */
301
+ " body.dsh-mobile-ui:has([class*=wSkVaW_tabs]) #dsh-mobile-menu-btn{display:none !important}",
302
+ /* 正文与 tabs 行的间距(折叠输入区后正文直接顶到 tabs 下方,实测贴得过近) */
303
+ " body.dsh-mobile-ui.dsh-mobile-composer-hidden [class*=scrollBody]{padding-bottom:calc(14px + env(safe-area-inset-bottom,0px)) !important}",
304
+ /* 折叠态的可发现性:内容末尾给一句提示(视觉模型复核时指出折叠后底部大片留白、
305
+ 用户可能不知道从哪恢复;提示跟随内容,长会话滚到底才看到,不打扰阅读) */
306
+ " body.dsh-mobile-ui.dsh-mobile-composer-hidden [class*=scrollBody]::after{content:'输入框已折叠 · 点标签行右侧 ▤ 图标展开';display:block;text-align:center;padding:14px 0 4px;font-size:11px;line-height:1.4;color:var(--dsw-alias-label-caption,rgba(255,255,255,.35))}",
307
+ /* tabs 行做成「标签靠左 / 工具靠右」 */
308
+ " body.dsh-mobile-ui [class*=wSkVaW_tabs]{align-items:center !important}",
309
+ " body.dsh-mobile-ui #dsh-mobile-tab-tools{margin-left:auto !important;display:inline-flex !important;align-items:center !important;gap:6px !important;flex:0 0 auto !important}",
310
+ " body.dsh-mobile-ui #dsh-mobile-tab-tools button{width:30px !important;height:26px !important;min-width:30px !important;display:inline-flex !important;align-items:center !important;justify-content:center !important;padding:0 !important;border:none !important;border-radius:8px !important;background:transparent !important;color:var(--dsw-alias-label-secondary,rgba(255,255,255,.7)) !important;cursor:pointer !important}",
311
+ " body.dsh-mobile-ui #dsh-mobile-tab-tools button:active{background:var(--dsw-alias-interactive-bg-hover,rgba(255,255,255,.08)) !important}",
312
+ " body.dsh-mobile-ui #dsh-mobile-tab-tools svg{width:17px !important;height:17px !important;fill:none !important;stroke:currentColor !important;stroke-width:1.8 !important;stroke-linecap:round !important;stroke-linejoin:round !important}",
313
+ /* ── 折叠输入区(阅读模式):省下 composerSeat 的 ~272px,全部让给正文 ──
314
+ 触发方式:tabs 行的键盘按钮(状态存 localStorage,刷新后保持)。
315
+ ⚠️ 提问卡片(ask_user_question)就渲染在 composerSeat 里 ⇒ 折叠状态下若检测到卡片,
316
+ JS 会强制展开(见 sync 里的 qaOpen 分支),否则用户看不到问题。 */
317
+ " body.dsh-mobile-ui.dsh-mobile-composer-hidden [class*=composerSeat],body.dsh-mobile-ui.dsh-mobile-composer-hidden [class*=composerStack],body.dsh-mobile-ui.dsh-mobile-composer-hidden [class*=uV2eYG_root]{display:none !important}",
318
+ " body.dsh-mobile-ui #dsh-mobile-tab-tools button.dsh-mobile-on{background:var(--dsw-specific-selector,rgba(255,255,255,.12)) !important;color:var(--dsw-alias-label-primary,rgba(255,255,255,.9)) !important}",
255
319
  /* 输入框聚焦反馈 */
256
320
  " body.dsh-mobile-ui textarea:focus,body.dsh-mobile-ui [class*=input]:focus-within{outline:none;box-shadow:0 0 0 2px var(--dsw-alias-brand-primary,rgba(79,124,255,.45))}",
257
321
  /* 消息流操作按钮(复制/反馈/分享)触摸目标提升。
@@ -453,6 +517,90 @@ window.__ModuleLoader__.load({
453
517
  document.body.appendChild(tabbar);
454
518
  }
455
519
 
520
+ /* ── tabs 行工具按钮(2026-09-14)──────────────────────────────────
521
+ header 第三行在手机上是死重(见 CSS 段注释:终端/打开方式点了是在服务器开终端,
522
+ 右侧边栏早被本插件隐藏),整行已用 CSS 隐藏;这里把唯一有用的「更多操作」代理过来
523
+ (点击转发给上游按钮 —— 它仍在 DOM 里只是不可见,程序化 click 照常触发 React 逻辑),
524
+ 并新增「折叠输入区」开关(阅读模式,省 ~272px 全部让给正文)。
525
+ ⚠️ 注入的节点会被 React 重渲染清掉 ⇒ 在 sync() 里保持 ensure(幂等)。
526
+ ⚠️ 提问卡片就住在 composerSeat 里 ⇒ 折叠状态下一旦检测到卡片必须强制展开,
527
+ 否则用户看不到问题(applyComposerHidden 里用 qaOpen 兜住)。 */
528
+ var COMPOSER_HIDDEN_KEY = "dsh-mobile-composer-hidden";
529
+ function composerHidden() {
530
+ try { return window.localStorage.getItem(COMPOSER_HIDDEN_KEY) === "1"; }
531
+ catch (e) { return false; }
532
+ }
533
+ function applyComposerHidden() {
534
+ try {
535
+ var hide = composerHidden() && !qaOpen;
536
+ document.body.classList.toggle("dsh-mobile-composer-hidden", !!hide);
537
+ var btn = document.getElementById("dsh-mobile-fold-composer");
538
+ if (btn) {
539
+ btn.classList.toggle("dsh-mobile-on", !!hide);
540
+ btn.setAttribute("aria-label", hide ? "显示输入框" : "隐藏输入框");
541
+ }
542
+ } catch (e) { /* 静默降级 */ }
543
+ }
544
+ function setComposerHidden(on) {
545
+ try { window.localStorage.setItem(COMPOSER_HIDDEN_KEY, on ? "1" : "0"); } catch (e) { /* 隐私模式 */ }
546
+ applyComposerHidden();
547
+ }
548
+ function ensureTabTools() {
549
+ try {
550
+ var tabs = document.querySelector("[class*=wSkVaW_tabs]");
551
+ if (!tabs) return;
552
+ if (!document.getElementById("dsh-mobile-tab-tools")) {
553
+ var box = document.createElement("div");
554
+ box.id = "dsh-mobile-tab-tools";
555
+ /* 「更多操作」代理:转发给上游按钮(含「下载 Session 日志」) */
556
+ var more = document.createElement("button");
557
+ more.type = "button";
558
+ more.id = "dsh-mobile-more-proxy";
559
+ more.setAttribute("aria-label", "更多操作");
560
+ more.innerHTML = "<svg viewBox='0 0 24 24'><circle cx='5' cy='12' r='1.7'/><circle cx='12' cy='12' r='1.7'/><circle cx='19' cy='12' r='1.7'/></svg>";
561
+ more.addEventListener("click", function (e) {
562
+ try {
563
+ e.preventDefault();
564
+ e.stopPropagation();
565
+ var up = document.querySelector("button[class*=nL4_yW_moreButton]");
566
+ if (up) up.click();
567
+ } catch (err) { /* 静默 */ }
568
+ });
569
+ /* 折叠/展开输入区 */
570
+ var fold = document.createElement("button");
571
+ fold.type = "button";
572
+ fold.id = "dsh-mobile-fold-composer";
573
+ fold.setAttribute("aria-label", "隐藏输入框");
574
+ fold.innerHTML = "<svg viewBox='0 0 24 24'><rect x='2.5' y='6' width='19' height='12' rx='2.5'/><path d='M6.5 10h11M6.5 14h6'/></svg>";
575
+ fold.addEventListener("click", function (e) {
576
+ try {
577
+ e.preventDefault();
578
+ e.stopPropagation();
579
+ setComposerHidden(!composerHidden());
580
+ } catch (err) { /* 静默 */ }
581
+ });
582
+ /* 抽屉入口(取代右上角悬浮汉堡,见 CSS 段注释) */
583
+ var menuBtn = document.createElement("button");
584
+ menuBtn.type = "button";
585
+ menuBtn.id = "dsh-mobile-tab-menu";
586
+ menuBtn.setAttribute("aria-label", "菜单");
587
+ menuBtn.innerHTML = "<svg viewBox='0 0 24 24'><path d='M4 7h16M4 12h16M4 17h10'/></svg>";
588
+ menuBtn.addEventListener("click", function (e) {
589
+ try {
590
+ e.preventDefault();
591
+ e.stopPropagation();
592
+ openDrawer();
593
+ } catch (err) { /* 静默 */ }
594
+ });
595
+ box.appendChild(more);
596
+ box.appendChild(fold);
597
+ box.appendChild(menuBtn);
598
+ tabs.appendChild(box);
599
+ }
600
+ applyComposerHidden();
601
+ } catch (e) { /* 静默降级 */ }
602
+ }
603
+
456
604
  function setTabActive(key) {
457
605
  try {
458
606
  if (!tabbar) return;
@@ -540,10 +688,21 @@ window.__ModuleLoader__.load({
540
688
  /* 抽屉态下 sidebar 保持可见,否则隐藏 */
541
689
  side.style.display = drawerOpen ? "" : "none";
542
690
  ensureChrome();
543
- /* 菜单按钮三态判定:抽屉 / 设置面板 / 提问卡片浮层打开时都隐藏。
691
+ /* 菜单按钮四态判定:抽屉 / 设置面板 / 提问卡片浮层 / **顶部 popover 菜单**
692
+ 打开时都隐藏。
544
693
  (旧写法只看 drawerOpen,会把设置面板打开时隐藏的菜单在下次 sync 又显示回来;
545
- 提问卡片时则会浮在卡片右上角遮住问题标题) */
546
- if (tabbar) tabbar.style.display = (drawerOpen || settingsOpen || qaOpen) ? "none" : "";
694
+ 提问卡片时则会浮在卡片右上角遮住问题标题;
695
+ 2026-09-14 补第 4 态:后台任务/「更多操作」菜单被本插件的浮层规则固定到
696
+ top:42 全宽后,右上角的汉堡按钮正好压在菜单首行右侧 —— vision 复核截图时发现
697
+ 「第一行的耗时被按钮遮住」,故菜单开着时也隐藏它) */
698
+ var popOpen = !!(document.querySelector(
699
+ "[class*=QsffPG_menu],[role=menu][class*=_list_1nxmc_]"));
700
+ if (tabbar) {
701
+ tabbar.style.display = (drawerOpen || settingsOpen || qaOpen || popOpen)
702
+ ? "none" : "";
703
+ }
704
+ /* tabs 行工具按钮(更多操作代理 + 折叠输入区)—— React 重渲染后重建 */
705
+ ensureTabTools();
547
706
  } else {
548
707
  document.body.classList.remove("dsh-mobile-ui");
549
708
  qaOpen = false;
@@ -567,6 +726,8 @@ window.__ModuleLoader__.load({
567
726
  mq.addListener(sync);
568
727
  }
569
728
 
729
+ /* 折叠状态在进入页面时就生效(localStorage 恢复),不等 React 渲染完 */
730
+ applyComposerHidden();
570
731
  /* 首次同步 + 等待 React 渲染出 frame 后重试 */
571
732
  sync();
572
733
  var tries = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-mobile-ui",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "Mobile UI for the DeepSeek Harness Web GUI: responsive layout, overlay session drawer, 44px touch targets, safe-area support, reading enhancements (≤768px, zero desktop impact)",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -43,6 +43,15 @@ GEOM = """() => {
43
43
  crumbsText: (crumbs.innerText || '').replace(/\\n/g, ' ').slice(0, 60),
44
44
  };
45
45
  }"""
46
+ POPOVER = """(sel) => { const m = document.querySelector(sel); if (!m) return null;
47
+ const r = m.getBoundingClientRect();
48
+ const items = m.querySelectorAll('[role=menuitem],[class*=row],[class*=item_]').length;
49
+ const mid = document.elementFromPoint(r.right - 40, r.top + 16);
50
+ return { left: Math.round(r.left), right: Math.round(r.right), h: Math.round(r.height), items: items,
51
+ ok: r.left >= -0.5 && r.right <= window.innerWidth + 0.5,
52
+ topRightHit: mid ? String(mid.className || '').split(' ').pop().slice(0, 22) : null,
53
+ hamburger: (document.getElementById('dsh-mobile-menu-btn') || {}).style ? document.getElementById('dsh-mobile-menu-btn').style.display : '?' }; }"""
54
+
46
55
  DRAWER = """() => { const s = document.querySelector('[class*=sidebarCol]');
47
56
  return s ? (getComputedStyle(s).display + ':' + Math.round(s.getBoundingClientRect().width)) : 'none'; }"""
48
57
 
@@ -139,6 +148,72 @@ def run(args) -> int:
139
148
  if seq != [1, 0, 1, 0]:
140
149
  fails.append(f"点击无法临时开合:{seq}(期望 [1,0,1,0])")
141
150
 
151
+ # ③b 顶部 popover 不许出视口(2026-09-14 用户报「点后台任务/终端/session 日志显示不全」:
152
+ # 上游按桌面宽度做左/右对齐 —— 后台任务菜单 right 溢出 207px、「更多操作」菜单 left 溢出 90px)
153
+ for name, trig_sel, menu_sel in (
154
+ ("后台任务菜单", "button[class*=QsffPG_trigger]", "[class*=QsffPG_menu]"),
155
+ # 「更多操作」的入口是插件注入的**代理按钮**:上游按钮已被移出可点层
156
+ # (position:absolute; opacity:0; pointer-events:none —— 否则它会算出 0×0 的菜单,
157
+ # 见 client.js 里 headerUtilities 段注释),点它才算走用户的真实路径
158
+ ("更多操作菜单", "#dsh-mobile-more-proxy", "[role=menu][class*=_list_1nxmc_]"),
159
+ ):
160
+ pos = page.evaluate(
161
+ """(s) => { const b = document.querySelector(s); if (!b) return null;
162
+ const r = b.getBoundingClientRect(); return [r.x + r.width / 2, r.y + r.height / 2]; }""", trig_sel)
163
+ if not pos:
164
+ print(f" [{name}] 入口不存在,跳过")
165
+ continue
166
+ page.touchscreen.tap(pos[0], pos[1])
167
+ page.wait_for_timeout(1500)
168
+ g = page.evaluate(POPOVER, menu_sel)
169
+ print(f" [{name}] {json.dumps(g, ensure_ascii=False)}")
170
+ if not g or not g.get("ok"):
171
+ fails.append(f"{name}跑出视口({g})—— 上游对齐按桌面宽度算,插件的浮层约束失效?")
172
+ elif g.get("items", 0) < 1:
173
+ fails.append(f"{name}打开后没有任何条目({g})")
174
+ page.keyboard.press("Escape")
175
+ page.wait_for_timeout(800)
176
+
177
+ # ③c header 瘦身 + 折叠输入区(2026-09-14:用户反馈「标题栏三行有点乱 / 正文显得窄」)
178
+ lay = page.evaluate("""() => {
179
+ const hdr = document.querySelector('header'); const hb = hdr ? hdr.getBoundingClientRect().bottom : 0;
180
+ const seat = document.querySelector('[class*=composerSeat]');
181
+ const seatVis = seat && getComputedStyle(seat).display !== 'none' && seat.getBoundingClientRect().height > 0;
182
+ const bottom = seatVis ? seat.getBoundingClientRect().top : window.innerHeight;
183
+ const tools = [...document.querySelectorAll('#dsh-mobile-tab-tools button')].map(b => b.id);
184
+ const hamburger = document.getElementById('dsh-mobile-menu-btn');
185
+ return { headerH: Math.round(hb), readPct: Math.round((bottom - hb) / window.innerHeight * 100),
186
+ tools: tools, hamburgerHidden: hamburger ? getComputedStyle(hamburger).display === 'none' : null }; }""")
187
+ print(f" [布局] header={lay['headerH']}px 正文占比={lay['readPct']}% 工具={lay['tools']}")
188
+ # 行数回涨(三行 header ⇒ 138px)或工具组缺失都算回归
189
+ if lay["headerH"] > 120:
190
+ fails.append(f"header 又变高了({lay['headerH']}px > 120px,是不是行数回涨了?)")
191
+ if len(lay["tools"]) < 3:
192
+ fails.append(f"tabs 行工具组不全:{lay['tools']}(应含 more-proxy / fold-composer / tab-menu)")
193
+ if lay.get("hamburgerHidden") is not True:
194
+ fails.append("会话页的原悬浮汉堡没隐藏(会压住第二行/tabs 行)")
195
+ # 折叠切换:正文占比应显著上升,且按钮状态跟着翻
196
+ fold = page.evaluate("""() => { const b = document.getElementById('dsh-mobile-fold-composer'); if (!b) return null;
197
+ const r = b.getBoundingClientRect(); return [r.x + r.width / 2, r.y + r.height / 2]; }""")
198
+ if fold:
199
+ page.touchscreen.tap(fold[0], fold[1])
200
+ page.wait_for_timeout(1200)
201
+ folded = page.evaluate("""() => {
202
+ const seat = document.querySelector('[class*=composerSeat]');
203
+ const seatVis = seat && getComputedStyle(seat).display !== 'none' && seat.getBoundingClientRect().height > 0;
204
+ const hb = document.querySelector('header').getBoundingClientRect().bottom;
205
+ const bottom = seatVis ? seat.getBoundingClientRect().top : window.innerHeight;
206
+ return { hidden: document.body.classList.contains('dsh-mobile-composer-hidden'),
207
+ readPct: Math.round((bottom - hb) / window.innerHeight * 100) }; }""")
208
+ print(f" [折叠] hidden={folded['hidden']} 正文占比={folded['readPct']}%")
209
+ if not folded["hidden"] or folded["readPct"] < lay["readPct"] + 15:
210
+ fails.append(f"折叠输入区没生效({lay['readPct']}% → {folded['readPct']}%)")
211
+ page.touchscreen.tap(fold[0], fold[1]) # 复原,别影响后续断言
212
+ page.wait_for_timeout(1000)
213
+ back = page.evaluate("() => document.body.classList.contains('dsh-mobile-composer-hidden')")
214
+ if back:
215
+ fails.append("再次点击没能恢复输入区")
216
+
142
217
  # ④ 菜单行可进入子代理(面包屑应变成两段)
143
218
  page.touchscreen.tap(sw["x"], sw["y"])
144
219
  page.wait_for_timeout(800)