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
- 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.
- swipe right anywhere on the conversation (mid-screen works — Android
- 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.
- tapping Settings in the drawer shows the section list (General,
- 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. - 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/bodyare 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 to100dvh), 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. - 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. - 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.clientin package.json;exports["./client"]is the ModuleLoader
bundle; the bundle'sinjectasks for thelayoutservice 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)(plusdisplay:nonechrome rules
outside it), tags the shell frame/columns/composer/settings-modal with
stabledata-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
- Open the dsh web GUI on a narrow window (or device emulation ≤ 768px) and
refresh. - The sidebar is gone; a slim top bar shows the session title, ☰ and ⋯.
- Swipe right mid-screen → sidebar slides in; swipe left → back/hide.
- Tap ☰ → drawer; tap Settings → the section list fills the drawer;
pick one → full-screen settings; X or left-swipe walks back. - Tap ⋯ → the Chat/Trajectory menu drops down; pick a tab → view switches.
- The composer shows the model name cleanly next to the send button.
- 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-mobilerow fromcordis.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.6on 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.