A mobile-first UI optimization plugin for DeepSeek Harness (client-only).
> One package, zero config: a from-scratch mobile UI layer for the DeepSeek Harness web shell — responsive drawer layout, font/spacing adaptation, ≥44px touch targets, mobile interactions for buttons/menus/tables, and safe-area handling. **Desktop rendering is untouched.** ## What it is The DeepSeek Harness web UI is designed for a desktop three-column layout. On phone widths it suffers from: the sidebar squeezing the conversation, oversized 16px text, undersized touch targets, wide tables/code blocks pushing a horizontal scrollbar onto the viewport, and a two-column settings dialog with dead space. `dsh-mobile-ux` is a **client-only plugin** (zero host logic). On narrow screens (`max-width: 1023px`, matching the shell's `SIDEBAR_AUTO_COLLAPSE`) it injects a mobile UI layer; on wide screens (`≥1024px`) every mobile control is hidden, leaving desktop untouched. It borrows the single-package client-plugin layout from [dsh-pocket](https://github.com/shaobeichen/dsh-pocket), but the mobile UI layer is **implemented from scratch** (no `dsh-web-mobile` dependency). Use it standalone, or alongside `dsh-pocket` — the latter keeps providing QR-code remote access, this plugin only makes the phone experience better. ## Install **Prerequisite**: [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (`dsh` CLI) installed. ```sh # Install from GitHub dsh plugin --profile web add "github:wangyuanchuan2022/dsh-mobile-ux" -w npx @deepseek-ai/dsh web # restart ``` > Local dev/trial (no network) can link the local path instead: > > ```sh > dsh plugin --profile web add "link:/absolute/path/dsh-mobile-ux" -w > ``` > > Note 1: Phone remote access is provided by [dsh-pocket](https://github.com/shaobeichen/dsh-pocket) (LAN/public QR); this plugin only handles mobile UX. > > Note 2: `dsh-pocket` ships its own mobile drawer layer; the two are interchangeable and should **not** be enabled together (two sets of mobile controls). If you already have `dsh-pocket` and want only this plugin's mobile UI, either `dsh plugin --profile web remove dsh-pocket -w` (losing QR remote access) or accept coexistence. ## ✨ Optimization points All rules apply only on narrow screens (`max-width: 1023px`); on wide screens (`min-width: 1024px`) mobile controls are fully hidden. | # | Optimization | How | Effect | |---|---|---|---| | 1 | **Responsive drawer layout** | Mark AppFrame with `data-mobile-ux="frame"`; re-grid to `minmax(0,1fr) 0 0`, pull the center column back to track 1; make the sidebar an off-canvas drawer (`translateX(-105%)` closed / `transform:none` open, `max-width:84vw`) | Sidebar stops squeezing the conversation; tap-outside / tap-nav closes the drawer | | 2 | **Font & spacing adaptation** | Body 15px, gutters 32→16px, remove desktop `scrollbar-gutter`/scrollbar | More text per screen, column centered | | 3 | **Touch targets ≥44px** | FAB 44×44px; header toggle 28px visual + `::after{inset:-8px}` → 44px hit area; settings close button 40px; appearance cards `min-height:44px` | Meets mobile usability baseline, fewer mis-taps | | 4 | **Header/menu mobile interactions** | Inject a directory toggle into `conversation.session.header.actions`; inject a floating button (FAB) into `shell.overlay` for hero/blank states; backdrop is `pointer-events:none` (pure dimming), close via document-level outside-tap | One-tap directory access with or without a session; backdrop never swallows taps | | 5 | **Settings dialog → sheet** | Two-column dialog becomes a near-full-width sheet (`left:8px; width:calc(100vw - 16px)`); nav tabs grid 3-col; option rows stacked; appearance cards horizontal; toolbar spread | No dead space / no horizontal overflow; measured 374px wide at 390px viewport | | 6 | **Wide-content overflow guard (tables/code)** | `table`/`pre` get `max-width:100%; overflow-x:auto`; message action rows `overflow:hidden` + ellipsis | Wide tables/code scroll inside their container, never the viewport | | 7 | **Stats line horizontal scroll** | Text-anchored mark `data-mobile-ux="stats"`; fixed 28px height, horizontally scrolling single line (TPS folded in) | Every metric reachable by swipe, no vertical growth | | 8 | **Composer polish** | Model pill shrinkable, permission pill keeps natural width; hero empty textarea collapses to one line | No overflow/overlap in the bottom row; tighter hero | | 9 | **Session header layout** | `[toggle][title][mode badge]` row; session-log capsule relocated to the drawer footer | Balanced header, no capsule overflow | | 10 | **Safe-area handling** | `viewport-fit=cover` + `env(safe-area-inset-top)`; `theme-color` follows theme | Notch/status bar never covers content | | 11 | **Double-tap zoom suppression** | `touch-action:manipulation` + legacy iOS `gesturestart` guard | Kills double-tap zoom & 300ms delay, keeps pan/pinch | | 12 | **Accessibility / reduced motion** | `Escape` closes drawer (yields to modals); `prefers-reduced-motion` disables animations; `aria-label` + `focus-visible` | Keyboard & assistive tech friendly | ## ✅ Verified Measured on an isolated `dsh web --port 3090` with headless Edge (CDP): | Viewport | H-scrollbar | Frame restructure | Mobile controls | Touch | |---|---|---|---|---| | Desktop 1280×800 | ✅ none (`scrollWidth==1280`) | ✅ untouched | ✅ all hidden | — | | Phone 390×844 | ✅ none (`scrollWidth==390`) | ✅ drawer active | ✅ FAB 44×44 visible | ✅ touch/mouse/click all open the drawer | | Phone 430×932 | ✅ none (`scrollWidth==430`) | ✅ drawer active | ✅ FAB 44×44 visible | ✅ | - Settings sheet: at 390px → `x:8, width:374, right:382`, no horizontal overflow, all nav tabs visible. - Touch target: FAB `getBoundingClientRect()` = **44×44px**. See "Development" below to reproduce; reports and screenshots land in `.verify-out/` (gitignored). ## 🗂 Architecture (single package, client-only) | File | Purpose | |---|---| | `lib/index.js` | Host entry: exports a valid cordis plugin face (`name`/`inject`/`apply`), zero host logic | | `cordis.patch.yml` | `dsh.bundle` manifest inserting the plugin into the profile layer | | `client/index.jsx` | Client entry: exports `apply`/`name`/`inject`, mounts the mobile layer | | `client/build.mjs` | esbuild: `client/index.jsx → client/client.js` (wrapped in `window.__ModuleLoader__.load`) | | `client/mobile/mobile.css.ts` | From-scratch mobile stylesheet (stable framework hooks only) | | `client/mobile/mobile-apply.tsx` | Client apply: styles + phone chrome + stats marker + three slot components | | `client/mobile/MobileNavToggle.tsx` | Header directory toggle | | `client/mobile/MobileNavOverlay.tsx` | Shell overlay: frame marker + backdrop + FAB + drawer interaction | | `client/mobile/MobileDrawerFooter.tsx` | Drawer footer session-log action | | `client/mobile/locales.ts` | `mobileUx` dictionaries (zh/en) | | `scripts/verify-mobile.mjs` | Headless Edge + CDP acceptance script | | `scripts/verify-settings.mjs` | Settings sheet acceptance script | | `test/*.test.js` | Unit tests (host/package/locale/CSS/bundle contracts, 19 cases) | ## 🛠 Development ```sh npm install npm run build:client npm test ``` Mobile verification (needs an isolated `dsh web`): ```sh # terminal 1 (isolated instance) $env:DSH_HOME = "D:\path\to\.dsh-test" # PowerShell dsh plugin --profile web add "link:D:/path/to/dsh-mobile-ux" -w dsh web --port 3090 # terminal 2 node scripts/verify-mobile.mjs node scripts/verify-settings.mjs ``` ## 🤝 Attribution - Plugin layout (single-package client plugin, `dsh.client`/`cordis.patch.yml`/`__ModuleLoader__` bundling) references [dsh-pocket](https://github.com/shaobeichen/dsh-pocket) (GPL-2.0). - The mobile UI layer is an independent from-scratch implementation, not derived from [dsh-web-mobile](https://github.com/mexiaosqwq/dsh-web-mobile); the two are interchangeable (do not enable two drawer layers at once). ## 📄 License [GPL-2.0](LICENSE). --- **Feedback?** Open an issue at [GitHub Issues](https://github.com/wangyuanchuan2022/dsh-mobile-ux/issues).