dsh-mobile-flow 0.7.3 → 0.8.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/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  Two mobile fixes for the [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (dsh) Web UI:
6
6
 
7
7
  1. **In-flow composer** — on narrow screens (≤720px) the input bar and AI confirmation cards scroll with the page instead of pinning to the viewport floor, so swiping up gives the message transcript the full screen back.
8
- 2. **Native input takeover** — on narrow screens the draft surface becomes a native `<textarea>`, sidestepping the Lexical contenteditable IME flow that swallows text on Android keyboards (voice input most of all).
8
+ 2. **Native input takeover** — on narrow screens **and on any touch device** (a large tablet included) the draft surface becomes a native `<textarea>`, sidestepping the Lexical contenteditable IME flow that swallows text on Android/HarmonyOS keyboards (voice input most of all).
9
9
 
10
10
  A client-side overlay plus one slot component. No product source is modified; removing the plugin restores the stock behavior exactly.
11
11
 
@@ -21,18 +21,19 @@ The stock composer's text surface is a **Lexical contenteditable** (`createEdito
21
21
 
22
22
  ## What it does
23
23
 
24
- On viewports ≤720px (the same breakpoint the official question card uses):
24
+ On viewports ≤720px (the same breakpoint the official question card uses) the first four are layout fixes; the rest key off **"narrow screen or touch device"** — the IME problem has nothing to do with screen size, and a HarmonyOS tablet loses typed text at 1280px just the same (user report 2026-10-05: MatePad + Doubao voice input):
25
25
 
26
26
  1. **The composer seat joins the document flow** — input bar and confirmation cards now live at the end of the transcript. Swipe up and they scroll out of view; the transcript becomes full-screen.
27
27
  2. **Short conversations still dock to the bottom** — when the transcript is shorter than one screen, the message area stretches so the composer stays flush with the viewport floor, visually identical to the stock behavior. Long conversations get the full-screen treatment.
28
28
  3. **Floating controls re-anchor** — the back-to-bottom button and turn navigator no longer reserve height for the sticky seat and sit close to the viewport floor again.
29
29
  4. **Slim edges** — the content-layer paddings that keep the transcript, header, input card, and confirmation cards well off the screen edges (16–32px per side) shrink to a slim 4px side inset (8px top), reclaiming most of the wasted width without the cramped feel of full-bleed. The page shell itself never adds whitespace; the gaps come entirely from these paddings.
30
- 5. **No auto-focus on session switch** — the stock UI returns focus to the input box on every mount / session switch (a desktop convenience), which pops the on-screen keyboard over half the screen on phones. In narrow viewports the programmatic focus is swallowed; tapping the input box still focuses it normally.
31
- 6. **Workspace row actions always visible** — the trailing buttons on workspace and session rows (the ⋯ menu with rename/delete and the + for a new session in that workspace) surface on hover only; touch has no hover, so narrow viewports show them whenever the sidebar list is rendered.
32
- 7. **Native input takeover** — the draft surface is drawn by a native textarea whose metrics match the stock box exactly (geometry is measured from the live composer every frame the card relayouts). Enter still performs the official send gesture (including `/` menu arbitration and the busy-Enter policy), file pastes are forwarded to the official attachment intake, and locked states follow the product's own editability gate.
30
+ 5. **No auto-focus on session switch** — the stock UI returns focus to the input box on every mount / session switch (a desktop convenience), which pops the on-screen keyboard over half the screen on phones (and is just as unwelcome on a tablet). Narrow viewports and touch devices swallow that programmatic focus; tapping the input box still focuses it normally.
31
+ 6. **Workspace row actions always visible** — the trailing buttons on workspace and session rows (the ⋯ menu with rename/delete and the + for a new session in that workspace) surface on hover only; touch has no hover, so narrow viewports and touch devices show them whenever the sidebar list is rendered.
32
+ 7. **Native input takeover** — narrow screens and touch devices are taken over by default (a mouse-driven desktop is left alone); the draft surface is drawn by a native textarea whose metrics match the stock box exactly (geometry is measured from the live composer every frame the card relayouts). Enter still performs the official send gesture (including `/` menu arbitration and the busy-Enter policy), file pastes are forwarded to the official attachment intake, and locked states follow the product's own editability gate.
33
33
  8. **The send button follows the field** — as soon as the field holds text, the send button reads as enabled (a live blue circle instead of the disabled grey one) and one tap sends it (queue/steer while a turn streams). While typing that state is painted by pure CSS with **zero DOM writes**, so the "no DOM movement while typing" invariant stands.
34
+ 9. **The switch is not a phone control** — the tool-row 「输入法✓/✗」 chip renders on every viewport, so a large tablet always has a one-tap say in this; it is also the fallback for a device the default gate fails to recognise as touch.
34
35
 
35
- Desktop (wide viewports) is completely unaffected.
36
+ **A mouse-driven desktop is completely unaffected** (a touch device is taken over however wide it is — that is the point of this change).
36
37
 
37
38
  ## How it works
38
39
 
@@ -45,7 +46,7 @@ The plugin ships a browser half (`exports["./client"]`, declared via `dsh.client
45
46
  ## Requirements
46
47
 
47
48
  - DeepSeek Harness Web profile (`dsh web`), any recent 0.1.x release
48
- - Selectors and slot contracts verified against 0.1.2-rc.1 and 0.1.5-rc.1; they target product slot contracts that are stable within a version line but may need small updates after a major product revamp
49
+ - Selectors and slot contracts verified against 0.1.2-rc.1, 0.1.5-rc.1 and 0.2.0-rc.2 (2026-10-04, by the live probes below plus a static audit of the shipped client bundles); they target product slot contracts that are stable within a version line but may need small updates after a major product revamp
49
50
 
50
51
  ## Install
51
52
 
@@ -65,16 +66,18 @@ Restart `dsh web`, then refresh the browser page.
65
66
 
66
67
  ## Overrides (optional)
67
68
 
68
- The native takeover is on by default **on narrow viewports only** (≤720px). To override it — for instance to exercise it on a desktop browser:
69
+ The native takeover is on by default **on narrow viewports and on any touch device** — screen size is not the criterion. To override it — for instance to exercise it on a desktop browser:
69
70
 
70
71
  ```js
71
72
  // Browser console, same origin, persists
72
73
  localStorage.setItem("dsh-mobile-flow:input", "off"); // never take over
73
- localStorage.setItem("dsh-mobile-flow:input", "on"); // narrow viewports only (default)
74
- localStorage.setItem("dsh-mobile-flow:input", "force"); // take over on any viewport
75
- localStorage.removeItem("dsh-mobile-flow:input"); // back to default
74
+ localStorage.setItem("dsh-mobile-flow:input", "on"); // always take over (any viewport, desktop included)
75
+ localStorage.setItem("dsh-mobile-flow:input", "force"); // same as "on" (what the URL flag =1 writes)
76
+ localStorage.removeItem("dsh-mobile-flow:input"); // back to default: narrow or touch
76
77
  ```
77
78
 
79
+ The easier route is the 「输入法✓/✗」 chip in the tool row: it reports **what this device is actually doing** (not the stored preference), and one tap flips between "always" and "never".
80
+
78
81
  One-shot override: `?dsh-mobile-input=1` (force) / `=0` (off).
79
82
 
80
83
  Diagnostics have their own persisted switch (the tool-row 诊断 button writes it; no URL typing needed):
@@ -95,20 +98,21 @@ localStorage.removeItem("dsh-mobile-flow:growth"); // back to default
95
98
 
96
99
  ## Verify
97
100
 
98
- Open a session on a phone (or a desktop DevTools window narrowed to ≤720px):
101
+ Open a session on a phone (or a desktop DevTools window narrowed to ≤720px) — **a tablet works the same at any width**:
99
102
 
100
103
  - Swipe up a few screens: the input bar and confirmation cards scroll away with the messages.
101
104
  - The draft surface should be a native textarea (the page carries `[data-mobile-input]`). Type with an IME or voice input: text must not be cleared.
102
105
  - Send with the send button or Enter (same gesture as the stock composer).
103
106
  - Send-button state: **with text in the field the button is live** (the grey→live flip follows the field's content); one tap sends one message, first tap included.
104
- - The tool row shows **two chips**: 「输入法✓」(the escape hatch) and 「诊断」(open the test bench).
107
+ - The tool row shows **one chip**, 「输入法✓」(the escape hatch); a 600 ms long press on it calls out the 「诊断」 chip.
108
+ - On 0.2.0 the first load of a fresh browser profile shows the product's 「预览版说明」 preview notice; acknowledge it (继续) before verifying — it takes focus and paints over the composer, so nothing under it can be typed into.
105
109
  - The v0.6.0 regression to watch: **no layout movement while typing** — the field's height stays put until focus
106
110
  leaves, and only then follows the content.
107
- - Widen the window and the stock behavior returns.
111
+ - Widen the window: the stock behavior returns on a **mouse-driven** desktop; a tablet/phone stays taken over however wide it gets (the tool row keeps the 「输入法✓」 chip).
108
112
 
109
113
  ## Update discipline (this plugin's convention)
110
114
 
111
- 1. **Hot updates only**: after a change, restart `dsh web` so the bundle revision is recomputed — client bundles are served `immutable`, and a new rev is what guarantees a phone refresh picks up the new code (never make the user clear caches).
115
+ 1. **Hot updates only**: after a change, **a page refresh is enough** — never make the user clear caches. Client bundles are served `immutable`, but the rev is recomputed from the artifact file's mtime/size by the client-modules HMR watch (measured 2026-10-06: new code served in a fresh page with no `dsh web` restart); only a disabled watch needs a restart.
112
116
  2. **Always keep an escape hatch**: anything that takes over stock UI behaviour must be revertible from the page itself (see the 输入法✓/✗ button below), so a bad build never blocks normal use.
113
117
 
114
118
  ## Compatibility hardening (v0.5.1 / v0.5.2 / v0.6.0, after HarmonyOS / ArkWeb feedback)
package/README.zh.md CHANGED
@@ -5,7 +5,7 @@
5
5
  [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(dsh)Web UI 的移动端插件,两件事:
6
6
 
7
7
  1. **in-flow composer**:窄屏(≤720px)时输入框与 AI 确认选择框随页面滚动,而不是钉死在屏幕底部——上滑阅读时消息区拿回全屏空间。
8
- 2. **原生输入框接管**:窄屏时输入区换成原生 `<textarea>`,绕开 Lexical contenteditable 在 Android 输入法(尤其是语音输入)下吞字/清空已输入文字的问题。
8
+ 2. **原生输入框接管**:窄屏**或触屏设备**(大屏平板同样在内)时输入区换成原生 `<textarea>`,绕开 Lexical contenteditable 在 Android/鸿蒙输入法(尤其是语音输入)下吞字/清空已输入文字的问题。
9
9
 
10
10
  纯客户端实现(CSS overlay + 一个 slot 组件),不修改产品源码;卸载插件即完全还原官方行为。
11
11
 
@@ -21,18 +21,19 @@
21
21
 
22
22
  ## 实际效果
23
23
 
24
- ≤720px 视口(与官方问题卡片的断点一致)下:
24
+ ≤720px 视口(与官方问题卡片的断点一致)下,前四条是布局修复;后几条按「窄屏或触屏设备」生效——**输入法问题跟屏幕大小无关**,鸿蒙平板在 1280px 上照样吞字(2026-10-05 用户反馈:MatePad + 豆包语音输入):
25
25
 
26
26
  1. **composer 座位回归文档流** —— 输入框和确认卡片位于消息流末尾。上滑时随内容滚出视口,消息全屏可读。
27
27
  2. **短会话仍然贴底** —— 消息不足一屏时消息区自动拉伸,输入框保持贴视口底,视觉与官方行为完全一致;长会话则享受全屏阅读。
28
28
  3. **浮动控件重新锚定** —— 回到底部按钮、轮次导航不再为吸底座位预留高度,回到贴近视口底的位置。
29
29
  4. **纤细边距** —— 消息区、会话头部、输入卡、确认卡片与屏幕边缘之间的内容层 padding(每边 16–32px)收窄为左右 4px、顶部 8px 的纤细边距,收复大部分被浪费的宽度,又不像完全贴边那样局促。页面外壳本身不产生空白,间隙完全来自这些 padding。
30
- 5. **切会话不自动聚焦** —— 官方在每次挂载/切换会话时把焦点还给输入框(桌面便利设计),手机上会弹出输入法占掉半屏。窄屏时拦截这类程序化聚焦;你主动点输入框时照常聚焦。
31
- 6. **workspace 行操作按钮常显** —— 分组行与会话行尾部的按钮(⋯ 菜单含重命名/删除、+ 在该 workspace 新建会话)官方仅在悬停时显示;触屏没有悬停,窄屏下侧边栏列表渲染时常显这两个按钮。
32
- 7. **原生输入框接管** —— 输入区绘制成原生 textarea,外观、内边距、字号行高与官方完全一致(几何按官方输入区实时测量对齐)。回车仍是官方发送手势(含 `/` 菜单仲裁、busy-Enter 策略),粘贴图片转发给官方附件入口,锁定态跟随官方可编辑性开关。
30
+ 5. **切会话不自动聚焦** —— 官方在每次挂载/切换会话时把焦点还给输入框(桌面便利设计),手机上会弹出输入法占掉半屏,平板上也一样挡事。窄屏与触屏设备上拦截这类程序化聚焦;你主动点输入框时照常聚焦。
31
+ 6. **workspace 行操作按钮常显** —— 分组行与会话行尾部的按钮(⋯ 菜单含重命名/删除、+ 在该 workspace 新建会话)官方仅在悬停时显示;触屏没有悬停,窄屏与触屏设备下侧边栏列表渲染时常显这两个按钮。
32
+ 7. **原生输入框接管** —— 窄屏或触屏设备默认接管(鼠标操作的桌面端不动),输入区绘制成原生 textarea,外观、内边距、字号行高与官方完全一致(几何按官方输入区实时测量对齐)。回车仍是官方发送手势(含 `/` 菜单仲裁、busy-Enter 策略),粘贴图片转发给官方附件入口,锁定态跟随官方可编辑性开关。
33
33
  8. **发送按钮跟「输入框里有什么」走** —— 只要输入框里有字,右下角发送按钮就是**可点的亮色**(不再是灰的),点一下即发送(运行中的回合则是排队/引导);打字期间这个状态由纯 CSS 驱动,**零 DOM 写入**,不破坏「打字期零 DOM 变动」这条硬约束。
34
+ 9. **开关不分屏幕大小** —— 工具行的「输入法✓/✗」在任何视口都显示(不只在手机上),所以「大屏平板要不要用原生输入框」永远有一键可切的地方;它同时也兼容「本插件没识别出这是触屏设备」的情况。
34
35
 
35
- 桌面端(宽视口)完全不受影响。
36
+ **鼠标操作的桌面端完全不受影响**(触屏设备即使屏幕很宽也会接管——这正是本次改动要的结果)。
36
37
 
37
38
  ## 工作原理
38
39
 
@@ -45,7 +46,7 @@
45
46
  ## 环境要求
46
47
 
47
48
  - DeepSeek Harness Web profile(`dsh web`),任意较新的 0.1.x 版本
48
- - 选择器与 slot 契约已在 0.1.2-rc.1 与 0.1.5-rc.1 上验证;它们瞄准产品 slot 契约,同一版本线内稳定,产品大改版后可能需要小幅更新
49
+ - 选择器与 slot 契约已在 0.1.2-rc.1、0.1.5-rc.1 与 0.2.0-rc.2 上验证(0.2.0 复核于 2026-10-04,用下方真机探针 + 装机客户端包静态审查);它们瞄准产品 slot 契约,同一版本线内稳定,产品大改版后可能需要小幅更新
49
50
 
50
51
  ## 安装
51
52
 
@@ -65,16 +66,18 @@ dsh plugin --profile web add github:imroc/dsh-mobile-flow
65
66
 
66
67
  ## 开关(可选)
67
68
 
68
- 原生输入框接管默认**只在窄屏(≤720px)启用**。需要临时覆盖时(例如在桌面浏览器上验证):
69
+ 原生输入框接管默认**在窄屏(≤720px)或触屏设备上启用**——跟屏幕多大无关。需要覆盖时(例如在桌面浏览器上验证):
69
70
 
70
71
  ```js
71
72
  // 浏览器控制台,同源页面均生效,持久化
72
73
  localStorage.setItem("dsh-mobile-flow:input", "off"); // 永远用官方输入框
73
- localStorage.setItem("dsh-mobile-flow:input", "on"); // 窄屏才接管
74
- localStorage.setItem("dsh-mobile-flow:input", "force"); // 任何视口都接管
75
- localStorage.removeItem("dsh-mobile-flow:input"); // 恢复默认
74
+ localStorage.setItem("dsh-mobile-flow:input", "on"); // 永远接管(任何视口,含桌面)
75
+ localStorage.setItem("dsh-mobile-flow:input", "force"); // 与 "on" 等价(URL 参数 =1 写的也是它)
76
+ localStorage.removeItem("dsh-mobile-flow:input"); // 恢复默认:窄屏或触屏设备自动接管
76
77
  ```
77
78
 
79
+ 页面上更省事的办法:点工具行的「输入法✓/✗」——它显示的是**当前这台设备实际有没有在接管**(不是存了什么偏好),点一下即在「永远接管」与「永远不接管」之间切换。
80
+
78
81
  一次性覆盖也可用 URL 参数:`?dsh-mobile-input=1`(强制)/`=0`(关闭)。
79
82
 
80
83
  诊断开关同理(持久化,点工具行「诊断」按钮即可,无需手打 URL):
@@ -95,19 +98,20 @@ localStorage.removeItem("dsh-mobile-flow:growth"); // 恢复默认
95
98
 
96
99
  ## 验证
97
100
 
98
- 手机上(或桌面 DevTools 缩到 ≤720px 窄窗)打开一个会话:
101
+ 手机上(或桌面 DevTools 缩到 ≤720px 窄窗)打开一个会话;**平板上(任何宽度)同样适用**:
99
102
 
100
103
  - 往上滑几屏——输入框和确认卡片应随消息滚出屏幕。
101
104
  - 输入区应是原生 textarea(页面里存在 `[data-mobile-input]`)。用输入法打字/语音输入,文字不应被自动清除。
102
105
  - 发送:点发按钮或按回车(回车等同官方发送手势)。
103
106
  - 发送按钮状态:**输入框里有字时按钮就是亮色可点的**(灰→亮由字段内容驱动);点一下即发送,一次点击一条消息。
104
- - 工具行应有**两个小按钮**:「输入法✓」(逃生通道)与「诊断」(开测试台)。
107
+ - 工具行日常只有**一个小按钮**「输入法✓」(逃生通道);在它上面**长按 600ms** 才会叫出「诊断」(再长按收起)。
108
+ - 0.2.0 上全新浏览器 profile 首次打开会弹产品的「预览版说明」提示框,先点「继续」再验证——它会夺走焦点并盖在输入框上,不点掉的话下面什么都打不进去。
105
109
  - 关键回归点(v0.6.0):**打字期间不要有任何布局跳动**——字段高度在聚焦期间不变,失焦后才按内容长高。
106
- - 拉宽窗口即恢复官方行为。
110
+ - 拉宽窗口:**用鼠标的桌面才恢复官方行为**;平板/手机即使屏幕很宽也应保持接管(工具行有「输入法✓」)。
107
111
 
108
112
  ## 更新纪律(本插件的约定)
109
113
 
110
- 1. **必须支持热更新**:改完即重启 dsh-web 重算 bundle rev(客户端插件产物带 `immutable` 缓存,换 rev 才能保证手机刷新拿到新码),不要让用户自己清缓存。
114
+ 1. **必须支持热更新**:改完**刷新页面即可**,不要让用户自己清缓存——客户端产物带 `immutable` 缓存,但 rev 是按产物文件 mtime/size 重算的(client-modules 的 HMR 监视,2026-10-06 实测:改完不重启 dsh-web,新开页面即拿到新码);只有监视被关掉时才需要重启 dsh-web。
111
115
  2. **必须预留逃生通道**:凡是接管官方 UI 行为的功能,都要能在页面上一条操作回退(见下文「输入法✓/✗」按钮),改坏了不影响正常使用。
112
116
 
113
117
  ## 兼容性加固(v0.5.1 / v0.5.2 / v0.6.0,HarmonyOS / ArkWeb 实测反馈)
package/lib/client.js CHANGED
@@ -88,13 +88,26 @@ window.__ModuleLoader__.load({
88
88
  " display: none !important;",
89
89
  " }",
90
90
  "}",
91
+ "/* Touch devices of any width: a tablet has no hover either, so the same",
92
+ " two affordances stay visible there. Only these two rules ride the",
93
+ " coarse-pointer query — the layout transforms above are phone tuning",
94
+ " (a tablet keeps the stock docked composer and stock paddings). */",
95
+ "@media (any-pointer: coarse) {",
96
+ " [class*='_rowActions'] {",
97
+ " display: inline-flex !important;",
98
+ " }",
99
+ " [class*='_sessionRow'] [class*='_time'] {",
100
+ " display: none !important;",
101
+ " }",
102
+ "}",
91
103
  ].join("\n");
92
104
 
93
105
  /* Native-input takeover styles. These ride OUTSIDE the media query on
94
- purpose: the JS half decides when the takeover is live (narrow viewport
95
- by default, or an explicit override) and marks the card with
96
- data-mobile-input-active, so a forced takeover also works on a desktop
97
- viewport. Every rule below is inert until that marker is present. */
106
+ purpose: the JS half decides when the takeover is live (a narrow viewport
107
+ or a touch device by default, or an explicit override) and marks the card
108
+ with data-mobile-input-active, so a forced takeover also works on a
109
+ desktop viewport. Every rule below is inert until that marker is
110
+ present. */
98
111
  var INPUT_CSS = [
99
112
  "/* ── dsh-mobile-flow: native textarea takeover ── */",
100
113
  "/* The stock draft surface keeps its box (it is what sizes the card) but",
@@ -197,11 +210,13 @@ window.__ModuleLoader__.load({
197
210
  " -webkit-mask: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M8.3125 0.980183C8.66767 1.0531 8.97902 1.20418 9.2627 1.43233C9.48724 1.61297 9.73029 1.85793 9.97949 2.10714L14.707 6.83468L13.293 8.24874L9 3.95577V15.0417H7V3.95577L2.70703 8.24874L1.29297 6.83468L6.02051 2.10714C6.26971 1.85793 6.51277 1.61297 6.7373 1.43233C6.97662 1.23986 7.28445 1.04402 7.6875 0.980183C7.8973 0.947006 8.1031 0.95516 8.3125 0.980183Z' fill='%23000'/%3E%3C/svg%3E\") center / 16px 16px no-repeat;",
198
211
  " mask: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M8.3125 0.980183C8.66767 1.0531 8.97902 1.20418 9.2627 1.43233C9.48724 1.61297 9.73029 1.85793 9.97949 2.10714L14.707 6.83468L13.293 8.24874L9 3.95577V15.0417H7V3.95577L2.70703 8.24874L1.29297 6.83468L6.02051 2.10714C6.26971 1.85793 6.51277 1.61297 6.7373 1.43233C6.97662 1.23986 7.28445 1.04402 7.6875 0.980183C7.8973 0.947006 8.1031 0.95516 8.3125 0.980183Z' fill='%23000'/%3E%3C/svg%3E\") center / 16px 16px no-repeat;",
199
212
  "}",
200
- "/* Escape hatch in the composer tool row (narrow viewports only): one tap",
201
- " returns the stock input box when the native one misbehaves on a device",
202
- " we cannot test on. Its neighbour switches the diagnostics surfaces on",
203
- " and off (a tokenized URL is rewritten before plugins load, so a phone",
204
- " cannot always pass ?dsh-mobile-input=... from the address bar). */",
213
+ "/* Escape hatch in the composer tool row (every viewport — a tablet needs it",
214
+ " as much as a phone): one tap returns the stock input box when the native",
215
+ " one misbehaves on a device we cannot test on, and it is also how a device",
216
+ " the default gate does not detect asks for the takeover. Its neighbour",
217
+ " switches the diagnostics surfaces on and off (a tokenized URL is rewritten",
218
+ " before plugins load, so a phone cannot always pass ?dsh-mobile-input=...",
219
+ " from the address bar). */",
205
220
  "[data-mobile-input-toggle],",
206
221
  "[data-mobile-input-copy],",
207
222
  "[data-mobile-input-diagnostics] {",
@@ -333,7 +348,8 @@ window.__ModuleLoader__.load({
333
348
  diagnostics.add("h=" + window.innerHeight
334
349
  + " growth=" + readGrowthMode()
335
350
  + " input=" + String(readOverride())
336
- + " narrow=" + String(window.matchMedia !== undefined && window.matchMedia(NARROW_QUERY).matches));
351
+ + " narrow=" + String(window.matchMedia !== undefined && window.matchMedia(NARROW_QUERY).matches)
352
+ + " touch=" + String(isTouchDevice()));
337
353
  ctx.effect(function () {
338
354
  return watchKeyboard(diagnostics);
339
355
  });
@@ -377,11 +393,12 @@ window.__ModuleLoader__.load({
377
393
  }, createNativeInputToggle(diagnostics)));
378
394
  }
379
395
 
380
- /* 5) No auto-focus on session switch (narrow viewports): InputBar's
381
- unlock effect focuses the contenteditable on every mount / session
382
- switch (packages/client/ui-conversation/.../InputBar.tsx, deps
396
+ /* 5) No auto-focus on session switch (narrow viewports and touch devices):
397
+ InputBar's unlock effect focuses the contenteditable on every mount /
398
+ session switch (packages/client/ui-conversation/.../InputBar.tsx, deps
383
399
  [locked, sessionId, editor]) — a desktop convenience that pops the
384
- IME over half the screen on phones. Swallow programmatic focus() on
400
+ IME over half the screen on phones AND on tablets (a big screen has no
401
+ room for a software keyboard either). Swallow programmatic focus() on
385
402
  the composer's editable unless the user just tapped it (or is
386
403
  keyboard-navigating); taps reach the box via the normal focus path.
387
404
  Question-card textareas (autoFocus on purpose) are not contenteditable
@@ -410,7 +427,7 @@ window.__ModuleLoader__.load({
410
427
  var nativeFocus = HTMLElement.prototype.focus;
411
428
  HTMLElement.prototype.focus = function (opts) {
412
429
  if (
413
- narrow.matches &&
430
+ (narrow.matches || isTouchDevice()) &&
414
431
  this.isContentEditable === true &&
415
432
  typeof this.closest === "function" &&
416
433
  this.closest("[data-composer-seat]") !== null &&
@@ -477,6 +494,24 @@ window.__ModuleLoader__.load({
477
494
  the `dsh-mobile-input` query parameter for one page load. */
478
495
  var OVERRIDE_KEY = "dsh-mobile-flow:input";
479
496
  var NARROW_QUERY = "(max-width: 720px)";
497
+ /* The takeover answers a MOBILE IME, and a mobile IME has nothing to do with
498
+ the viewport width: a HarmonyOS/Android tablet runs the same keyboard (and
499
+ loses typed text the same way) at 1280px. Touch is the honest signal the
500
+ page can read, so the default gate is "narrow OR touch" — a mouse-driven
501
+ desktop, where the Lexical editor is not at risk, stays untouched.
502
+ (Reported 2026-10-05 from a MatePad + Doubao voice input.) */
503
+ var TOUCH_QUERY = "(any-pointer: coarse)";
504
+
505
+ function isTouchDevice() {
506
+ try {
507
+ if (typeof window.matchMedia === "function" && window.matchMedia(TOUCH_QUERY).matches === true) return true;
508
+ } catch (error) { /* fall through to the capability probe */ }
509
+ try {
510
+ if (typeof navigator === "object" && navigator !== null
511
+ && typeof navigator.maxTouchPoints === "number" && navigator.maxTouchPoints > 0) return true;
512
+ } catch (error) { /* no navigator: treat as a mouse device */ }
513
+ return false;
514
+ }
480
515
 
481
516
  function readOverride() {
482
517
  try {
@@ -806,33 +841,23 @@ window.__ModuleLoader__.load({
806
841
  return media !== null && media.matches === true;
807
842
  }
808
843
 
809
- /** Narrow viewport only (no preference): the toggle's own visibility gate. */
810
- function useNarrowViewport() {
811
- var media = React.useMemo(function () {
812
- try {
813
- return window.matchMedia(NARROW_QUERY);
814
- } catch (error) {
815
- return null;
816
- }
817
- }, []);
818
- var state = React.useState(function () { return media !== null && media.matches === true; });
819
- var narrow = state[0];
820
- var setNarrow = state[1];
821
- React.useEffect(function () {
822
- if (media === null) return undefined;
823
- var update = function () { setNarrow(media.matches === true); };
824
- update();
825
- if (typeof media.addEventListener === "function") media.addEventListener("change", update);
826
- else if (typeof media.addListener === "function") media.addListener(update);
827
- return function () {
828
- if (typeof media.removeEventListener === "function") media.removeEventListener("change", update);
829
- else if (typeof media.removeListener === "function") media.removeListener(update);
830
- };
831
- }, [media]);
832
- return narrow;
844
+ /**
845
+ * Whether the takeover should own the surface on this viewport.
846
+ *
847
+ * Preference first ("on"/"force" = always, "off" = never); with no stored
848
+ * preference the DEVICE decides: narrow viewports (the original gate) or
849
+ * any touch device (a tablet's IME is the same problem at a larger width).
850
+ * @param media - the narrow-viewport matcher, or null.
851
+ */
852
+ function takeoverLive(media) {
853
+ var override = readOverride();
854
+ if (override === "on" || override === "force") return true;
855
+ if (override === "off") return false;
856
+ if (media !== null && media.matches === true) return true;
857
+ return isTouchDevice();
833
858
  }
834
859
 
835
- /** Narrow-viewport flag, live across rotation, resize and override edits. */
860
+ /** Viewport + device flag, live across rotation, resize and override edits. */
836
861
  function useTakeoverViewport() {
837
862
  var media = React.useMemo(function () {
838
863
  try {
@@ -1761,17 +1786,19 @@ window.__ModuleLoader__.load({
1761
1786
  }
1762
1787
 
1763
1788
  /**
1764
- * Escape hatch in the composer tool row (narrow viewports only): one tap
1765
- * swaps the native input back to the stock editor and remembers it, so a
1766
- * device the takeover misbehaves on is never a dead end.
1789
+ * Escape hatch in the composer tool row (every viewport, not just phones):
1790
+ * one tap swaps the native input back to the stock editor and remembers it,
1791
+ * so a device the takeover misbehaves on is never a dead end. It is also the
1792
+ * only way to ask for the takeover on a device this build does not detect —
1793
+ * a large tablet reports touch, but a device that lies needs the switch.
1767
1794
  * @returns the slot entry component.
1768
1795
  */
1769
1796
  function createNativeInputToggle(diagnostics) {
1770
1797
  return function NativeInputToggle() {
1771
- var narrow = useNarrowViewport();
1772
- var state = React.useState(function () { return readOverride(); });
1773
- var preference = state[0];
1774
- var setPreference = state[1];
1798
+ /* The chip reports WHAT THE TAKEOVER IS DOING on this device, not the raw
1799
+ preference: on a wide mouse-only desktop the default is "off" and the
1800
+ chip must say so (and offer to turn it on). */
1801
+ var on = useTakeoverViewport();
1775
1802
  var revealState = React.useState(function () { return readReveal(); });
1776
1803
  var revealed = revealState[0];
1777
1804
  var setRevealed = revealState[1];
@@ -1797,7 +1824,6 @@ window.__ModuleLoader__.load({
1797
1824
  };
1798
1825
  React.useEffect(function () {
1799
1826
  var update = function () {
1800
- setPreference(readOverride());
1801
1827
  setRevealed(readReveal());
1802
1828
  };
1803
1829
  window.addEventListener(PREFERENCE_EVENT, update);
@@ -1810,8 +1836,6 @@ window.__ModuleLoader__.load({
1810
1836
  endPress();
1811
1837
  };
1812
1838
  }, []);
1813
- if (!narrow) return null;
1814
- var on = preference !== "off";
1815
1839
  var label = (on ? "原生输入框:已开启(点击改用官方输入框)" : "原生输入框:已关闭(点击启用)")
1816
1840
  + ";长按显示/隐藏诊断按钮";
1817
1841
  var diagnosing = diagnosticsModes().length !== 0;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-mobile-flow",
3
- "version": "0.7.3",
4
- "description": "Mobile fixes for the DeepSeek Harness Web UI: on narrow (≤720px) screens the input bar and AI confirmation cards scroll with the page instead of pinning to the viewport floor, edges slim down, session switches no longer auto-focus the input, workspace row actions stay visible, and the draft surface becomes a native textarea (ArkWeb/Android-IME hardened: zero DOM writes while typing, resizing only at commit points, with a tool-row escape hatch and an on-device diagnostics bench) so mobile IMEs stop clearing typed text and stop dropping the keyboard; the send button follows the field's content (live as soon as there is text, one tap sends), not the deliberately stale machine draft.",
3
+ "version": "0.8.0",
4
+ "description": "Mobile fixes for the DeepSeek Harness Web UI: on narrow (≤720px) screens the input bar and AI confirmation cards scroll with the page instead of pinning to the viewport floor, edges slim down, and touch devices (any width, a tablet included) no longer auto-focus the input or hide the workspace row actions; the draft surface becomes a native textarea on narrow screens and on any touch device (ArkWeb/Android-IME hardened: zero DOM writes while typing, resizing only at commit points, with a tool-row escape hatch and an on-device diagnostics bench) so mobile IMEs stop clearing typed text and stop dropping the keyboard; the send button follows the field's content (live as soon as there is text, one tap sends), not the deliberately stale machine draft.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "main": "lib/index.js",