DeepSeek Harness Plugin Hub

发布与管理完整 Harness Profiles,发现适合你的插件。

探索

插件目录环境预设文档中心动态

社区

发布插件联系我们报告问题

相关链接

Plugin Hub GitHubDeepSeek Harness 官方项目系统状态隐私说明
© 2026 DeepSeek Harness Plugin HubPowered byPaxTech

独立、非官方社区项目,与 DeepSeek 官方无隶属、授权或背书关系。

DeepSeek Harness Plugin Hub
ProfilesPlugins分类动态文档登录管理 Profiles
ProfilesPlugins分类动态文档登录
← Plugins

dsh-mobile-flow

Mobile Flow

DeepSeek Harness Web UI 的移动端修复:在窄屏(≤720px)上,输入栏和 AI 确认卡片会随页面滚动,而不是固定在视口底部,边缘变窄,并且触摸设备(任何宽度,包括平板)不再自动

插件会安装到这里;不确定时保持 web。

npx -y @deepseek-ai/dsh plugin --profile web add dsh-mobile-flow@0.8.2
README兼容性版本

兼容性与来源证明

Mobile Flow 以 dsh-mobile-flow 发布,当前版本为 0.8.2。Plugin Hub 会校验它的 manifest,并保存精确安装来源,便于复现安装结果。

DSH 兼容范围
*
运行环境
web
发布来源
npm
Registry 更新时间
2026/11/5

版本

0.8.2stable
2026/11/5
0.8.1stable
2026/10/12
0.8.0stable
2026/10/6
查看其余 10 个版本收起版本
0.7.3stable
2026/9/21
0.7.2stable
2026/9/19
0.7.1stable
2026/9/14
0.4.1stable
2026/9/5
0.4.0stable
2026/9/5
0.3.0stable
2026/9/5
0.2.0stable
2026/9/5
0.1.2stable
2026/9/4
0.1.1stable
2026/9/4
0.1.0stable
2026/9/4

相关插件

正在加载相关插件…

最新版
0.8.2
DSH
*
HMR
重启进程
Tree shaking
未声明可安全裁剪
解包体积
168.4 kB
文件数
7
Surface
web
许可证
MIT
发布源
npm
GitHub
★ 0
周下载
43
最近提交
2026/9/5
查看源码 ↗项目主页 ↗
README Badge

点击下方 Badge 复制 Markdown,粘贴到 README 即可。

这是你的 Plugin?认领权益 · 优先安全扫描

验证 package.json 声明的 GitHub 仓库,即可管理这个公开页面。认领后,Hub 会优先安排当前版本的安全扫描,并在通过后公开展示结果。

认领这个 Plugin →
报告问题

相关插件

继续浏览 ui-customization 分类下经过校验的插件。

Web App@deepseek-ai/dsh-web-appdsh 浏览器界面捆绑包:位于 dsh-base 之上的 Web 补丁层,加上运行时粘合插件(提供前端 dist、Web 界面提示符、bash 运行时变量和 URL 行)Experimental Session Titles Bundle@deepseek-ai/dsh-experimental-session-titles-bundle新的用户提示到达时刷新会话标题。Personalized Recommendations@deepseek-ai/dsh-personalized-recommendations回答下方以卡片形式提供个性化页面和内置插件推荐Whale Widgetdsh-whale-widgetDSH Web 界面右下角的 DeepSeek 余额小鲸鱼挂件:余额/今日已用/峰谷定价、自定义泡泡点击序列(文本/余额/今日/峰谷/图片/随机语句与并列加权选择)、逐行样式与字体、悬浮快捷编辑、音效与每轮消耗、自定义角色/动图/音效、吸附与翻转自定义

README

dsh-mobile-flow

English | 简体中文

Two mobile fixes for the DeepSeek Harness (dsh) Web UI:

  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.
  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).

A client-side overlay plus one slot component. No product source is modified; removing the plugin restores the stock behavior exactly.

The problems

1. The composer is pinned to the bottom of the screen

The stock Web UI pins the composer seat with position: sticky; bottom: 0. That seat hosts not only the input bar but also the interactive cards the AI raises mid-conversation — ask_user_question option cards, approval prompts, plan review. On a phone all of them stay glued to the bottom of the screen, permanently eating a chunk of the viewport while you scroll back through replies.

2. Mobile IMEs clear text you already typed

The stock composer's text surface is a Lexical contenteditable (createEditor + registerPlainText in packages/client/ui-conversation). Android IMEs — voice keyboards above all — drive composition through Chrome's InputConnection → beforeinput("insertCompositionText") recomposition flow, and a framework editor answers those events by writing its own state back into the DOM. The text being composed, and on recomposition text already committed, gets wiped. A native <textarea> is edited by the platform itself and never enters that fight, which is why the same IME behaves in an ordinary textarea comment box.

What it does

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):

  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.
  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.
  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.
  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.
  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.
  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.
  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.
  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.
  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.

A mouse-driven desktop is completely unaffected (a touch device is taken over however wide it is — that is the point of this change).

How it works

The plugin ships a browser half (exports["./client"], declared via dsh.client.platform: "web"), discovered by the client-modules scanner and loaded from the boot manifest.

  • Layout half: one <style> tag with @media (max-width: 720px) overrides, removed on unload — fully reversible. All selectors target the product's stable data-* attributes (data-composer-seat, data-conversation-scroll, data-phase, data-slot), never CSS-Modules-hashed class names. The sticky-to-static switch is guarded with :not(:has([data-conversation-composer-overlay])) so views that own their composer overlay (e.g. trajectory) keep the official absolute positioning. On engines without :has() the rules degrade safely back to the stock sticky behavior.
  • Input half: registered into the official conversation.input.overlay slot (a session-scope seat inside the composer card) with the standard props it carries (useInput / inputActions). Every keystroke is mirrored into the official input machine (conversation.input.for(ctx).setDraft), while the read side subscribes to the same store's live snapshot — so the send button, placeholder, / trigger pipeline, attachments and draft persistence all keep working, with no stale-render race to clobber fast typing. The Lexical editor keeps its layout box (hidden, never removed) because that box is what sizes the card for the mirrored draft.
  • Scope: the takeover owns the surface in plain and sending. sending is the 0.2.2 phase where a just-sent draft stays in a read-only editor until the Host accepts it; keeping the takeover there (with the field read-only, the draft clearing only on acceptance) is what stops every send from unmounting the textarea and dropping focus to <body> — otherwise each message costs a fresh tap. A claim (a / command picked from the menu, adjudicating, submitting) hands the surface straight back to the stock editor — command tokens and chips are state a plain-text mirror cannot reproduce.

Requirements

  • DeepSeek Harness Web profile (dsh web), any recent 0.1.x release
  • Selectors and slot contracts verified against 0.1.2-rc.1, 0.1.5-rc.1, 0.2.0-rc.2 and 0.2.2-rc.1 (0.2.0 on 2026-10-04, 0.2.2 on 2026-11-05, both by a static audit of the shipped client bundles plus source); they target product slot contracts that are stable within a version line but may need small updates after a major product revamp

Install

From npm

dsh plugin --profile web add dsh-mobile-flow

From GitHub

dsh plugin --profile web add github:imroc/dsh-mobile-flow

Restart dsh web, then refresh the browser page.

Overrides (optional)

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:

// Browser console, same origin, persists
localStorage.setItem("dsh-mobile-flow:input", "off");    // never take over
localStorage.setItem("dsh-mobile-flow:input", "on");     // always take over (any viewport, desktop included)
localStorage.setItem("dsh-mobile-flow:input", "force");  // same as "on" (what the URL flag =1 writes)
localStorage.removeItem("dsh-mobile-flow:input");        // back to default: narrow or touch

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".

One-shot override: ?dsh-mobile-input=1 (force) / =0 (off).

Diagnostics have their own persisted switch (the tool-row 诊断 button writes it; no URL typing needed):

localStorage.setItem("dsh-mobile-flow:diagnostics", "bench");  // device test bench
localStorage.setItem("dsh-mobile-flow:diagnostics", "debug");  // event panel
localStorage.removeItem("dsh-mobile-flow:diagnostics");        // off

Growth policy (commit by default: resize at commit points only):

localStorage.setItem("dsh-mobile-flow:growth", "live");  // per-keystroke growth (v0.5.2 behaviour, for A/B)
localStorage.setItem("dsh-mobile-flow:growth", "none");  // always the floor height
localStorage.removeItem("dsh-mobile-flow:growth");       // back to default

Verify

Open a session on a phone (or a desktop DevTools window narrowed to ≤720px) — a tablet works the same at any width:

  • Swipe up a few screens: the input bar and confirmation cards scroll away with the messages.
  • 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.
  • Send with the send button or Enter (same gesture as the stock composer).
  • 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.
  • The tool row shows one chip, 「输入法✓」(the escape hatch); a 600 ms long press on it calls out the 「诊断」 chip.
  • 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.
  • The v0.6.0 regression to watch: no layout movement while typing — the field's height stays put until focus leaves, and only then follows the content.
  • 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).

Update discipline (this plugin's convention)

  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.
  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.

Compatibility hardening (v0.5.1 / v0.5.2 / v0.6.0, after HarmonyOS / ArkWeb feedback)

The first release broke on HarmonyOS 7's built-in browser (ArkWeb): taps rarely opened the keyboard, and only one character could be typed. Both are addressed:

  1. Taps not reaching the field — a paint/hit-order bug: the stock input row lives in .grow (position: relative) which comes after our overlay seat in DOM order, so at equal stacking level it painted (and hit-tested) above the textarea; a document.elementFromPoint probe hit the stock container at 3 of 5 sample points inside the input box. Fixed with z-index: 5 + an opaque seat background, pointer-events: none on the hidden row, and a capture-phase pointerdown on the card that focuses the field for taps anywhere inside it.
  2. One character then nothing — two defences: the DSH-side draft is never written back into a focused field (that write resets an IME session; only a committed-send clear is applied), and the stock editor is taken out of the focus/IME candidate tree (inert + aria-hidden, with a focusin guard for engines without inert); geometry writes are idempotent and the field declares user-select: text / touch-action: manipulation.

v0.5.2 (second round of HarmonyOS feedback): with the takeover on, the keyboard closed after every character. Cause: mirroring the draft on every keystroke re-rendered the composer card and made Lexical rewrite the hidden editor's DOM — a strict engine drops the IME when the editing surface churns. Now:

  • zero DOM churn while typing: the field owns its text and only commits to the machine at commit points — Enter, blur, any tap outside the field (toolbar/send), page hide, unmount — plus immediately when a / or @ trigger character is typed (the menus need the machine).
  • the field grows itself: the textarea autosizes (cap from the product's own 14-line token, floor read from the stock min-height: 36px docked / 52px hero) and a matching min-height floor is put on the stock row's box, because the absolutely positioned seat cannot make the card grow; it shrinks back after a send clears the draft.
  • send-button bridge: the stock send button is disabled while the machine draft is empty, so a tap outside the field commits first and, if that commit enabled the button, the tap is replayed onto it (one tap = send).
  • the stock editor is additionally forced to contenteditable="false" next to inert + aria-hidden, so the IME can only ever target the textarea; while the field is focused the seat never moves, the hidden scroller is left alone, and geometry is rounded to whole pixels (no sub-pixel style churn).

v0.6.0 (third round of HarmonyOS feedback, current): taking the mirror off the typing path was still not enough — the per-keystroke geometry writes were the trigger. autosize set the field to height: 0px on every key, read scrollHeight, wrote the height back (the focused editable collapsed once per keystroke), and the seat re-aligned itself to the card's box. The rule is now absolute: while the field has focus the plugin performs zero DOM writes.

  • one fixed height while typing: the field keeps the product's own floor (36px docked / 52px blank-session hero) and scrolls internally;
  • only commit points touch the DOM: blur, Enter, page hide, unmount, phase flips — that is when measurement, re-alignment and chrome syncing (placeholder, read-only) happen. Measurement is non-destructive (the height: 0px probe is gone); after blur the field grows to its content, keeps the stock row's min-height in step, and shrinks back when a send clears the draft (the committed send is the only write performed on a focused field — it follows an explicit user action);
  • uncommitted text can no longer be wiped by an empty draft: the machine draft is stale by design while typing, so an empty publish is no longer read as "the user cleared the field";
  • the diagnostics panel stops perturbing what it measures: lines land in a memory buffer and are painted only on blur, on demand, or on a keyboard transition — the old panel wrote the DOM on every event, making it a suspect in the very bug it reported;
  • keyboard transitions become facts: window / visualViewport height changes (how ArkWeb reports the on-screen keyboard) are timestamped next to the input events.

v0.6.1 (fourth round, the slash case): on the device only ONE case still dropped the keyboard — a draft starting with / (how skills are invoked by hand); plain text was already fine. The cause was the plugin's single per-keystroke exception: TRIGGER.test(fieldValue) asked "does the text contain /", so any draft containing a slash mirrored the machine on every keystroke after it — re-rendering the composer card, refreshing the command menu and rewriting the hidden editor's DOM once per character. Now:

  • only the trigger character itself is mirrored (typed or deleted), because the menu needs the machine; the characters typed after it stay in the field until a commit point;
  • nothing is mirrored mid-composition (the trigger count captured at compositionstart decides at the end);
  • the diagnostics panel gained 「触发符:仅触发 / 逐键 / 关」 switches to A/B this on the device (the old per-keystroke behaviour is one tap away, which is also how the cause was confirmed);
  • the tool row gained a 「复制日志」 chip whenever diagnostics are on, and the log now records CHURN while focused lines — app-side mutations (menu, Lexical) under a live IME are logged too, so "our writes" and "the app re-rendering" can be told apart from a log alone.

Known trade-off: the slash menu still opens, but its list may not filter live while you type (the filter reads the machine draft, which is deliberately stale until a commit point). Switch to 「触发符:逐键」 for live filtering, at the cost of the keyboard dropping on that device.

v0.7.0 (fifth round, the slash moment; ✅ confirmed on a HarmonyOS 7 ArkWeb device: plain typing, / commands and continuous input all behave): on the device only a SINGLE keyboard drop remained — the one at the moment / is typed (everything after it is fine). The device log plus the stock-editor control (typing / there keeps the keyboard) narrowed the mechanism to one thing: republishing the draft makes the app rewrite the hidden stock editor's DOM asynchronously (EDITOR childList +1, outside our gesture), and ArkWeb drops the IME when an editable surface changes from outside the input method. In the stock path that same DOM change IS the user's own editing, inside the gesture, so nothing drops.

The trigger mirror is now drop focus → land the write → take focus back, entirely inside the keystroke's gesture: blur() first (IME down), then flushSync (react-dom) so the re-render the write causes — editor rewrite and menu mount — completes with nothing focused, then focus() to bring the IME back, still inside the gesture.

  • the panel gained 「触发:重聚焦 (default) / 仅触发 (v0.6.1) / 逐键 / 关」 to A/B the strategies in one tap;
  • a hidden slash-flow defect is fixed too: picking a command from the menu now lands in the field (the old "focused field ignores external writes" guard swallowed it; it now only protects UNCOMMITTED text).

v0.7.1: the tool row stays clean — everyday it carries only the escape hatch (输入法✓ / 输入法✗); long-press it for 600ms to call out the 诊断 (diagnostics) button, long-press again to put it away. While diagnostics are on, both 诊断 and 复制日志 (copy log) show up by themselves.

v0.7.2 (phone report: tapping the input box after "new session" did nothing): right after a new session is created its workspace may not be resolved yet — in that window the product's card is not an input at all but the workspace picker trigger (aria-haspopup="menu", contenteditable="false", no bound editor). The takeover used to claim it anyway and latched that non-editability into the native textarea's readOnly: tapping the box raised no keyboard and no caret, the card's own "open the picker" tap was swallowed, so the user could neither type nor pick a workspace — and it did not recover once the workspace resolved (only a session switch, i.e. a remount, did). That is exactly the reported "first new session dead, switch away and create another and it works" pattern.

Fixes:

  • The takeover now asks the product whether the composer is a real text input before owning it, using two product-owned attributes it never writes itself: aria-haspopup="menu" (picker trigger) and aria-disabled="true" (blocked composer / removed session / offline parent). When it is not an input, the takeover stays completely out of the way: no card marker, no seat, no swallowed tap — the product's own picker tap works.
  • That question is answered live (MutationObserver on those two attributes): the moment the workspace resolves and the composer becomes a real input, the takeover attaches — no latch.
  • Belt and braces: the takeover only ever writes contenteditable="false", so any observed "true" is the product making its composer editable again — the field's readOnly is cleared then, so a latched field can never outlive the state that caused it.

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.

Two fixes:

  • 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.
  • 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.

v0.8.1 (tablet re-check: "sending a message kills the session"): the report was that sending while a turn streams terminates that turn — the message lands in the pending queue instead, and 「插话发送」 (steer) is greyed out forever (only a running turn can be steered), so the only way out was delete-and-resend. The cause sits in the v0.7.3 gesture bridge: canceling pointerdown does not cancel the tap's own click (measured 2026-10-12, Chrome 152: a trusted click on that very button ~100 ms later, when the finger lifts). By then the carried submit has emptied the machine draft, so the product reads its own button as the running turn's Stop control and calls session/cancel — the turn the user just sent into dies with turn/end aborted reason user, the queued message stays behind (keepInbox), exactly what the host log showed. The fix records the carried control and swallows that one trailing click (same button, within 1 s) in the card's capture phase (preventDefault + stopPropagation), so one tap is one send; a click with no carried tap in front of it still reaches the product (tapping the button with an empty field remains the stock Stop). Verified by two new jsdom assertions plus the live page at 640×1024 and 1024×768: only session/prompt is sent, no session/cancel, the turn keeps streaming; the empty-field tap still sends session/cancel.

v0.8.2 (0.2.2-rc.1 compatibility): 0.2.2 adds a sending phase to the input machine — a just-sent draft stays in a read-only editor until the Host accepts it, where 0.2.1 returns to plain immediately. The old "own the surface only while plain" rule therefore turned every send into a release-and-retake: the textarea unmounted, focus fell to <body>, and each message cost a fresh tap (not true on 0.2.1). It is also exactly the input-surface hand-off between our textarea and the stock Lexical editor that ArkWeb handles worst. Changes:

  • The takeover now spans sending: the textarea stays mounted and keeps focus, readOnly goes on while the draft waits, and acceptance clears it and restores editing — the product's own new semantics; 0.2.1 never enters that phase, so nothing changes there;
  • Enter is not replayed during sending (the product ignores it in that phase; a replay only risked clearing the uncommitted mirror);
  • The "send button looks enabled" rule gained :not([data-sending]), so the progress face 0.2.2 paints while it waits for the Host is never overwritten with our enabled-looking arrow (the attribute is absent on 0.2.1, making the guard a no-op);
  • Uncommitted text can no longer be swallowed by a refused draft write: mirrorNow() now releases the pending value after a successful push (it used to release first, so a refused write let the next sync overwrite the field with an empty machine draft);
  • A missing input no longer defaults to plain (that default happened to be the permissive one, which would leave the takeover released only by luck); it now means "do not take over".

Known difference left unfixed: on hover-less devices 0.2.2 deliberately hides the row actions of a collapsed project row (@media (hover: none) plus [aria-expanded='false']), while our "always show row actions on touch" rule still surfaces them. That rule is required on 0.2.1, and aria-expanded exists in both versions, so there is no safe way to tell the hosts apart — left as is (symptom: one extra action button on a collapsed project row).

Not verified on hardware: keeping the takeover through sending removes the per-send input-surface hand-off (the ArkWeb risk), but it also means the product's own "dismiss the keyboard after sending" still cannot run under the takeover (it needs the Lexical root to own focus, and the takeover makes that root inert) — whether the IME collapses after a send is up to the engine and needs a HarmonyOS device to confirm.

Escape hatch: a small tool-row button (输入法✓ / 输入法✗, narrow viewports only) swaps back to the stock input box and remembers the choice — no device can be left stuck.

Diagnostics (tool-row 诊断 button, or URL ?dsh-mobile-input=bench / ,debug):

  • ⚠️ a tokenized URL has its query rewritten away by the shell (it is gone before plugins apply), so on a phone use the tool-row button: it persists the request in localStorage and reloads. The URL parameter works for cookie-authenticated (token-less) loads.
  • the panel only occupies the top of the screen (variants collapsed behind 「展开变体 A-E」), so the real composer field below it stays typeable — test that first; expand the variants to localise the culprit — A bare textarea (normal flow, no JS writes), B textarea inside a height:0 absolute container (the seat's shape), C B plus a style write per keystroke (the pre-0.6 autosize, the control), D B with logging only (the 0.6 behaviour), E bare textarea inside an iframe (a clean document). The variant followed by a KEYBOARD ...px line is the guilty one;
  • three switches (生产: commit / live / none) A/B the production field's growth policy on the device itself;
  • the debug panel records the composer's tap coordinates and hit target, focus changes, input/composition events and geometry writes. Both panels copy their log with one tap, and neither repaints while a field has focus (a panel write would pollute the very evidence it collects).

Verified on a live page: test/probe-live.mjs drives the real app over CDP (390x844) and asserts, among other things, that typing writes nothing to the page DOM.

Known limits

  • 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.
  • 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.
  • IME candidate/assist behavior itself belongs to the system keyboard and is outside the plugin's control.
  • 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.

Development

npm install --no-save jsdom react@18 react-dom@18
node test/takeover.test.mjs      # runs the real client bundle in a jsdom composer card
node test/probe-live.mjs <token> # live app: takeover contract + diagnostics switches (CDP, 390x844)
node test/probe-send.mjs <token> # live app: the send button's look, one-tap send, the running turn

The launch token comes from journalctl --user -u dsh-web | grep -o 'token=[A-Za-z0-9_-]*' | tail -1 (a new one per restart; the probes spend it on their first navigation).

The test boots the shipped lib/client.js, renders the slot entry through React against a composer-card DOM, and asserts the render path, the draft mirror (typing / IME composition / machine-side writes), the Enter gesture, and the phase gate that hands the surface back to the stock editor. It exists because a broken entry renders nothing visible — only a console error.

Rollback

  • Bundle install: dsh plugin --profile web remove dsh-mobile-flow
  • Manual: remove the dependency and the dsh.profile.bundles entry, restart dsh web
  • Temporarily: localStorage.setItem("dsh-mobile-flow:input", "off") and refresh (turns off the native input only; the layout fixes stay)

No product source is modified; upgrades do not overwrite it.

License

MIT