dsh-mobile-flow 0.7.2 → 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 +35 -16
- package/README.zh.md +30 -15
- package/lib/client.js +148 -59
- package/package.json +2 -2
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,17 +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.
|
|
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
|
+
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.
|
|
33
35
|
|
|
34
|
-
|
|
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).
|
|
35
37
|
|
|
36
38
|
## How it works
|
|
37
39
|
|
|
@@ -44,7 +46,7 @@ The plugin ships a browser half (`exports["./client"]`, declared via `dsh.client
|
|
|
44
46
|
## Requirements
|
|
45
47
|
|
|
46
48
|
- DeepSeek Harness Web profile (`dsh web`), any recent 0.1.x release
|
|
47
|
-
- Selectors and slot contracts verified against 0.1.2-rc.1
|
|
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
|
|
48
50
|
|
|
49
51
|
## Install
|
|
50
52
|
|
|
@@ -64,16 +66,18 @@ Restart `dsh web`, then refresh the browser page.
|
|
|
64
66
|
|
|
65
67
|
## Overrides (optional)
|
|
66
68
|
|
|
67
|
-
The native takeover is on by default **on narrow viewports
|
|
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:
|
|
68
70
|
|
|
69
71
|
```js
|
|
70
72
|
// Browser console, same origin, persists
|
|
71
73
|
localStorage.setItem("dsh-mobile-flow:input", "off"); // never take over
|
|
72
|
-
localStorage.setItem("dsh-mobile-flow:input", "on"); //
|
|
73
|
-
localStorage.setItem("dsh-mobile-flow:input", "force"); //
|
|
74
|
-
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
|
|
75
77
|
```
|
|
76
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
|
+
|
|
77
81
|
One-shot override: `?dsh-mobile-input=1` (force) / `=0` (off).
|
|
78
82
|
|
|
79
83
|
Diagnostics have their own persisted switch (the tool-row 诊断 button writes it; no URL typing needed):
|
|
@@ -94,19 +98,21 @@ localStorage.removeItem("dsh-mobile-flow:growth"); // back to default
|
|
|
94
98
|
|
|
95
99
|
## Verify
|
|
96
100
|
|
|
97
|
-
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**:
|
|
98
102
|
|
|
99
103
|
- Swipe up a few screens: the input bar and confirmation cards scroll away with the messages.
|
|
100
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.
|
|
101
105
|
- Send with the send button or Enter (same gesture as the stock composer).
|
|
102
|
-
-
|
|
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.
|
|
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.
|
|
103
109
|
- The v0.6.0 regression to watch: **no layout movement while typing** — the field's height stays put until focus
|
|
104
110
|
leaves, and only then follows the content.
|
|
105
|
-
- Widen the window
|
|
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).
|
|
106
112
|
|
|
107
113
|
## Update discipline (this plugin's convention)
|
|
108
114
|
|
|
109
|
-
1. **Hot updates only**: after a change,
|
|
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.
|
|
110
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.
|
|
111
117
|
|
|
112
118
|
## Compatibility hardening (v0.5.1 / v0.5.2 / v0.6.0, after HarmonyOS / ArkWeb feedback)
|
|
@@ -210,6 +216,13 @@ Fixes:
|
|
|
210
216
|
making its composer editable again — the field's `readOnly` is cleared then, so a latched field can never outlive
|
|
211
217
|
the state that caused it.
|
|
212
218
|
|
|
219
|
+
**v0.7.3 (phone report: with the native input on, typed text left the bottom-right send button grey — it looked like nothing could be sent)**: the button is derived from the MACHINE draft, and the takeover keeps that draft deliberately stale while the user types (the ArkWeb lesson: commit points only). So the product kept believing the composer was empty and left its send button disabled at 40 % opacity. The old bridge (replay the tap on the button the commit enabled) depended on the engine re-testing that control after the pointer had already landed, which differs per engine — and under a running turn the same button wore the Stop square, so a tap could interrupt instead of send.
|
|
220
|
+
|
|
221
|
+
Two fixes:
|
|
222
|
+
|
|
223
|
+
- **The look**: the machine draft is stale, but the FIELD knows whether it holds text, and `:placeholder-shown` is the one pure-CSS value probe. While the field holds text and the machine draft is still empty, the takeover paints that button enabled (opacity 1, pointer cursor, pressed feedback) — **zero DOM writes**, the hard rule stands. Once the draft is in sync (e.g. after a commit) the button is left exactly as the product renders it.
|
|
224
|
+
- **The gesture**: the card's capture `pointerdown` now recognises the submit control (the trailing row's last primary button; an interruptible child's extra Stop circle renders before it) and, when a draft is still uncommitted, **takes the gesture**: commit the draft, then run **the product's own send gesture** — the same path Enter takes (slash adjudication, busy policy, steer/queue). One tap, one send, no engine timing involved. Under a running turn the button now wears the send arrow while the field holds text (the Stop square is hidden and the product's own arrow path is painted as a mask), so face and gesture agree.
|
|
225
|
+
|
|
213
226
|
**Escape hatch**: a small tool-row button (**输入法✓ / 输入法✗**, narrow viewports only) swaps back to the stock
|
|
214
227
|
input box and remembers the choice — no device can be left stuck.
|
|
215
228
|
|
|
@@ -236,14 +249,20 @@ things, that **typing writes nothing to the page DOM**.
|
|
|
236
249
|
- While the takeover is active, inline chip/decoration rendering inside the draft (e.g. `@` reference decorations) is not shown; entering a command claim switches back to the stock editor automatically.
|
|
237
250
|
- Pasting an image is implemented by forwarding the paste to the official attachment intake; browsers that refuse a constructed paste event fall back to the paperclip picker.
|
|
238
251
|
- IME candidate/assist behavior itself belongs to the system keyboard and is outside the plugin's control.
|
|
252
|
+
- **While a turn streams and the field holds uncommitted text**, that button means Send (queue/steer) and no Stop affordance is offered — clear the field to interrupt. This matches the product's own choice of Send as the primary action whenever a draft exists.
|
|
239
253
|
|
|
240
254
|
## Development
|
|
241
255
|
|
|
242
256
|
```sh
|
|
243
257
|
npm install --no-save jsdom react@18 react-dom@18
|
|
244
|
-
node test/takeover.test.mjs
|
|
258
|
+
node test/takeover.test.mjs # runs the real client bundle in a jsdom composer card
|
|
259
|
+
node test/probe-live.mjs <token> # live app: takeover contract + diagnostics switches (CDP, 390x844)
|
|
260
|
+
node test/probe-send.mjs <token> # live app: the send button's look, one-tap send, the running turn
|
|
245
261
|
```
|
|
246
262
|
|
|
263
|
+
The launch token comes from `journalctl --user -u dsh-web | grep -o 'token=[A-Za-z0-9_-]*' | tail -1`
|
|
264
|
+
(a new one per restart; the probes spend it on their first navigation).
|
|
265
|
+
|
|
247
266
|
The test boots the shipped `lib/client.js`, renders the slot entry through React against a
|
|
248
267
|
composer-card DOM, and asserts the render path, the draft mirror (typing / IME composition /
|
|
249
268
|
machine-side writes), the Enter gesture, and the phase gate that hands the surface back to the
|
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.
|
|
8
|
+
2. **原生输入框接管**:窄屏**或触屏设备**(大屏平板同样在内)时输入区换成原生 `<textarea>`,绕开 Lexical contenteditable 在 Android/鸿蒙输入法(尤其是语音输入)下吞字/清空已输入文字的问题。
|
|
9
9
|
|
|
10
10
|
纯客户端实现(CSS overlay + 一个 slot 组件),不修改产品源码;卸载插件即完全还原官方行为。
|
|
11
11
|
|
|
@@ -21,17 +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. **原生输入框接管** ——
|
|
30
|
+
5. **切会话不自动聚焦** —— 官方在每次挂载/切换会话时把焦点还给输入框(桌面便利设计),手机上会弹出输入法占掉半屏,平板上也一样挡事。窄屏与触屏设备上拦截这类程序化聚焦;你主动点输入框时照常聚焦。
|
|
31
|
+
6. **workspace 行操作按钮常显** —— 分组行与会话行尾部的按钮(⋯ 菜单含重命名/删除、+ 在该 workspace 新建会话)官方仅在悬停时显示;触屏没有悬停,窄屏与触屏设备下侧边栏列表渲染时常显这两个按钮。
|
|
32
|
+
7. **原生输入框接管** —— 窄屏或触屏设备默认接管(鼠标操作的桌面端不动),输入区绘制成原生 textarea,外观、内边距、字号行高与官方完全一致(几何按官方输入区实时测量对齐)。回车仍是官方发送手势(含 `/` 菜单仲裁、busy-Enter 策略),粘贴图片转发给官方附件入口,锁定态跟随官方可编辑性开关。
|
|
33
|
+
8. **发送按钮跟「输入框里有什么」走** —— 只要输入框里有字,右下角发送按钮就是**可点的亮色**(不再是灰的),点一下即发送(运行中的回合则是排队/引导);打字期间这个状态由纯 CSS 驱动,**零 DOM 写入**,不破坏「打字期零 DOM 变动」这条硬约束。
|
|
34
|
+
9. **开关不分屏幕大小** —— 工具行的「输入法✓/✗」在任何视口都显示(不只在手机上),所以「大屏平板要不要用原生输入框」永远有一键可切的地方;它同时也兼容「本插件没识别出这是触屏设备」的情况。
|
|
33
35
|
|
|
34
|
-
|
|
36
|
+
**鼠标操作的桌面端完全不受影响**(触屏设备即使屏幕很宽也会接管——这正是本次改动要的结果)。
|
|
35
37
|
|
|
36
38
|
## 工作原理
|
|
37
39
|
|
|
@@ -44,7 +46,7 @@
|
|
|
44
46
|
## 环境要求
|
|
45
47
|
|
|
46
48
|
- DeepSeek Harness Web profile(`dsh web`),任意较新的 0.1.x 版本
|
|
47
|
-
- 选择器与 slot 契约已在 0.1.2-rc.1
|
|
49
|
+
- 选择器与 slot 契约已在 0.1.2-rc.1、0.1.5-rc.1 与 0.2.0-rc.2 上验证(0.2.0 复核于 2026-10-04,用下方真机探针 + 装机客户端包静态审查);它们瞄准产品 slot 契约,同一版本线内稳定,产品大改版后可能需要小幅更新
|
|
48
50
|
|
|
49
51
|
## 安装
|
|
50
52
|
|
|
@@ -64,16 +66,18 @@ dsh plugin --profile web add github:imroc/dsh-mobile-flow
|
|
|
64
66
|
|
|
65
67
|
## 开关(可选)
|
|
66
68
|
|
|
67
|
-
|
|
69
|
+
原生输入框接管默认**在窄屏(≤720px)或触屏设备上启用**——跟屏幕多大无关。需要覆盖时(例如在桌面浏览器上验证):
|
|
68
70
|
|
|
69
71
|
```js
|
|
70
72
|
// 浏览器控制台,同源页面均生效,持久化
|
|
71
73
|
localStorage.setItem("dsh-mobile-flow:input", "off"); // 永远用官方输入框
|
|
72
|
-
localStorage.setItem("dsh-mobile-flow:input", "on"); //
|
|
73
|
-
localStorage.setItem("dsh-mobile-flow:input", "force"); //
|
|
74
|
-
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"); // 恢复默认:窄屏或触屏设备自动接管
|
|
75
77
|
```
|
|
76
78
|
|
|
79
|
+
页面上更省事的办法:点工具行的「输入法✓/✗」——它显示的是**当前这台设备实际有没有在接管**(不是存了什么偏好),点一下即在「永远接管」与「永远不接管」之间切换。
|
|
80
|
+
|
|
77
81
|
一次性覆盖也可用 URL 参数:`?dsh-mobile-input=1`(强制)/`=0`(关闭)。
|
|
78
82
|
|
|
79
83
|
诊断开关同理(持久化,点工具行「诊断」按钮即可,无需手打 URL):
|
|
@@ -94,18 +98,20 @@ localStorage.removeItem("dsh-mobile-flow:growth"); // 恢复默认
|
|
|
94
98
|
|
|
95
99
|
## 验证
|
|
96
100
|
|
|
97
|
-
手机上(或桌面 DevTools 缩到 ≤720px
|
|
101
|
+
手机上(或桌面 DevTools 缩到 ≤720px 窄窗)打开一个会话;**平板上(任何宽度)同样适用**:
|
|
98
102
|
|
|
99
103
|
- 往上滑几屏——输入框和确认卡片应随消息滚出屏幕。
|
|
100
104
|
- 输入区应是原生 textarea(页面里存在 `[data-mobile-input]`)。用输入法打字/语音输入,文字不应被自动清除。
|
|
101
105
|
- 发送:点发按钮或按回车(回车等同官方发送手势)。
|
|
102
|
-
-
|
|
106
|
+
- 发送按钮状态:**输入框里有字时按钮就是亮色可点的**(灰→亮由字段内容驱动);点一下即发送,一次点击一条消息。
|
|
107
|
+
- 工具行日常只有**一个小按钮**「输入法✓」(逃生通道);在它上面**长按 600ms** 才会叫出「诊断」(再长按收起)。
|
|
108
|
+
- 0.2.0 上全新浏览器 profile 首次打开会弹产品的「预览版说明」提示框,先点「继续」再验证——它会夺走焦点并盖在输入框上,不点掉的话下面什么都打不进去。
|
|
103
109
|
- 关键回归点(v0.6.0):**打字期间不要有任何布局跳动**——字段高度在聚焦期间不变,失焦后才按内容长高。
|
|
104
|
-
-
|
|
110
|
+
- 拉宽窗口:**用鼠标的桌面才恢复官方行为**;平板/手机即使屏幕很宽也应保持接管(工具行有「输入法✓」)。
|
|
105
111
|
|
|
106
112
|
## 更新纪律(本插件的约定)
|
|
107
113
|
|
|
108
|
-
1.
|
|
114
|
+
1. **必须支持热更新**:改完**刷新页面即可**,不要让用户自己清缓存——客户端产物带 `immutable` 缓存,但 rev 是按产物文件 mtime/size 重算的(client-modules 的 HMR 监视,2026-10-06 实测:改完不重启 dsh-web,新开页面即拿到新码);只有监视被关掉时才需要重启 dsh-web。
|
|
109
115
|
2. **必须预留逃生通道**:凡是接管官方 UI 行为的功能,都要能在页面上一条操作回退(见下文「输入法✓/✗」按钮),改坏了不影响正常使用。
|
|
110
116
|
|
|
111
117
|
## 兼容性加固(v0.5.1 / v0.5.2 / v0.6.0,HarmonyOS / ArkWeb 实测反馈)
|
|
@@ -153,6 +159,13 @@ localStorage.removeItem("dsh-mobile-flow:growth"); // 恢复默认
|
|
|
153
159
|
|
|
154
160
|
**v0.7.1**:工具行清爽化——日常只留 **「输入法✓/✗」** 一个按钮(逃生通道),**长按 600ms** 才把 **「诊断」** 按钮叫出来(再长按收起);诊断开着时「诊断」「复制日志」两个按钮自动出现,方便你取日志或关掉。
|
|
155
161
|
|
|
162
|
+
**v0.7.3(手机反馈:勾选「输入法」后输入文本,右下角发送按钮是灰的,像没法发)**:根因是**按钮读的是机器草稿,而接管期间机器草稿故意是旧的**——打字时插件只在提交点才回写草稿(ArkWeb 收键盘的教训),于是官方发送按钮一直以为输入框是空的,保持禁用(40% 透明度的灰)。旧代码虽然写了「提交后补发点击」的桥接,但它依赖引擎在指针落下之后再复查一次这个刚被"点亮"的按钮,不同引擎结果不一样,且运行中的回合还会把 Stop 方块画在这颗按钮上(点下去可能变成打断)。
|
|
163
|
+
|
|
164
|
+
改法两条:
|
|
165
|
+
|
|
166
|
+
- **外观**:机器草稿是旧的,但**字段自己知道有没有字**,而 `:placeholder-shown` 是纯 CSS 里唯一能读字段值的探针。于是接管卡片在「字段有字 + 机器草稿仍为空」时把发送按钮画成启用态(opacity 1、手型光标、按下有反馈)——**零 DOM 写入**,硬约束不破;机器草稿已经同步(如已提交)时不动它,保持官方原样。
|
|
167
|
+
- **手势**:卡片捕获阶段的 `pointerdown` 判断「点的是不是提交按钮(trailing 行里最后一颗 primary,可中断子会话额外的 Stop 圆钮在它前面)」且「还有未提交的文字」,是则**拿走这次手势**:先落草稿,再走**官方自己的发送手势**(与回车同一条路:斜杠仲裁、busy 策略、排队/引导),一次点击一条消息,不再看引擎脸色。运行中的回合里,字段有字时按钮**换成发送箭头**(隐藏官方的 Stop 方块,用官方同一份箭头 path 画 mask),点它就是发送——脸和手势一致。
|
|
168
|
+
|
|
156
169
|
**v0.7.2(手机反馈:新建会话后点输入框没反应)**:新会话刚建出来、**workspace 还没解析出来**时,官方卡片其实是「选择工作区」的**选择器触发器**(`aria-haspopup="menu"`、`contenteditable="false"`、没有绑编辑器),根本不是输入框。旧版接管照样压上去,还把这份「不可编辑」**锁存**成原生 textarea 的 `readOnly` —— 于是:点输入框**不弹键盘、不出现光标**,同时把官方那记「打开选择器」的点击吞掉,用户既不能打字也打不开选择器;而且 workspace 解析好之后它**不会自愈**(只有换会话重新挂载才恢复),正好对上「第一次新建会话没反应、切走再新建就好」的现象(2026-09-19)。
|
|
157
170
|
|
|
158
171
|
处理:
|
|
@@ -183,6 +196,7 @@ localStorage.removeItem("dsh-mobile-flow:growth"); // 恢复默认
|
|
|
183
196
|
- 原生接管期间,输入区内联的 chip/装饰(如 `@` 引用装饰)不显示;进入命令 claim 阶段会自动切回官方编辑器。
|
|
184
197
|
- 移动端「粘贴图片」通过转发给官方附件入口实现,浏览器若不允许构造粘贴事件则退化为用回形针按钮选择文件。
|
|
185
198
|
- 输入法自身的候选词栏/联想行为由系统输入法决定,插件无法控制。
|
|
199
|
+
- **回合运行中 + 输入框里有未提交文字**时,那颗按钮是「发送」(排队/引导),不再提供「停止」——想打断这个回合就先清空输入框(清空后按钮立刻变回 Stop)。这与官方「草稿非空时主按钮是发送」的取向一致。
|
|
186
200
|
|
|
187
201
|
## 开发与自测
|
|
188
202
|
|
|
@@ -191,6 +205,7 @@ npm install --no-save jsdom react@18 react-dom@18
|
|
|
191
205
|
node test/takeover.test.mjs # jsdom:渲染 + 打字路径(含"打字期零 DOM 写入"断言)
|
|
192
206
|
node test/facts-probe.mjs <token> # 真实页面:DOM/CSS 结构与引擎能力
|
|
193
207
|
node test/probe-live.mjs <token> # 真实页面:接管契约 + 诊断开关(CDP,390x844 移动视口)
|
|
208
|
+
node test/probe-send.mjs <token> # 真实页面:发送按钮的亮色状态 + 一次点击发送 + 运行中回合
|
|
194
209
|
```
|
|
195
210
|
|
|
196
211
|
token 取自 `journalctl --user -u dsh-web | grep -o 'token=[A-Za-z0-9_-]*' | tail -1`(每次重启换新;单次有效,探针只用一次)。
|
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
|
|
96
|
-
data-mobile-input-active, so a forced takeover also works on a
|
|
97
|
-
viewport. Every rule below is inert until that marker is
|
|
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",
|
|
@@ -156,11 +169,54 @@ window.__ModuleLoader__.load({
|
|
|
156
169
|
" color: var(--dsw-alias-label-tertiary);",
|
|
157
170
|
" cursor: not-allowed;",
|
|
158
171
|
"}",
|
|
159
|
-
"/*
|
|
160
|
-
"
|
|
161
|
-
"
|
|
162
|
-
"
|
|
163
|
-
"
|
|
172
|
+
"/* Send-button honesty (phone report 2026-09-21: text typed, the send",
|
|
173
|
+
" button stays grey — it reads as \"cannot send\"). The product derives",
|
|
174
|
+
" that button from the MACHINE draft, and the takeover keeps the machine",
|
|
175
|
+
" draft deliberately stale while the user types (the ArkWeb fix), so a",
|
|
176
|
+
" filled field sits next to a disabled, 40%-opacity button. The field's",
|
|
177
|
+
" own value state is the truth the eye needs, and :placeholder-shown is",
|
|
178
|
+
" the one PURE-CSS value probe — no DOM write, which is the hard rule",
|
|
179
|
+
" while a field has focus. The gesture is carried by the commit bridge in",
|
|
180
|
+
" the card's capture pointerdown, so the look and the tap agree: one tap,",
|
|
181
|
+
" one send. The submit button is the trailing row's LAST primary control;",
|
|
182
|
+
" an interruptible child's extra Stop circle renders before it. */",
|
|
183
|
+
"[data-composer-card][data-mobile-input-active]:has([data-mobile-input]:not(:placeholder-shown)) [class*='_trailing'] > [class*='_primary']:last-child:disabled {",
|
|
184
|
+
" opacity: 1;",
|
|
185
|
+
" cursor: pointer;",
|
|
186
|
+
"}",
|
|
187
|
+
"/* Pressed feedback for that same button: the product's own :active rules",
|
|
188
|
+
" only cover the enabled state, and a tap with no visible answer reads as",
|
|
189
|
+
" a dead control. */",
|
|
190
|
+
"[data-composer-card][data-mobile-input-active]:has([data-mobile-input]:not(:placeholder-shown)) [class*='_trailing'] > [class*='_primary']:last-child:disabled:active {",
|
|
191
|
+
" opacity: 0.72;",
|
|
192
|
+
"}",
|
|
193
|
+
"/* The running-turn case: while a turn streams, the product puts its Stop",
|
|
194
|
+
" square on that button (its stale-empty draft tells it the composer holds",
|
|
195
|
+
" nothing, so interrupting is the only action left). With text in the",
|
|
196
|
+
" field the tap SENDS — the bridge turns it into the product's own submit",
|
|
197
|
+
" gesture (queue / steer under a running turn) — so the face has to say",
|
|
198
|
+
" send. Gated on the machine draft still being EMPTY (the stock editor is",
|
|
199
|
+
" never empty in that state), so the arrow only ever replaces the Stop",
|
|
200
|
+
" square and never the product's own send arrow. The glyph is the",
|
|
201
|
+
" product's own arrow: 16px, currentColor on the blue fill. */",
|
|
202
|
+
"[data-composer-card][data-mobile-input-active]:has([data-composer-input]:empty):has([data-mobile-input]:not(:placeholder-shown)) [class*='_trailing'] > [class*='_primary']:last-child:not(:disabled) > svg {",
|
|
203
|
+
" display: none;",
|
|
204
|
+
"}",
|
|
205
|
+
"[data-composer-card][data-mobile-input-active]:has([data-composer-input]:empty):has([data-mobile-input]:not(:placeholder-shown)) [class*='_trailing'] > [class*='_primary']:last-child:not(:disabled)::after {",
|
|
206
|
+
" content: '';",
|
|
207
|
+
" width: 16px;",
|
|
208
|
+
" height: 16px;",
|
|
209
|
+
" background: currentColor;",
|
|
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;",
|
|
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;",
|
|
212
|
+
"}",
|
|
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). */",
|
|
164
220
|
"[data-mobile-input-toggle],",
|
|
165
221
|
"[data-mobile-input-copy],",
|
|
166
222
|
"[data-mobile-input-diagnostics] {",
|
|
@@ -292,7 +348,8 @@ window.__ModuleLoader__.load({
|
|
|
292
348
|
diagnostics.add("h=" + window.innerHeight
|
|
293
349
|
+ " growth=" + readGrowthMode()
|
|
294
350
|
+ " input=" + String(readOverride())
|
|
295
|
-
+ " 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()));
|
|
296
353
|
ctx.effect(function () {
|
|
297
354
|
return watchKeyboard(diagnostics);
|
|
298
355
|
});
|
|
@@ -336,11 +393,12 @@ window.__ModuleLoader__.load({
|
|
|
336
393
|
}, createNativeInputToggle(diagnostics)));
|
|
337
394
|
}
|
|
338
395
|
|
|
339
|
-
/* 5) No auto-focus on session switch (narrow viewports):
|
|
340
|
-
unlock effect focuses the contenteditable on every mount /
|
|
341
|
-
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
|
|
342
399
|
[locked, sessionId, editor]) — a desktop convenience that pops the
|
|
343
|
-
IME over half the screen on phones
|
|
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
|
|
344
402
|
the composer's editable unless the user just tapped it (or is
|
|
345
403
|
keyboard-navigating); taps reach the box via the normal focus path.
|
|
346
404
|
Question-card textareas (autoFocus on purpose) are not contenteditable
|
|
@@ -369,7 +427,7 @@ window.__ModuleLoader__.load({
|
|
|
369
427
|
var nativeFocus = HTMLElement.prototype.focus;
|
|
370
428
|
HTMLElement.prototype.focus = function (opts) {
|
|
371
429
|
if (
|
|
372
|
-
narrow.matches &&
|
|
430
|
+
(narrow.matches || isTouchDevice()) &&
|
|
373
431
|
this.isContentEditable === true &&
|
|
374
432
|
typeof this.closest === "function" &&
|
|
375
433
|
this.closest("[data-composer-seat]") !== null &&
|
|
@@ -436,6 +494,24 @@ window.__ModuleLoader__.load({
|
|
|
436
494
|
the `dsh-mobile-input` query parameter for one page load. */
|
|
437
495
|
var OVERRIDE_KEY = "dsh-mobile-flow:input";
|
|
438
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
|
+
}
|
|
439
515
|
|
|
440
516
|
function readOverride() {
|
|
441
517
|
try {
|
|
@@ -765,33 +841,23 @@ window.__ModuleLoader__.load({
|
|
|
765
841
|
return media !== null && media.matches === true;
|
|
766
842
|
}
|
|
767
843
|
|
|
768
|
-
/**
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
var
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
var update = function () { setNarrow(media.matches === true); };
|
|
783
|
-
update();
|
|
784
|
-
if (typeof media.addEventListener === "function") media.addEventListener("change", update);
|
|
785
|
-
else if (typeof media.addListener === "function") media.addListener(update);
|
|
786
|
-
return function () {
|
|
787
|
-
if (typeof media.removeEventListener === "function") media.removeEventListener("change", update);
|
|
788
|
-
else if (typeof media.removeListener === "function") media.removeListener(update);
|
|
789
|
-
};
|
|
790
|
-
}, [media]);
|
|
791
|
-
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();
|
|
792
858
|
}
|
|
793
859
|
|
|
794
|
-
/**
|
|
860
|
+
/** Viewport + device flag, live across rotation, resize and override edits. */
|
|
795
861
|
function useTakeoverViewport() {
|
|
796
862
|
var media = React.useMemo(function () {
|
|
797
863
|
try {
|
|
@@ -1072,7 +1138,12 @@ window.__ModuleLoader__.load({
|
|
|
1072
1138
|
var editor = card === null ? null : card.querySelector("[data-composer-input]");
|
|
1073
1139
|
if (editor === null) return;
|
|
1074
1140
|
var placeholder = editor.getAttribute("data-placeholder");
|
|
1075
|
-
|
|
1141
|
+
/* A surface that renders no placeholder still gets an invisible one:
|
|
1142
|
+
the CSS that keeps the send button honest reads the field's value
|
|
1143
|
+
state through :placeholder-shown, which needs the attribute to be
|
|
1144
|
+
there at all. */
|
|
1145
|
+
if (placeholder === null || placeholder === "") placeholder = " ";
|
|
1146
|
+
if (area.placeholder !== placeholder) area.placeholder = placeholder;
|
|
1076
1147
|
/* The product's own gate (locked / inert / takeover states) rides this
|
|
1077
1148
|
attribute; mirror it instead of re-deriving the policy. The takeover
|
|
1078
1149
|
only ever writes "false" AND marks those writes, so an unmarked value
|
|
@@ -1560,6 +1631,18 @@ window.__ModuleLoader__.load({
|
|
|
1560
1631
|
}
|
|
1561
1632
|
return null;
|
|
1562
1633
|
};
|
|
1634
|
+
/** The composer's submit control: the trailing row's LAST primary
|
|
1635
|
+
button (an interruptible child's extra Stop circle renders before
|
|
1636
|
+
it). It is the one control whose gate AND face read the machine
|
|
1637
|
+
draft — exactly the state the takeover keeps stale while typing. */
|
|
1638
|
+
var submitAt = function (button) {
|
|
1639
|
+
if (button === null || typeof button.className !== "string"
|
|
1640
|
+
|| button.className.indexOf("_primary") < 0) return false;
|
|
1641
|
+
var trailing = typeof button.closest === "function" ? button.closest("[class*='_trailing']") : null;
|
|
1642
|
+
if (trailing === null) return false;
|
|
1643
|
+
var primaries = trailing.querySelectorAll("[class*='_primary']");
|
|
1644
|
+
return primaries.length !== 0 && primaries[primaries.length - 1] === button;
|
|
1645
|
+
};
|
|
1563
1646
|
var onPointerDown = function (event) {
|
|
1564
1647
|
var area = areaRef.current;
|
|
1565
1648
|
var wrap = wrapRef.current;
|
|
@@ -1579,17 +1662,24 @@ window.__ModuleLoader__.load({
|
|
|
1579
1662
|
}
|
|
1580
1663
|
return;
|
|
1581
1664
|
}
|
|
1582
|
-
/* Outside the field: a toolbar action
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1665
|
+
/* Outside the field: a toolbar action. Everywhere else the commit is
|
|
1666
|
+
bookkeeping and the gesture runs untouched. The submit button is
|
|
1667
|
+
the exception: its gate AND its face read the machine draft, so
|
|
1668
|
+
while the user types they describe a composer the product has not
|
|
1669
|
+
heard about yet — a grey send button that a tap may never reach,
|
|
1670
|
+
or the running turn's Stop square on a button whose tap means
|
|
1671
|
+
send. Commit first (the submit reads the machine draft), then run
|
|
1672
|
+
the product's own send gesture — the same path Enter takes (slash
|
|
1673
|
+
adjudication, busy policy, steer/queue) — so one tap is one send,
|
|
1674
|
+
whatever the engine would have made of the stale control. */
|
|
1586
1675
|
var button = buttonAt(x, y);
|
|
1587
|
-
|
|
1588
|
-
mirrorNow();
|
|
1589
|
-
if (button !== null && wasDisabled && button.disabled === false) {
|
|
1676
|
+
if (submitAt(button) === true && pendingRef.current !== null) {
|
|
1590
1677
|
event.preventDefault();
|
|
1591
|
-
|
|
1678
|
+
mirrorNow();
|
|
1679
|
+
debug("carry tap -> " + (sendGesture() ? "submit" : "nothing"));
|
|
1680
|
+
return;
|
|
1592
1681
|
}
|
|
1682
|
+
mirrorNow();
|
|
1593
1683
|
};
|
|
1594
1684
|
/* Engines without a working inert: if anything hands focus back to the
|
|
1595
1685
|
stock editor, take it straight back to the field. */
|
|
@@ -1696,17 +1786,19 @@ window.__ModuleLoader__.load({
|
|
|
1696
1786
|
}
|
|
1697
1787
|
|
|
1698
1788
|
/**
|
|
1699
|
-
* Escape hatch in the composer tool row (
|
|
1700
|
-
* swaps the native input back to the stock editor and remembers it,
|
|
1701
|
-
* 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.
|
|
1702
1794
|
* @returns the slot entry component.
|
|
1703
1795
|
*/
|
|
1704
1796
|
function createNativeInputToggle(diagnostics) {
|
|
1705
1797
|
return function NativeInputToggle() {
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
var
|
|
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();
|
|
1710
1802
|
var revealState = React.useState(function () { return readReveal(); });
|
|
1711
1803
|
var revealed = revealState[0];
|
|
1712
1804
|
var setRevealed = revealState[1];
|
|
@@ -1732,7 +1824,6 @@ window.__ModuleLoader__.load({
|
|
|
1732
1824
|
};
|
|
1733
1825
|
React.useEffect(function () {
|
|
1734
1826
|
var update = function () {
|
|
1735
|
-
setPreference(readOverride());
|
|
1736
1827
|
setRevealed(readReveal());
|
|
1737
1828
|
};
|
|
1738
1829
|
window.addEventListener(PREFERENCE_EVENT, update);
|
|
@@ -1745,8 +1836,6 @@ window.__ModuleLoader__.load({
|
|
|
1745
1836
|
endPress();
|
|
1746
1837
|
};
|
|
1747
1838
|
}, []);
|
|
1748
|
-
if (!narrow) return null;
|
|
1749
|
-
var on = preference !== "off";
|
|
1750
1839
|
var label = (on ? "原生输入框:已开启(点击改用官方输入框)" : "原生输入框:已关闭(点击启用)")
|
|
1751
1840
|
+ ";长按显示/隐藏诊断按钮";
|
|
1752
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.
|
|
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,
|
|
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",
|