DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

AliceLJY /

AliceLJY/dsh-thumb

Verified

A phone shell for the DeepSeek Harness (dsh) web GUI — sidebar becomes an overlay drawer, settings goes full-screen single column. Zero hardcoded host class hashes.

★ 0 Stars0 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHub
READMESource: main@6bf08239

dsh-thumb

A phone shell for the DeepSeek Harness (dsh) web GUI. The sidebar stops squeezing the chat and becomes an overlay drawer, tapping a session closes it, the settings sheet goes full-screen single column, and the transcript is retuned for a phone reading distance. Desktop is untouched.

中文说明

Personal tool, published as-is. Written for one person's phone and one dsh version (0.1.0-rc.6). No maintenance and no issue response are promised. If a dsh upgrade breaks it, the four entries in LOCATORS at the top of src/client.js are where to look — that is a minutes-long edit.

Why

On a phone, dsh's first screen is fine — the sidebar auto-collapses to a 56px icon rail and the composer fits. Everything breaks only after the sidebar is expanded, which happens to be the one path you must take to switch sessions.

Measured on an iPhone 14 Pro viewport (393×660), dsh 0.1.0-rc.6, before this plugin:

Situation Before After
Collapsed rail fine untouched
Sidebar expanded takes 71% of the screen and squeezes chat to 113px overlays; chat keeps its full 393px
After tapping a session stays open, so you read in the 113px slot closes itself
Dismissing it only via the collapse button tap anywhere on the scrim
Settings sheet 800px two-column layout crammed into 393px; every English word wraps; value pickers fall off-screen full-screen single column, text wraps normally, pickers on-screen
Clipped elements in settings 7 1
Transcript height (3-turn session) 610px — sized for a desktop reading distance 490px, a fifth shorter

This is not a bug upstream — it is a stated contract. From the column solver in @deepseek-ai/dsh-client-ui-layout:

The sidebar never concedes: its rendered width is always the drag preference (or the collapsed rail), and center absorbs any remaining deficit as the last resort.

With SIDEBAR_DEFAULT = 280, a 393px viewport gives exactly 393 − 280 = 113. The concession chain shrinks the details panel, then closes it, and then the center column eats whatever is left. Reasonable in a narrow desktop window with a few hundred pixels to spare; not on a phone.

Install / remove / switch off

# install
dsh plugin --profile web add github:AliceLJY/dsh-thumb
# confirm "dsh-thumb" is in dsh.profile.bundles in ~/.dsh/profiles/web/package.json, then restart the service

# remove
dsh plugin --profile web remove dsh-thumb
# again, confirm it is gone from the bundles array, then restart

There is no npm package: dsh plugin add hands its arguments to pnpm, so a github: specifier installs the repository directly. Measured at 8.9s on a cold store, and dsh appends it to dsh.profile.bundles on its own.

Working on it locally instead? Point the same command at a path:

dsh plugin --profile web add link:/absolute/path/to/dsh-thumb

⚠️ Rolling back means undoing two things: package.json and the link in node_modules. Restore only the former and the next pnpm add sees the link still present, decides "Already up to date", and writes nothing while reporting success — it looks installed but isn't.

To switch off without restarting anything:

  • append ?thumb=0 to the URL, or
  • run localStorage.setItem('dsh-thumb','0') in the console and reload

How this differs from the similar plugins on GitHub

A dozen dsh mobile projects appeared around the same time, and the usual way to do this job is to hardcode dsh's class names in CSS (pI_x6G_sidebarCol, Md3f7G_scroll, and so on). Those are generated by CSS Modules at build time, so they all change on every dsh rebuild and the plugin dies silently — styles stop applying, the page still renders, nothing is logged. It just feels worse one day.

No host class name appears anywhere in this package. Instead the three columns are located once at runtime by their semantic suffix (sidebarCol — that half comes from the upstream source variable and survives hashing) and stamped with our own data-thumb attributes; every rule keys off those. If upstream renames something, one locator breaks in one place where you can find it, instead of styles failing quietly everywhere.

Behaviour goes through official interfaces too: closing the drawer calls ctx.layout.toggleSidebar() — the public ILayout method that ui-sidebar itself uses — rather than synthesising a click on some button.

Scope

The layout rules apply only when both conditions hold: viewport ≤1023px (matching upstream's SIDEBAR_AUTO_COLLAPSE = 1024, so there is only one breakpoint in play) and the sidebar has been manually expanded. The 56px rail is left entirely alone — it already works.

Is the drawer still worth it on a tablet?

An earlier version of this file guessed that it stops being worth it somewhere around tablet widths, and suggested dropping the breakpoint to 767px. Measured, that guess was wrong.

What matters is the state you are left in after tapping a session, since that is the path you have to take to switch sessions. Stock leaves the sidebar open, so the transcript stays at width − 280. The shell closes it, leaving the 56px rail and width − 56:

viewport stock with the shell gain
393px 113px 337px +198%
480px 200px 424px +112%
604px 324px 548px +69%
768px 488px 712px +46%
900px 620px 844px +36%
1023px 743px 967px +30%

Regenerate that table with node test/measure-widths.mjs; the file also explains why it measures the width you are left with rather than the width the drawer covers, which is what the first attempt got wrong.

The gain shrinks with width, as expected — but it never inverts anywhere inside the supported range. Lowering the breakpoint would switch off something still worth 30–46% at the sizes it was supposed to help. The 1023px breakpoint stays.

The density rules are the one deliberate exception: they apply at ≤1023px with the drawer shut, because that is the state you read a transcript in. On the measured session that is 610px of column down to 490px — 60px of it from the gap between turns alone (16px → 10px), which costs nothing to interact with. Body text goes 16px/28px → 14px/21px and the action buttons 28px → 24px; the buttons were already under the 44px iOS touch target, so that last one is a trade, and --thumb-hit at the top of the stylesheet puts it back.

Desktop is verified unchanged: sidebar 280px, center 1160px, position: static, no scrim, settings still an 800px two-column sheet.

Known limitations

  • Hover tooltips can still overflow the right edge (the dark card when hovering a workspace row). Deliberately not fixed: its classes (_card / _copyable) are too generic to target safely, and doing it properly would mean scanning every position: fixed layer each frame and clamping them back into the viewport — an unclear blast radius for what is a cosmetic issue that blocks nothing. The right fix belongs upstream, in the tooltip's own touch handling.
  • Measured at 393, 480, 560, 604, 640, 700, 768, 900 and 1023px, plus desktop 1440×900 — see the table under Scope. Phone sizes are the ones used daily; the rest were measured once, not lived in.
  • The settings nav still stacks vertically instead of scrolling horizontally. The flex-direction: row rule lands on a wrapper that is not the one actually laying those items out, so it costs some vertical space at the top of the sheet. Left as is: the sheet went from unusable to usable, and chasing the exact nav container is polish, not repair.
  • The action rows under assistant replies keep their 28px while the ones under user messages shrink to 24px. They sit in a second flex wrapper and moved for none of height, align-self or min-height, and no rule naming those classes sets a height at all — so whatever sizes them needs a wider probe than a class-name scan. Left alone deliberately: it is 12px on a three-turn session, roughly 2% of the column.
  • A change to how upstream lays out its columns means updating this. The locators are the four entries in LOCATORS at the top of src/client.js — a minutes-long edit.

Verification

node test/smoke.mjs        # 21 assertions against a running dsh

The suite drives a real dsh instance at the phone viewport and again at 1440×900: locators stamped, the chat keeping full width behind the drawer, the four density numbers, containment, tablet width, the off switch, and a desktop regression pass that asserts body text is still 16px there. One assertion is end-to-end rather than a property check — it loads the same transcript twice, once with ?thumb=0, and requires the shell's version to be at least 15% shorter.

It has caught two real regressions so far, and both are worth knowing about.

The off switch, on the suite's first run. ?thumb=0 only ever disabled the React component, and until the density rules landed that was enough — every rule was gated on a drawer attribute only a live component sets. The density rules apply with the drawer shut, so the stylesheet became load-bearing on its own and the switch no longer reached it. ensureStyle and stampFrame now check it directly.

Containment, which the suite did not catch — it came back from a phone. The center pane mounts the trace view as well as the transcript, and the trace toolbar is made of text buttons. [class*="_actions"] > button sized every one of them to 24px square, collapsing Duration / Turns / Calls into an unreadable overlap. Every density rule now goes through FLOW, a scope that identifies the transcript column by the message items it holds rather than by a class name. The lesson was not the selector; it was that a suite which only ever opens one tab will keep passing while a neighbouring view is broken. Hence the containment assertion — and it was verified by reintroducing the bug and watching it fail, which it does, naming the three buttons.

Note for reproducing that one: the trace tab does not accept a Playwright click at phone width. Open it at desktop size and then shrink the viewport — same mounted view, narrow layout, no navigation in between.

Screenshots from the runs are not published — they show real workspace and session titles.

Three traps worth knowing before reproducing it:

  1. ESM ignores NODE_PATH — a globally installed playwright needs an absolute specifier, and it is CommonJS, so import the default.
  2. Chrome picks up the system proxy and ts.net addresses time out — use http://127.0.0.1:3080 with --no-proxy-server.
  3. waitUntil: 'networkidle' never fires — dsh holds a live connection open, so use domcontentloaded plus a fixed wait.

Development notes

Two traps, both of the "looks like it's somewhere else, is actually your own doing" kind:

Plugin never activates, whole page blank. dsh.client.inject in package.json and the inject exported from client.js look identical and mean different things: the first lists package names (module load order), the second lists cordis service names (['slots', 'layout']). Get the second wrong and the plugin stays pending, the shell reports web boot: 1 entry did not activate, and nothing renders at all. Copy what @deepseek-ai/dsh-client-ui-sidebar injects.

A drawer that can never be closed. The expanded check originally read the sidebar's rendered width (>56px means expanded) — and the drawer CSS is what pins that sidebar to 320px. The condition was contaminated by its own side effect, so it stayed true forever: collapse button, scrim and auto-close all appeared broken, and it looked like ctx.layout.toggleSidebar() was a no-op on narrow viewports (it was fine all along). It now reads the inline grid-template-columns that AppFrame writes on the frame — upstream's intent, which this plugin never touches. Rule: never derive a condition from a quantity you overwrite yourself.

License

MIT

—/ 5

No ratings yet

Verified DSH bundle

Commit 6bf08239fd22

Community comments

No comments yet. Be the first to write one.

DSH HUB

A community index for DSH plugins. Not an official GitHub or DeepSeek AI product.

CommunityResourcesAPIAbout