dsh-mobile-access 1.1.1 → 1.1.3

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 (3) hide show
  1. package/README.md +30 -9
  2. package/client.js +116 -5
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -79,7 +79,7 @@ iPhone 打开「设置 → 无线局域网」,确认和 Mac 连的是同一个
79
79
  ### 第 2 步:在 Mac 终端执行一条命令
80
80
 
81
81
  ```bash
82
- dsh plugin --profile web add dsh-mobile-access@1.1.1 --registry=https://registry.npmjs.org/
82
+ dsh plugin --profile web add dsh-mobile-access@1.1.3 --registry=https://registry.npmjs.org/
83
83
  ```
84
84
 
85
85
  这条命令做三件事:
@@ -175,12 +175,30 @@ Safari 分享按钮 → 添加到主屏幕
175
175
  - 插件只在 iPhone / iPad 上安装「Return 换行」监听,Mac 和非 Apple 平台不会安装;
176
176
  - 左边栏只多一个「手机访问」入口,其他入口和面板保持原样。
177
177
 
178
- ### iPhone 上切换大模型时页面放大了怎么办
178
+ ### iPhone 上切换左侧栏会话为什么要两次点击
179
179
 
180
- 从 `1.1.1` 起不需要改 DSH,也不需要关掉 Safari 的缩放。插件只在
181
- iPhone / iPad 上把可编辑控件(输入框、模型搜索框、提示词编辑器)的字号
182
- 固定到 16px,并给按钮加 `touch-action: manipulation`,避免 iOS Safari 因为
183
- 字号小于 16px 自动放大页面;模型选择菜单因此不会被裁掉。Mac 不生效。
180
+ 如果你在左侧栏已经展开、能看到会话标题的情况下,点第一下没有切换、点第二下才切换,
181
+ 这是官方会话行的触摸行为:每一行是 `role="treeitem"`、`tabindex="0"` 的可聚焦元素,
182
+ iOS Safari 会把第一次触摸先用去聚焦,第二次才派发 `click`。
183
+
184
+ 从 `1.1.3` 起,插件只在 iPhone / iPad 上、在你触摸会话行的那一刻临时移除该行的
185
+ `tabindex`,400 毫秒后自动恢复;同时给会话行加 `touch-action: manipulation`,
186
+ 避免旧式双击缩放延迟。Mac 和键盘操作不受影响。展开侧栏后,点一次会话标题即可切换。
187
+
188
+ 如果左侧栏仍然是窄窄的图标条,第一下是展开侧栏——这是官方布局,不是会话行的问题;
189
+ 展开后每个会话只需单击一次。
190
+
191
+ ### iPhone 上切换大模型时页面放大、键盘自动弹出怎么办
192
+
193
+ 从 `1.1.2` 起不需要改 DSH,也不需要关掉 Safari 的缩放:
194
+
195
+ - 插件只在 iPhone / iPad 上把可编辑控件(输入框、模型搜索框、提示词编辑器)
196
+ 的字号固定到 16px,并给按钮加 `touch-action: manipulation`,避免 iOS
197
+ Safari 因为字号小于 16px 自动放大页面;
198
+ - 打开右下角模型选择时,插件不会让模型搜索框自动抢焦点,所以键盘不会自动
199
+ 弹出;需要搜索时,点一下搜索框再输入即可;
200
+ - 如果提示词键盘本来就是打开的,点模型入口时会先收起键盘;
201
+ - Mac 和非 Apple 平台不安装这些处理,行为保持原样。
184
202
 
185
203
  如果页面已经处于放大状态,先点 Safari 地址栏刷新一次,或双击页面空白处
186
204
  复位,再重新打开模型选择即可。
@@ -346,7 +364,7 @@ iPhone 软键盘没有可用的 Shift 键,因此插件在 **iOS / iPadOS** 上
346
364
  ### 推荐:npm 官方命令
347
365
 
348
366
  ```bash
349
- dsh plugin --profile web add dsh-mobile-access@1.1.1 --registry=https://registry.npmjs.org/
367
+ dsh plugin --profile web add dsh-mobile-access@1.1.3 --registry=https://registry.npmjs.org/
350
368
  ```
351
369
 
352
370
  ### 使用 `@latest`
@@ -461,7 +479,7 @@ dsh plugin --profile web remove dsh-mobile-access
461
479
  - 左侧栏入口消失;
462
480
  - `/m` 恢复为普通 SPA fallback(不再是配对页);
463
481
  - `/api/mobile-access/*` 不再注册;
464
- - iOS 剪贴板回退和 Return 换行监听被清理。
482
+ - iOS 剪贴板回退、Return 换行监听、16px 缩放守卫和模型搜索焦点守卫被清理。
465
483
 
466
484
  已签发的登录 Cookie 可能仍留在浏览器中,直到过期;这是 DSH 认证层的行为,不是插件额外写入的凭证。
467
485
 
@@ -473,7 +491,10 @@ dsh plugin --profile web remove dsh-mobile-access
473
491
  - `1.0.1` — 安装文档与 npm 发布元数据。
474
492
  - `1.0.2` — iPhone / iPad 自带键盘 Return 换行。
475
493
  - `1.0.3` — Safari 不支持改写键盘事件时的换行回退。
476
- - `1.1.0` — 回环监听自动局域网网关(macOS / Windows,HTTP + `/api/remote.mux` WebSocket);当前 npm `latest`。
494
+ - `1.1.0` — 回环监听自动局域网网关(macOS / Windows,HTTP + `/api/remote.mux` WebSocket)。
495
+ - `1.1.1` — iPhone / iPad 上模型选择页 16px 缩放守卫。
496
+ - `1.1.2` — 打开模型选择时不自动弹键盘、不整体上移。
497
+ - `1.1.3` — iPhone / iPad 上会话行第一次点击即可切换;当前 npm `latest`。
477
498
 
478
499
  ## 许可证
479
500
 
package/client.js CHANGED
@@ -83,7 +83,6 @@ function installIOSReturnNewline() {
83
83
  Object.defineProperty(event, 'shiftKey', { configurable: true, value: true });
84
84
  }
85
85
  catch {
86
- // Fall through to the explicit edit fallback.
87
86
  }
88
87
  const currentShift = event.shiftKey;
89
88
  if (currentShift !== true) {
@@ -140,7 +139,6 @@ function installClipboardFallback() {
140
139
  }
141
140
  }
142
141
  catch {
143
- // A host page that froze the descriptor wins; never throw during cleanup.
144
142
  }
145
143
  };
146
144
  }
@@ -169,8 +167,11 @@ const IOS_INPUT_ZOOM_GUARD_CSS = `@media (hover: none) and (pointer: coarse) {
169
167
  button,
170
168
  [role="button"],
171
169
  [role="menuitem"],
172
- [role="menuitemradio"] {
170
+ [role="menuitemradio"],
171
+ [data-row-key^="session:"],
172
+ [role="treeitem"] {
173
173
  touch-action: manipulation;
174
+ -webkit-user-drag: none;
174
175
  }
175
176
  }`;
176
177
  function installIOSInputZoomGuard() {
@@ -183,6 +184,106 @@ function installIOSInputZoomGuard() {
183
184
  document.head.appendChild(style);
184
185
  return () => style.remove();
185
186
  }
187
+ /**
188
+ * The official model menu can move focus into its search field while it opens.
189
+ * On iPhone that opens the software keyboard and shifts the fixed-position menu
190
+ * above the visible viewport. Suppress only programmatic search-box focus until
191
+ * the user deliberately taps that field; blur a focused composer control before
192
+ * any composer menu opens. Native taps still focus normally, and Mac is untouched.
193
+ */
194
+ /**
195
+ * Safari treats a touch on a focusable, non-button row as a focus step: the
196
+ * first tap focuses the row and only the second tap reaches its click handler.
197
+ * Official session rows are `role="treeitem"` divs with `tabIndex={0}`, so
198
+ * switching sessions on iPhone takes two taps. Remove that one attribute for
199
+ * the duration of the tap on iOS only; React restores it on the next render and
200
+ * this hook restores it after a short delay, leaving Mac and keyboard access
201
+ * untouched. `touch-action: manipulation` is also applied in the iOS stylesheet
202
+ * to avoid the legacy double-tap-zoom delay.
203
+ */
204
+ function installIOSSidebarTapGuard() {
205
+ if (!isAppleMobile())
206
+ return () => undefined;
207
+ const doc = document;
208
+ const timers = new Set();
209
+ const onTouchStart = (event) => {
210
+ const target = event.target;
211
+ if (target === null || typeof target.closest !== 'function')
212
+ return;
213
+ const row = target.closest('[data-row-key^="session:"]');
214
+ if (row === null)
215
+ return;
216
+ const tabindex = row.getAttribute('tabindex');
217
+ if (tabindex === null)
218
+ return;
219
+ row.removeAttribute('tabindex');
220
+ const timer = window.setTimeout(() => {
221
+ timers.delete(timer);
222
+ if (row.isConnected)
223
+ row.setAttribute('tabindex', tabindex);
224
+ }, 400);
225
+ timers.add(timer);
226
+ };
227
+ doc.addEventListener('touchstart', onTouchStart, true);
228
+ return () => {
229
+ doc.removeEventListener('touchstart', onTouchStart, true);
230
+ for (const timer of timers)
231
+ window.clearTimeout(timer);
232
+ timers.clear();
233
+ };
234
+ }
235
+ function installIOSModelKeyboardGuard() {
236
+ if (!isAppleMobile())
237
+ return () => undefined;
238
+ if (typeof HTMLInputElement === 'undefined')
239
+ return () => undefined;
240
+ const doc = document;
241
+ const allowed = new WeakSet();
242
+ const onPointerDown = (event) => {
243
+ const target = event.target;
244
+ if (target === null || typeof target.closest !== 'function')
245
+ return;
246
+ const search = target.closest('input[role="searchbox"]');
247
+ if (search !== null) {
248
+ allowed.add(search);
249
+ return;
250
+ }
251
+ const trigger = target.closest('button[aria-haspopup="menu"]');
252
+ if (trigger === null || trigger.closest('[data-composer-card]') === null)
253
+ return;
254
+ const active = doc.activeElement;
255
+ if (active !== null && active !== trigger && typeof active.blur === 'function') {
256
+ try {
257
+ active.blur();
258
+ }
259
+ catch {
260
+ }
261
+ }
262
+ };
263
+ const prototype = HTMLInputElement.prototype;
264
+ const originalFocus = prototype.focus;
265
+ const guardedFocus = function (options) {
266
+ if (this.getAttribute('role') === 'searchbox' && !allowed.has(this))
267
+ return;
268
+ originalFocus.call(this, options);
269
+ };
270
+ Object.defineProperty(prototype, 'focus', {
271
+ configurable: true,
272
+ writable: true,
273
+ value: guardedFocus,
274
+ });
275
+ doc.addEventListener('pointerdown', onPointerDown, true);
276
+ return () => {
277
+ doc.removeEventListener('pointerdown', onPointerDown, true);
278
+ if (prototype.focus === guardedFocus) {
279
+ Object.defineProperty(prototype, 'focus', {
280
+ configurable: true,
281
+ writable: true,
282
+ value: originalFocus,
283
+ });
284
+ }
285
+ };
286
+ }
186
287
  function useMobileAccessState() {
187
288
  const [state, setState] = React.useState(null);
188
289
  const [loading, setLoading] = React.useState(true);
@@ -312,7 +413,6 @@ function statusOf(state, loading, error, t) {
312
413
  return { label: t('statusExpired'), tone: 'warn' };
313
414
  return { label: t('statusReady'), tone: 'ok' };
314
415
  }
315
-
316
416
  "use strict";
317
417
  const NS = 'dsh-mobile-access';
318
418
  const PANEL_ID = 'mobile-access';
@@ -459,13 +559,24 @@ function factory(require) {
459
559
  catch (error) {
460
560
  ctx.logger?.warn?.(`[mobile-access] iOS input zoom guard unavailable: ${String(error)}`);
461
561
  }
562
+ try {
563
+ cleanups.push(installIOSModelKeyboardGuard());
564
+ }
565
+ catch (error) {
566
+ ctx.logger?.warn?.(`[mobile-access] iOS model keyboard guard unavailable: ${String(error)}`);
567
+ }
568
+ try {
569
+ cleanups.push(installIOSSidebarTapGuard());
570
+ }
571
+ catch (error) {
572
+ ctx.logger?.warn?.(`[mobile-access] iOS sidebar tap guard unavailable: ${String(error)}`);
573
+ }
462
574
  return () => {
463
575
  for (const cleanup of cleanups.reverse()) {
464
576
  try {
465
577
  cleanup();
466
578
  }
467
579
  catch {
468
- // Plugin cleanup must never break the host UI.
469
580
  }
470
581
  }
471
582
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-mobile-access",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
4
4
  "description": "Independent DSH bundle that pairs iPhones and iPads with the official DeepSeek Harness Web GUI over the local network.",
5
5
  "type": "module",
6
6
  "license": "MIT",