Back to home

davidgereb

dsh-plugin-better-mobile-ui

Better UI for DeepSeek Harness's web view on mobile.

Stars
0
Language
JavaScript
Created
Aug 15, 2026
Updated
Aug 15, 2026

Introduction

dsh-plugin-better-mobile-ui

A dsh web client plugin that makes the GUI usable on phones/tablets (viewport ≤ 768px by default). It only activates in the mobile media query — desktop is byte-for-byte untouched.

Install

From GitHub

One block — installs the plugin and registers the loader row:

dsh plugin --profile web add github:davidgereb/dsh-plugin-better-mobile-ui
PATCH="${DSH_HOME:-$HOME/.dsh}/profiles/web/cordis.patch.yml"
grep -q "name: dsh-plugin-better-mobile-ui" "$PATCH" 2>/dev/null || cat >> "$PATCH" <<'EOF'

- insert:
    - id: ui-better-mobile
      name: dsh-plugin-better-mobile-ui
EOF

From a local checkout

# 1. build
node scripts/build-client.js
# 2. make the package resolvable by the profile
dsh plugin --profile web link /path/to/dsh-plugin-better-mobile-ui
# 3. register the loader row in cordis.patch.yml (see above)

What it fixes

  1. Sidebar → swipe drawer — the sidebar grid track is forced to 0 on mobile, so the collapsed 56px rail no longer eats screen space. The sidebar becomes an off-canvas drawer:
    • swipe right anywhere on the conversation (mid-screen works — Android owns the screen edges) to slide it in;
    • swipe left anywhere to go back (see the navigation stack below);
    • the ☰ button in the top bar toggles it;
    • the app's own layout store is kept in the "expanded" state on mobile, so the drawer always shows the full sidebar (sessions, workspaces), never the icon rail.
  2. Settings as drawer screens (Android-settings style navigation stack). The app's settings modal renders inside the sidebar column, so on mobile it becomes two screens:
    • tapping Settings in the drawer shows the section list (General, Models, Plugins, …) filling the drawer, with a ← back arrow;
    • selecting a section expands the drawer to the full viewport as the settings page;
    • the X dismisses back to the settings list; the left-swipe backs are fully drag-follow like the sidebar — the drawer follows the finger and snaps on release: settings page → settings list → sidebar → hidden.
  3. Session header dropdown — the header (session title + Chat/Trajectory mode tabs) is hidden on mobile and becomes a dropdown menu under the button in the top bar (tabs stack vertically). Picking a tab or crumb dismisses it; action/utility buttons inside it (e.g. the cost-lens cheapass toggle) stay usable with the menu open. Tapping outside or pressing Escape dismisses it too.
  4. Top action bar — a 48px fixed bar shows the session title next to a ☰ sidebar button, with the menu button on the right. The background is fully opaque; a gradient fades its bottom edge into the content. It stays pinned while the mobile keyboard is open: html/body are locked against document scrolling, the bar re-anchors to visualViewport.offsetTop, and the app frame tracks visualViewport.height (via --bmu-vh) so the composer always rides above the keyboard. The override is cleared when the keyboard closes (falling back to 100dvh), and a slow re-sync interval plus window/orientation listeners keep the frame from getting stuck raised if a keyboard show/hide event is missed.
  5. Composer toolbar — the model selector used to get crushed to zero width while its label overlapped the "+" commands button. The toolbar now wraps, the trailing group shrinks instead of overflowing, the token-context meter is dropped on mobile, and the model name ellipsizes instead of colliding with icons. Plain Enter inserts a newline (mobile only); submission happens via the blue send button (modifier chords like Ctrl/Cmd+Enter keep the app's accelerated behavior). While the agent runs, the primary button becomes Stop, so an extra send button appears next to it — tapping it queues the message via the app's own steering path without stopping the run.
  6. Sidebar navigation — opening a session (or New Session) closes the drawer; expanding/collapsing a workspace or its action buttons keep it open.

Swipes deliberately do not trigger on buttons/links/inputs, on transient popups (menus/dialogs — the settings panel is a full-screen page, so it is exempt), or on horizontally scrollable areas (wide tables, the attachment rail). Swiping on the message box (composer) is allowed on purpose — it sits at thumb height.

Layout

dsh-plugin-better-mobile-ui/
├── package.json              # dsh.client web plugin declaration
├── lib/
│   ├── index.js              # host-side stub (empty apply; makes the loader entry real)
│   └── client.js             # browser bundle (generated)
├── src/
│   └── client-source.js      # single source of truth for the browser half
└── scripts/
    └── build-client.js       # generates lib/client.js

How it works

  • dsh.client in package.json; exports["./client"] is the ModuleLoader bundle; the bundle's inject asks for the layout service so the plugin can drive the app's sidebar state (ctx.layout.toggleSidebar()).
  • The browser half injects one <style> whose rules are all inside @media (max-width: <breakpoint>px) (plus display:none chrome rules outside it), tags the shell frame/columns/composer/settings-modal with stable data-bmu-* handles (class names are hashed; the tags are not), and runs the pointer-gesture controller + top-bar/dropdown wiring.
  • The sidebar grid track is overridden with !important; the columns are pinned to their grid tracks so the center column keeps full width while the sidebar drawer floats above it.
  • Settings "page" mode widens the drawer column to 100vw (no transform ancestors, so the modal's absolute positioning just fills the viewport).

Verify

  1. Open the dsh web GUI on a narrow window (or device emulation ≤ 768px) and refresh.
  2. The sidebar is gone; a slim top bar shows the session title, ☰ and ⋯.
  3. Swipe right mid-screen → sidebar slides in; swipe left → back/hide.
  4. Tap ☰ → drawer; tap Settings → the section list fills the drawer; pick one → full-screen settings; X or left-swipe walks back.
  5. Tap ⋯ → the Chat/Trajectory menu drops down; pick a tab → view switches.
  6. The composer shows the model name cleanly next to the send button.
  7. Widen past 768px → everything returns to the normal desktop layout.

Rebuilding / iterating

node scripts/build-client.js
dsh plugin --profile web add github:davidgereb/dsh-plugin-better-mobile-ui
# the running GUI hot-swaps rebuilt bundles via the client-hmr SSE channel
# (no refresh needed for bundle-only changes once the plugin is loaded).

Configuration

Before boot, window.__BETTER_MOBILE_UI_CONFIG__ = { breakpoint: 820 } overrides the mobile breakpoint (default 768). Read once at apply time.

Reverting

  • Remove the ui-better-mobile row from cordis.patch.yml.
  • dsh plugin --profile web rm dsh-plugin-better-mobile-ui
  • Restart the dsh server (new-entry removal needs a boot) and refresh the page.

Compatibility. Tested against dsh 0.1.0-rc.6 on Node.js v24.19.0 (dsh web profile). Older or newer dsh releases may change the internals this plugin hooks into — check the changelog before upgrading.


⚠️ AI-generated, provided as-is. This project was written with the assistance of an AI. It is provided AS IS without warranty of any kind, express or implied. The author cannot be held responsible for any damage, data loss, or misbehaviour that results from using it. Use at your own risk.