dsh-mobile-flow 0.8.0 → 0.8.1

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
@@ -30,7 +30,7 @@ On viewports ≤720px (the same breakpoint the official question card uses) the
30
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
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
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.
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), with the tap's own trailing `click` swallowed so the product can never read that same tap as an interrupt (v0.8.1). While typing that state is painted by pure CSS with **zero DOM writes**, so the "no DOM movement while typing" invariant stands.
34
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.
35
35
 
36
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).
@@ -223,6 +223,20 @@ Two fixes:
223
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
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
225
 
226
+ **v0.8.1 (tablet re-check: "sending a message kills the session")**: the report was that sending while a turn streams
227
+ **terminates that turn** — the message lands in the pending queue instead, and 「插话发送」 (steer) is greyed out
228
+ forever (only a *running* turn can be steered), so the only way out was delete-and-resend. The cause sits in the
229
+ v0.7.3 gesture bridge: canceling `pointerdown` does **not** cancel the tap's own `click` (measured 2026-10-12, Chrome
230
+ 152: a trusted `click` on that very button ~100 ms later, when the finger lifts). By then the carried submit has
231
+ emptied the machine draft, so the product reads its own button as the running turn's **Stop** control and calls
232
+ `session/cancel` — the turn the user just sent into dies with `turn/end aborted reason user`, the queued message stays
233
+ behind (`keepInbox`), exactly what the host log showed. The fix records the carried control and **swallows that one
234
+ trailing click** (same button, within 1 s) in the card's capture phase (`preventDefault` + `stopPropagation`), so one
235
+ tap is one send; a click with no carried tap in front of it still reaches the product (tapping the button with an
236
+ empty field remains the stock Stop). Verified by two new jsdom assertions plus the live page at 640×1024 and
237
+ 1024×768: only `session/prompt` is sent, no `session/cancel`, the turn keeps streaming; the empty-field tap still
238
+ sends `session/cancel`.
239
+
226
240
  **Escape hatch**: a small tool-row button (**输入法✓ / 输入法✗**, narrow viewports only) swaps back to the stock
227
241
  input box and remembers the choice — no device can be left stuck.
228
242
 
package/README.zh.md CHANGED
@@ -30,7 +30,7 @@
30
30
  5. **切会话不自动聚焦** —— 官方在每次挂载/切换会话时把焦点还给输入框(桌面便利设计),手机上会弹出输入法占掉半屏,平板上也一样挡事。窄屏与触屏设备上拦截这类程序化聚焦;你主动点输入框时照常聚焦。
31
31
  6. **workspace 行操作按钮常显** —— 分组行与会话行尾部的按钮(⋯ 菜单含重命名/删除、+ 在该 workspace 新建会话)官方仅在悬停时显示;触屏没有悬停,窄屏与触屏设备下侧边栏列表渲染时常显这两个按钮。
32
32
  7. **原生输入框接管** —— 窄屏或触屏设备默认接管(鼠标操作的桌面端不动),输入区绘制成原生 textarea,外观、内边距、字号行高与官方完全一致(几何按官方输入区实时测量对齐)。回车仍是官方发送手势(含 `/` 菜单仲裁、busy-Enter 策略),粘贴图片转发给官方附件入口,锁定态跟随官方可编辑性开关。
33
- 8. **发送按钮跟「输入框里有什么」走** —— 只要输入框里有字,右下角发送按钮就是**可点的亮色**(不再是灰的),点一下即发送(运行中的回合则是排队/引导);打字期间这个状态由纯 CSS 驱动,**零 DOM 写入**,不破坏「打字期零 DOM 变动」这条硬约束。
33
+ 8. **发送按钮跟「输入框里有什么」走** —— 只要输入框里有字,右下角发送按钮就是**可点的亮色**(不再是灰的),点一下即发送(运行中的回合则是排队/引导),并且把这一记 tap 自己的尾随 `click` 吞掉,官方就再也不会把同一次点击读成「打断」(v0.8.1);打字期间这个状态由纯 CSS 驱动,**零 DOM 写入**,不破坏「打字期零 DOM 变动」这条硬约束。
34
34
  9. **开关不分屏幕大小** —— 工具行的「输入法✓/✗」在任何视口都显示(不只在手机上),所以「大屏平板要不要用原生输入框」永远有一键可切的地方;它同时也兼容「本插件没识别出这是触屏设备」的情况。
35
35
 
36
36
  **鼠标操作的桌面端完全不受影响**(触屏设备即使屏幕很宽也会接管——这正是本次改动要的结果)。
@@ -166,6 +166,8 @@ localStorage.removeItem("dsh-mobile-flow:growth"); // 恢复默认
166
166
  - **外观**:机器草稿是旧的,但**字段自己知道有没有字**,而 `:placeholder-shown` 是纯 CSS 里唯一能读字段值的探针。于是接管卡片在「字段有字 + 机器草稿仍为空」时把发送按钮画成启用态(opacity 1、手型光标、按下有反馈)——**零 DOM 写入**,硬约束不破;机器草稿已经同步(如已提交)时不动它,保持官方原样。
167
167
  - **手势**:卡片捕获阶段的 `pointerdown` 判断「点的是不是提交按钮(trailing 行里最后一颗 primary,可中断子会话额外的 Stop 圆钮在它前面)」且「还有未提交的文字」,是则**拿走这次手势**:先落草稿,再走**官方自己的发送手势**(与回车同一条路:斜杠仲裁、busy 策略、排队/引导),一次点击一条消息,不再看引擎脸色。运行中的回合里,字段有字时按钮**换成发送箭头**(隐藏官方的 Stop 方块,用官方同一份箭头 path 画 mask),点它就是发送——脸和手势一致。
168
168
 
169
+ **v0.8.1(平板复验反馈:发一条消息,正在跑的回合被打断)**:现象是「一发送,会话就被终止」——消息进了待发送队列,而「插话发送」永远是灰的(只有**运行中**的回合才能插话),只能删掉那条消息重新发。根因在 v0.7.3 的手势桥接:**取消 `pointerdown` 并不能取消这一记 tap 自己的 `click`**(2026-10-12 在 Chrome 152 上实测:手指抬起后约 100 ms,同一颗按钮上照样派发一次 `isTrusted` 的 click)。那时我们 carry 的那次提交已经把机器草稿清空,官方读到「运行中 + 草稿为空」就把它当成 **Stop 圆钮**,于是发出 `session/cancel` —— 用户刚发进去的那个回合当场被杀(宿主机日志里正是 `turn/end aborted reason user`,且因为 `keepInbox` 排队消息留在队列里),与现象完全对上。修法:carry 成功时记下这颗按钮,紧随其后的那一记 click(同一颗按钮、1 秒内)在卡片捕获阶段 `preventDefault + stopPropagation` 吞掉——一次 tap 就是一次发送;**没有 carry 的 click 照旧放行**(空输入框时点它就是官方的 Stop,不能挡)。验证:jsdom 新增两条断言 + 真实页面探针(竖屏 640×1024、横屏 1024×768 各跑一遍:只发 `session/prompt`、没有 `session/cancel`、回合继续跑;空输入框点击仍发出 `session/cancel`)。
170
+
169
171
  **v0.7.2(手机反馈:新建会话后点输入框没反应)**:新会话刚建出来、**workspace 还没解析出来**时,官方卡片其实是「选择工作区」的**选择器触发器**(`aria-haspopup="menu"`、`contenteditable="false"`、没有绑编辑器),根本不是输入框。旧版接管照样压上去,还把这份「不可编辑」**锁存**成原生 textarea 的 `readOnly` —— 于是:点输入框**不弹键盘、不出现光标**,同时把官方那记「打开选择器」的点击吞掉,用户既不能打字也打不开选择器;而且 workspace 解析好之后它**不会自愈**(只有换会话重新挂载才恢复),正好对上「第一次新建会话没反应、切走再新建就好」的现象(2026-09-19)。
170
172
 
171
173
  处理:
package/lib/client.js CHANGED
@@ -1643,10 +1643,14 @@ window.__ModuleLoader__.load({
1643
1643
  var primaries = trailing.querySelectorAll("[class*='_primary']");
1644
1644
  return primaries.length !== 0 && primaries[primaries.length - 1] === button;
1645
1645
  };
1646
+ /** The tap whose gesture this bridge carried, waiting for the engine's
1647
+ own trailing `click` (see onClickCapture). */
1648
+ var carriedClick = null;
1646
1649
  var onPointerDown = function (event) {
1647
1650
  var area = areaRef.current;
1648
1651
  var wrap = wrapRef.current;
1649
1652
  if (area === null || wrap === null || event.target === area) return;
1653
+ carriedClick = null; /* a new tap supersedes any carry still waiting */
1650
1654
  var x = event.clientX;
1651
1655
  var y = event.clientY;
1652
1656
  debug("tap " + Math.round(x) + "," + Math.round(y) + " target=" + (event.target && event.target.tagName));
@@ -1676,11 +1680,37 @@ window.__ModuleLoader__.load({
1676
1680
  if (submitAt(button) === true && pendingRef.current !== null) {
1677
1681
  event.preventDefault();
1678
1682
  mirrorNow();
1679
- debug("carry tap -> " + (sendGesture() ? "submit" : "nothing"));
1683
+ var carried = sendGesture();
1684
+ debug("carry tap -> " + (carried ? "submit" : "nothing"));
1685
+ /* Canceling `pointerdown` does NOT cancel the tap's own `click`
1686
+ (measured 2026-10-12 on Chrome 152: a trusted click on this very
1687
+ button ~100 ms later, after the finger lifts). By then the
1688
+ carried submit has emptied the machine draft, so the product
1689
+ reads its own button as the running turn's Stop control and
1690
+ cancels the turn the user just sent into — "sending a message
1691
+ kills the session", then the message sits in the queue with
1692
+ steering unavailable (only a running turn can be steered).
1693
+ Swallow that one trailing click. */
1694
+ carriedClick = carried ? { button: button, at: Date.now() } : null;
1680
1695
  return;
1681
1696
  }
1697
+ carriedClick = null;
1682
1698
  mirrorNow();
1683
1699
  };
1700
+ /** The engine's click for a tap this bridge already carried: the same
1701
+ control, moments later, and never a second gesture. */
1702
+ var onClickCapture = function (event) {
1703
+ var carried = carriedClick;
1704
+ carriedClick = null;
1705
+ if (carried === null || Date.now() - carried.at > 1000) return;
1706
+ var target = event.target;
1707
+ var insideButton = carried.button !== null && typeof carried.button.contains === "function"
1708
+ && target !== null && (target === carried.button || carried.button.contains(target) === true);
1709
+ if (insideButton !== true && submitAt(buttonAt(event.clientX, event.clientY)) !== true) return;
1710
+ event.preventDefault();
1711
+ event.stopPropagation();
1712
+ debug("swallow trailing click of a carried tap");
1713
+ };
1684
1714
  /* Engines without a working inert: if anything hands focus back to the
1685
1715
  stock editor, take it straight back to the field. */
1686
1716
  var onFocusIn = function (event) {
@@ -1697,11 +1727,13 @@ window.__ModuleLoader__.load({
1697
1727
  if (document.visibilityState === "hidden") mirrorNow();
1698
1728
  };
1699
1729
  card.addEventListener("pointerdown", onPointerDown, true);
1730
+ card.addEventListener("click", onClickCapture, true);
1700
1731
  document.addEventListener("focusin", onFocusIn, true);
1701
1732
  window.addEventListener("pagehide", onHide);
1702
1733
  document.addEventListener("visibilitychange", onHide);
1703
1734
  return function () {
1704
1735
  card.removeEventListener("pointerdown", onPointerDown, true);
1736
+ card.removeEventListener("click", onClickCapture, true);
1705
1737
  document.removeEventListener("focusin", onFocusIn, true);
1706
1738
  window.removeEventListener("pagehide", onHide);
1707
1739
  document.removeEventListener("visibilitychange", onHide);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-mobile-flow",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
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",