dsh-mobile-ui 0.2.6 → 0.2.8

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
@@ -290,12 +290,19 @@ window.__ModuleLoader__.load({
290
290
  上游按钮仍留在 DOM 里只是不可见,程序化 click 照常触发 React 逻辑);
291
291
  ③ tabs 行右侧本来空着 246px,顺带放「折叠输入框」开关。
292
292
  隐藏后 titleRow 只剩 titleCluster 一行组(crumbs 行 + headerActions 行),header ≈ 105px。 */
293
- /* ⚠️ headerUtilities 用「移出文档流但保留坐标」而不是 display:none:
293
+ /* ⚠️⚠️ headerUtilities 用「移出文档流但保留坐标」而不是 display:none:
294
294
  上游「更多操作」按钮的菜单位置由它自己的 getBoundingClientRect 算出,display:none 时
295
295
  该矩形全 0 ⇒ 菜单渲染成 0×0 根本看不见(2026-09-14 实测踩到)。
296
- 改成 absolute + opacity:0 + pointer-events:none:不占布局高度(header 照样瘦身)、
297
- 矩形照常可测、人也点不到(点击由 tabs 行的代理按钮转发)。 */
298
- " body.dsh-mobile-ui [class*=wSkVaW_headerUtilities]{position:absolute !important;top:74px !important;left:40px !important;opacity:0 !important;pointer-events:none !important}",
296
+ ⚠️⚠️ 更不能再用 opacity:0(0.2.5–0.2.7 的写法,错):**opacity 是「组不透明度」,祖先
297
+ 为 0 会让整棵子树(含弹层)一起透明** —— 实测「更多操作」菜单就挂在这棵子树里
298
+ (祖先链:menu → span._root_1nxmc_ → div → headerUtilities),于是菜单**弹出来了、几何也正确**
299
+ (实测 rect=[8,122,396,42]、含「下载 Session 日志」),却**完全看不见**,还因继承
300
+ pointer-events:none 而**点不动** ⇒ 用户第三次反馈「点三个点还是没反应」的真根因
301
+ (2026-09-15 定案;前两轮修的把手死区/竞态都是真问题,但不是这一条)。
302
+ ✅ 正解 = visibility:hidden:同样不占布局、getBoundingClientRect 照常可测,
303
+ 且 **visibility 可被后代用 visible 覆盖**(opacity 不行)⇒ 弹层照常显示、照常可点。 */
304
+ " body.dsh-mobile-ui [class*=wSkVaW_headerUtilities]{position:absolute !important;top:74px !important;left:40px !important;visibility:hidden !important}",
305
+ " body.dsh-mobile-ui [class*=wSkVaW_headerUtilities] [role=menu],body.dsh-mobile-ui [class*=wSkVaW_headerUtilities] [class*=_list_1nxmc_],body.dsh-mobile-ui [class*=wSkVaW_headerUtilities] [class*=_portal_],body.dsh-mobile-ui [class*=wSkVaW_headerUtilities] [class*=QsffPG_menu]{visibility:visible !important;pointer-events:auto !important}",
299
306
  " body.dsh-mobile-ui [class*=wSkVaW_headerCorner]{display:none !important}",
300
307
  /* 抽屉入口(原右上角悬浮汉堡 top:48/right:12,42px)在 header 从 138 降到 107 后
301
308
  正好压在第二行(后台任务)与 tabs 行上 —— vision 复核截图时抓到。
@@ -316,6 +323,19 @@ window.__ModuleLoader__.load({
316
323
  " body.dsh-mobile-ui #dsh-mobile-tab-tools button{width:36px !important;height:34px !important;min-width:36px !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}",
317
324
  " body.dsh-mobile-ui #dsh-mobile-tab-tools button:active{background:var(--dsw-alias-interactive-bg-hover,rgba(255,255,255,.08)) !important}",
318
325
  " body.dsh-mobile-ui #dsh-mobile-tab-tools svg{width:18px !important;height:18px !important;fill:none !important;stroke:currentColor !important;stroke-width:1.8 !important;stroke-linecap:round !important;stroke-linejoin:round !important}",
326
+ /* ⚠️⚠️ 隐形死区:右侧栏 resize 把手的残留(2026-09-14 定案,用户报「三个点没反应」的真根因)
327
+ 上游 pI_x6G_handle 是 rightbarCol 的拖拽热区:position:absolute;top:0;bottom:0;
328
+ width:8px;margin-left:-4px;z-index:11;pointer-events:auto,left 是**固定像素**(实测 276px)。
329
+ 折叠右侧栏时 rightbarCol 收缩成 height:0(实测 rect=[0,915,412,0]),
330
+ **但把手不跟着消失**,仍以 8px 宽、**贯穿整个视口高度**(0→915)钉在 x=276..284。
331
+ 后果:那一整列 8px 宽的 tap 全被吃掉(手机上没有 col-resize,用户只会觉得「点了没反应」),
332
+ 正文区的按钮/输入框也一并被挡。实测(同一份 client.js,仅视口不同):
333
+ 412px:⋯ 按钮 260..296 的中心 278 落在 276..284 内 ⇒ elementFromPoint=handle,tap 开合命中 0/5
334
+ 390/435/460px:按钮恰好错开这条带 ⇒ 5/5(所以这个 bug「挑宽度」,极难靠换机型复现)
335
+ 移动端不需要 col-resize ⇒ 直接移除;同时给工具组抬 z-index 做第二道保险
336
+ (把手 z-index:11,工具组 z-index:30,即便将来又冒出别的覆盖层也压不住按钮)。 */
337
+ " body.dsh-mobile-ui [class*=pI_x6G_handle]{display:none !important}",
338
+ " body.dsh-mobile-ui #dsh-mobile-tab-tools{position:relative !important;z-index:30 !important}",
319
339
  /* ── 折叠输入区(阅读模式):省下 composerSeat 的 ~272px,全部让给正文 ──
320
340
  触发方式:tabs 行的键盘按钮(状态存 localStorage,刷新后保持)。
321
341
  ⚠️ 提问卡片(ask_user_question)就渲染在 composerSeat 里 ⇒ 折叠状态下若检测到卡片,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-mobile-ui",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
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",
@@ -47,9 +47,20 @@ POPOVER = """(sel) => { const m = document.querySelector(sel); if (!m) return nu
47
47
  const r = m.getBoundingClientRect();
48
48
  const items = m.querySelectorAll('[role=menuitem],[class*=row],[class*=item_]').length;
49
49
  const mid = document.elementFromPoint(r.right - 40, r.top + 16);
50
+ // ⚠️ 几何正常 ≠ 人能看见/能点:必须同时查「中心命中自身」+「祖先链上的有效可见性」
51
+ // (2026-09-15 教训:菜单 rect 完全正确、条目也在,但祖先 opacity:0 把它整棵子树变透明,
52
+ // 只断言 left/right/items 的旧版探针一路放行 ⇒ 用户连报三轮「没反应」)
53
+ const hit = document.elementFromPoint(r.left + r.width / 2, r.top + Math.min(20, r.height / 2));
54
+ // ⚠️ visibility 只看**自身计算值**:祖先 visibility:hidden 会被后代的 visible 覆盖(这正是本插件的修法),
55
+ // 扫祖先链会把「已经修好」判成 hidden(2026-09-15 自测踩到)。opacity 相反 —— 组不透明度**必须逐级相乘**。
56
+ const vis = getComputedStyle(m).visibility; let op = 1, n = m;
57
+ while (n && n !== document.body) { op *= parseFloat(getComputedStyle(n).opacity || '1'); n = n.parentElement; }
50
58
  return { left: Math.round(r.left), right: Math.round(r.right), h: Math.round(r.height), items: items,
51
59
  ok: r.left >= -0.5 && r.right <= window.innerWidth + 0.5,
52
60
  topRightHit: mid ? String(mid.className || '').split(' ').pop().slice(0, 22) : null,
61
+ visibility: vis, effectiveOpacity: +op.toFixed(2), pointerEvents: getComputedStyle(m).pointerEvents,
62
+ hitSelf: !!(hit && (hit === m || m.contains(hit))),
63
+ hitTop: hit ? String(hit.id || hit.className || hit.tagName).split(' ').filter(Boolean).pop().slice(0, 22) : null,
53
64
  hamburger: (document.getElementById('dsh-mobile-menu-btn') || {}).style ? document.getElementById('dsh-mobile-menu-btn').style.display : '?' }; }"""
54
65
 
55
66
  DRAWER = """() => { const s = document.querySelector('[class*=sidebarCol]');
@@ -171,6 +182,16 @@ def run(args) -> int:
171
182
  fails.append(f"{name}跑出视口({g})—— 上游对齐按桌面宽度算,插件的浮层约束失效?")
172
183
  elif g.get("items", 0) < 1:
173
184
  fails.append(f"{name}打开后没有任何条目({g})")
185
+ # ⚠️⚠️ 光有几何和条目不算数:必须真能看见、真能点到
186
+ # (2026-09-15 血泪:菜单 rect 全对、条目也在,但祖先 wSkVaW_headerUtilities 的
187
+ # opacity:0 把整棵子树变透明 + pointer-events:none 被继承 ⇒ 用户「点了没反应」,
188
+ # 而只断言 left/right/items 的旧版探针一路绿灯)
189
+ elif g.get("visibility") == "hidden" or g.get("effectiveOpacity", 1) < 0.9 \
190
+ or g.get("pointerEvents") == "none" or not g.get("hitSelf"):
191
+ fails.append(
192
+ f"{name}看得见却点不到(visibility={g.get('visibility')} "
193
+ f"有效opacity={g.get('effectiveOpacity')} pointer-events={g.get('pointerEvents')} "
194
+ f"中心命中={g.get('hitTop')})—— 祖先 opacity:0 的组透明 / pointer-events 继承泄漏?")
174
195
  page.keyboard.press("Escape")
175
196
  page.wait_for_timeout(800)
176
197
 
@@ -235,6 +256,58 @@ def run(args) -> int:
235
256
  if size and (size[0] < 32 or size[1] < 30):
236
257
  fails.append(f"工具按钮触摸目标过小:{size}")
237
258
 
259
+ # ③e ⚠️⚠️ 宽度相关的「隐形死区」回归(2026-09-14 用户第三次报「三个点没反应」的真根因)
260
+ # 上游右侧栏 resize 把手 pI_x6G_handle 是 absolute;top:0;bottom:0;width:8px;
261
+ # z-index:11;pointer-events:auto —— 右侧栏折叠后 rightbarCol 缩成 height:0,
262
+ # 但把手**不跟着消失**,仍以 8px 宽、贯穿整个视口高度钉在固定 x(实测 276..284)。
263
+ # 工具组是右对齐的(⋯ 中心 = 视口宽 - 134)⇒ 它只在 **410–417px** 这段视口里
264
+ # 正好压住 ⋯ 的中心:「390px 测着全好、用户手机却点不动」就是这么来的。
265
+ # ⇒ 断言**必须扫一批宽度**,单宽度探针天然漏检;同时确认把手已不可命中
266
+ # (插件 0.2.7 起 display:none,另给工具组 position:relative;z-index:30 做第二道保险)。
267
+ HITTEST = """() => {
268
+ const h = document.querySelector('[class*=pI_x6G_handle]');
269
+ const hs = h ? getComputedStyle(h) : null;
270
+ return { handle: h ? { display: hs.display, pe: hs.pointerEvents } : null,
271
+ buttons: [...document.querySelectorAll('#dsh-mobile-tab-tools button')].map(b => {
272
+ const r = b.getBoundingClientRect();
273
+ const t = document.elementFromPoint(r.x + r.width / 2, r.y + r.height / 2);
274
+ return { id: b.id, self: !!(t && (t === b || b.contains(t))),
275
+ top: t ? (t.id || String(t.className || '').split(' ').filter(Boolean).pop() || t.tagName) : null }; }) }; }"""
276
+ dead, covered, probe412 = [], [], None
277
+ for vw in (320, 360, 375, 390, 400, 410, 412, 415, 418, 428, 460, 768):
278
+ page.set_viewport_size({"width": vw, "height": h})
279
+ page.wait_for_timeout(320)
280
+ r = page.evaluate(HITTEST)
281
+ if r["handle"] and r["handle"]["display"] != "none":
282
+ dead.append(vw)
283
+ miss = [f"{x['id']}←{x['top']}" for x in r["buttons"] if not x["self"]]
284
+ if miss:
285
+ covered.append(f"{vw}px:{','.join(miss)}")
286
+ if vw == 412: # 用户实测踩中的那一段宽度,做端到端点击验证
287
+ c412 = page.evaluate("""() => { const b = document.getElementById('dsh-mobile-more-proxy');
288
+ if (!b) return null; const r = b.getBoundingClientRect();
289
+ return [r.x + r.width / 2, r.y + r.height / 2]; }""")
290
+ seq2 = []
291
+ if c412:
292
+ for _ in range(2):
293
+ page.touchscreen.tap(c412[0], c412[1])
294
+ page.wait_for_timeout(900)
295
+ seq2.append(page.evaluate(menus_js))
296
+ if seq2[1]: # 别把菜单留着干扰后续断言
297
+ page.touchscreen.tap(c412[0], c412[1])
298
+ page.wait_for_timeout(700)
299
+ probe412 = seq2
300
+ page.set_viewport_size({"width": w, "height": h})
301
+ page.wait_for_timeout(500)
302
+ print(f" [宽度扫描 12 档] 把手残留={dead or '无'} 按钮被覆盖={covered or '无'} 412px ⋯开合={probe412}(期望 [1,0])")
303
+ if dead:
304
+ fails.append(f"右侧栏拖拽把手又在这些宽度残留成隐形死区:{dead}px"
305
+ f"(pI_x6G_handle 覆盖整列 ⇒ 该列 tap 全被吃掉)")
306
+ if covered:
307
+ fails.append(f"tabs 行工具按钮被别的层盖住(点不到):{covered}")
308
+ if probe412 is not None and probe412 != [1, 0]:
309
+ fails.append(f"412px(用户手机宽度)下 ⋯ 按钮仍不能开合:{probe412}")
310
+
238
311
  # ④ 菜单行可进入子代理(面包屑应变成两段)
239
312
  page.touchscreen.tap(sw["x"], sw["y"])
240
313
  page.wait_for_timeout(800)