DeepSeek Harness Plugin

TZHR-invest/dsh-plugins#dsh-mobile-ui

Stars ★ 3 Category UI Enhancements Added 2026-08-15

Mobile UI for the Web GUI on narrow screens: full-width responsive layout, overlay session drawer, 44px touch targets, safe-area support, and reading enhancements with zero desktop impact.

Install

# from GitHub (first run asks for allowBuilds approval — follow the hint, retry)

dsh plugin --profile web add github:TZHR-invest/dsh-plugins#path:/packages/dsh-mobile-ui

Any plugin you install runs third-party code with your own permissions — it can read your files, use your credentials, and reach the network, and tool approvals don’t sandbox it. GitHub-sourced plugins also run build scripts at install time — pnpm blocks those until you allow them, so an install can stop with ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED or ERR_PNPM_IGNORED_BUILDS; dsh prints the exact key to add under allowBuilds in your profile’s pnpm-workspace.yaml, and the install works on the next run. Allowing a build is a trust decision: only install sources you trust, and pin a commit (github:owner/repo#sha).

README

Mobile UI enhancements for the DeepSeek Harness Web GUI — turns dsh into a real mobile experience on phones (≤768px viewport):

  • Responsive layout — hides the left icon rail, content goes full-width; session list opens as an overlay drawer
  • Touch-optimized — 44px touch targets (iOS HIG), no double-tap zoom delay, no tap highlight, 16px inputs (prevents iOS focus auto-zoom)
  • Reading enhancements — message spacing, bubble width, and helper text (tool rows / context-injection / status stats) scaled for small screens
  • Right-top menu — the drawer (new session / sessions / settings) opens from a floating round button
  • Safe-area aware — env(safe-area-inset-*) support for notched displays
  • Zero desktop impact — every enhancement lives inside a max-width: 768px media query

Install

# from the plugin source directory
bash install.sh            # install (idempotent)
bash install.sh --restart  # install + restart dsh web (headless smoke test; aborts on plugin problems)
bash install.sh --uninstall  # remove completely

Or via npm:

dsh plugin --profile web add dsh-mobile-ui

What the installer does:

  1. Source kept at ~/.dsh/plugins/dsh-mobile-ui/
  2. Runtime copy at ~/.dsh/profiles/node_modules/dsh-mobile-ui/
  3. Web profile bundle wiring (cordis.patch.yml insert)

Development

bash scripts/build.sh     # syntax + contract preflight (incl. classic-script check)
bash scripts/package.sh   # build dist/dsh-mobile-ui-install.tar.gz

Fast local iteration: copy to ~/.dsh/profiles/node_modules/dsh-mobile-ui/ and refresh the page (client plugins have an HMR channel — client.js edits apply on refresh).

How it works

  • CSS layer: injects style[data-plugin-css=dsh-mobile-ui], all rules wrapped in @media (max-width:768px) — desktop loads none of it
  • JS layer: matchMedia-driven; on narrow screens the main grid goes single-column, the sidebar is hidden and becomes a fixed overlay drawer with scrim
  • QA card (ask_user_question, Mbwy4a component): JS detects the card and turns its composer seat into a full-height floating panel — no :has() dependency (works in WeChat/X5-style engines). The card's own max-height is lifted and its body stays the single scroll container, so long option lists scroll with a finger on every device; the footer button row wraps when narrow, so the submit button is always visible
  • Composer action row = two lines (2026-09-11, settled over two rounds of user feedback): line 1 holds the tools (+ / attach / access mode) then the context ring and send pushed to its right edge; line 2 is the model trigger alone, spanning the full width. ⚠️ The first two-line attempt (tools alone on line 1, model + context + send on line 2) was wrong: it left 183px of dead space at the end of line 1 (measured at 412px) and the user immediately spotted it as "an empty patch up there". Lifting the context ring and send onto line 1 fills it (183px → 8px). Implemented with display:contents on the trailing group so its children become direct flex items of the row, then order for visual sequence and margin-left:auto to push the last two right — pure CSS, no DOM moves (relocating React nodes throws removeChild on re-render). Two lines are required, not cosmetic: the full model name (commandcode/deepseek/deepseek-v4.1-flash = 40 chars ≈ 225px) physically cannot fit on one line inside a 390px viewport — the row has 332px of content width and the tools need 120px, leaving the model trigger ~61px (10/40 chars, measured). Wrapping lets it take 218px at the same viewport (38/40 chars) and the full name from 400px up, at the cost of row height 52 → 90px. The access-mode button always shows its shield icon plus its text (@container (min-width: 170px), judged on the row's real width); a clipped model name always uses a real ellipsis.
  • Composer row visual consistency (2026-09-11, from the user's "the heights don't match, it looks off" report): upstream ships six different control heights (add 28 / PermissionSelect 28 / model 28 / ContextMeter 28 / primary 34) and our own earlier patches had left a 36/40/44 mix with three font sizes (model 11px / access 13px / context 10px). Everything is now 44px tall with a 12px font (which also satisfies the iOS HIG minimum touch size — measured: +/attach/access were only 36px), the primary button's upstream translateY(-2px) is cancelled (a single-line-era optical compensation that pushed its centre 2px below its row), and the access-mode button gets the same round solid background as +/attach (upstream renders it as a background-less compact trigger, which looked like a missing piece between two round buttons). Invariants: exactly two lines; within a row every button is the same height, the same font size, and their vertical centres spread ≤1.5px; and no line may end in more than 60px of dead space (the quantitative guard for that "empty patch"). ⚠️ One more subtlevty: the UA default for <button> is text-align:center — once the model name spans a full line, centring makes the text start at x=69px instead of aligning with the + button above, so text-align:left must be explicit (the old layout never exposed this because the label's width exactly equalled its text width). ⚠️ This surfaced a fourth word-root leak: the message flow contains no [class*=tools] at all (only xzv4MW_actions / TS9iAW_actions), so the old [class*=scrollBody] [class*=tools] button{min-height:36px} was really hitting the composer's uV2eYG_tools and crushing +/attach/access to 36px — the actual cause of "row 1 is 36px while row 2 is 40/44px". It is now scoped to [class*=flowItem], so message-flow buttons stay 36px and composer controls 44px. Never fake a single line with "no-wrap + shrink": upstream group buttons carry a non-compressible min-width: 44px, so a narrow row cannot resolve by shrinking and simply overlaps (the original bug: at 390px the model trigger covered the context ring by 4.3px and the access-mode button covered it by 5.7px).
  • Regression probe (repo checkout only — the npm package ships just the runtime bundle): python3 tests/mobile-layout-probe.py drives a real headless Chromium (upstream CSS + real DOM nesting + the actual plugin bundle) across viewports, asserting two families of invariants: for the QA card — "option list scrolls / last option reachable after a real touch swipe / submit always visible / the fixed top-right menu button does not overlap the card"; for the composer row — "no two controls overlap / nothing spills past the card's right edge / every control is hit-testable / the access-mode icon and its text are always visible / the model name keeps at least 120px so it never degrades back into a sliver / a clipped model name uses a real ellipsis / icon, text and chevron are vertically centred". Exit code 1 on regression.
  • ⚠️ The most dangerous pitfall in this plugin: word-root selectors (three separate silent breakages on 2026-09-11). Upstream's _7KE1Ra_triggerLabel / triggerIcon / triggerEffort all contain "trigger", so a bare [class*=trigger] hits them too: with display:flex the text-overflow:ellipsis is ignored (flex containers ignore it) and the model name is cut mid-character; with min-height:40px the label is stretched to full row height and its text hugs the top, so the chevron looks like it dropped to another line. Always narrow to button[class*=trigger], and keep ellipsis on display:block. Likewise button[aria-label*=访问模式] span { display:none } hides the shield icon as well (it is a span too), leaving a completely blank button — hide only [class*=triggerLabel] and keep the icon permanently visible. Two more rules it guards: never override the card body's overflow (that is exactly what broke scrolling before), and hide the floating menu button whenever the question card is open (it sits at top:48px; right:12px, right on top of the card title). Related: a min-width floor on the trailing group's content is harmful — flex:1 1 0 + min-width:0 + justify-content:flex-end spills children leftward over the tools group once the box is narrower than its content minimum (measured: 9px at 365px viewport with min-width:96px), so express "the model name should stay readable" some other way.

Maintenance notes (important)

  • dsh frontend class names are build artifacts (hash prefixes). After a dsh upgrade, if selectors break:
    1. Verify body.dsh-mobile-ui and #dsh-mobile-menu-btn exist in the console
    2. Breakage usually hits layout classes ([class*=frame] etc.) — adjust CSS/JS to the new prefix
    3. Re-run bash install.sh after fixing
  • Layout detection uses structural heuristics ("3-column grid + 56px first column"), not hard-coded class names, and buttons prefer aria-label — this tolerates class-name drift
  • All DOM work is try/catch wrapped; any failure degrades silently (CSS layer still applies)
  • Never move React-rendered DOM nodes with JS (insertBefore/appendChild on pXSMma_root etc.): React's fiber tree still records the old parent, so a re-render throws removeChild NotFoundError and the whole conversation view unmounts (blank page). Express every layout need in CSS (flex / order / :has) — the hero title pinning and the QA card are pure CSS for this reason.

Rollback

bash install.sh --uninstall + restart dsh removes everything (no residual styles/DOM).

License

MIT. Chinese documentation: README.zh.md


Part of dsh-plugins — a small monorepo of DSH plugins: dsh-lan-gateway, dsh-vision-tool, dsh-mobile-ui.

Content from the project README on GitHub ↗

Links

More in this category

View the whole category →

Community comments

Comments are public GitHub Discussions. Loading them connects to GitHub and Giscus; a GitHub account is required to post.