# dsh-thumb 给 DeepSeek Harness(dsh)网页版做的手机外壳。侧栏从「挤压聊天区」改成「覆盖式抽屉」,点会话自动收起,设置面板改全屏单列,正文排版按手机的阅读距离重调。桌面端一点不碰。 [English](./README.md) > **个人自用工具,按原样发布。** 是照着一个人的手机、一个 dsh 版本(`0.1.0-rc.6`)写的,不承诺维护、不承诺响应 issue。dsh 升级后失效的话,改 `src/client.js` 顶部 `LOCATORS` 那四条定位器就行,是分钟级的事。 ## 为什么需要它 dsh 的网页界面在手机上**首屏是正常的**——侧栏自动收成 56px 图标条,输入框完整。问题全在侧栏展开之后,而那正是手机上切会话的必经路径。 实测(iPhone 14 Pro 视口 393×660,dsh `0.1.0-rc.6`,装本插件之前): | 场景 | 改造前 | 改造后 | |---|---|---| | 收起态(图标条) | 正常 | **不介入**,原样 | | 侧栏展开 | 侧栏占 71% 屏宽,**挤压**聊天区到 113px | 抽屉覆盖,聊天区 **393px 满宽** | | 点会话之后 | 侧栏不收,一直卡在 113px 窄缝 | 自动收起 | | 关闭抽屉 | 只能手动点收起按钮 | 点遮罩任意处即可 | | 设置面板 | 800px 两列硬塞,英文逐词换行,选择器切出屏幕 | 全屏单列,文字正常换行,选择器回到屏内 | | 设置面板被切元素 | 7 处 | 1 处 | | 正文总高(三轮会话) | 610px,按桌面阅读距离排的 | **490px**,少五分之一 | 根因不是 bug,是上游的显式契约。`@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**. 配合 `SIDEBAR_DEFAULT = 280`,在 393px 视口上算出来就是 `393 − 280 = 113`。让步链是「先缩详情栏 → 再关详情栏 → 最后中间栏吃掉全部亏空」。在窄的桌面窗口上这很合理(还剩几百 px),在手机上就不行了。 ## 装 / 卸 / 临时关 ```bash # 装 dsh plugin --profile web add github:AliceLJY/dsh-thumb # 确认 ~/.dsh/profiles/web/package.json 的 dsh.profile.bundles 里有 "dsh-thumb",然后重启服务 # 卸 dsh plugin --profile web remove dsh-thumb # 同样确认 bundles 数组里已移除,再重启 ``` 没有 npm 包:`dsh plugin add` 把参数直接交给 pnpm,所以 `github:` 规格能直接装仓库。冷存储下实测 8.9 秒,dsh 会自己把它追加进 `dsh.profile.bundles`。 想在本地改着用?同一条命令换成路径即可: ```bash dsh plugin --profile web add link:/绝对路径/dsh-thumb ``` ⚠️ **卸载/回滚要两件一起做**:`package.json` 和 `node_modules` 里的 link。只还原 `package.json` 的话,下次 `pnpm add` 会看到 link 还在、判定 "Already up to date",**不写任何东西也不报错**——看起来装上了,实际没有。 临时关闭,不用重启服务: - 地址后加 `?thumb=0` - 或控制台 `localStorage.setItem('dsh-thumb','0')` 后刷新 ## 它跟 GitHub 上那些同类插件的区别 同期社区有十几个 dsh 移动端项目,做同一件事的插件的通行写法是**在 CSS 里硬编码 dsh 的 class**(`pI_x6G_sidebarCol`、`Md3f7G_scroll` 这种)。那些 class 是 CSS Modules 构建时生成的,**dsh 每次重新构建就全变,插件随之静默失效**——样式不生效、页面还在、控制台不报错,你只会觉得"今天怎么又难用了"。 样式表里不放任何宿主 class 名(源码注释里出现过两次,是作为"不该硬编码成什么样"的反例)。做法是运行时按**语义后缀**(`sidebarCol` 这半截来自上游源码的变量名,不随构建变)定位一次,给节点打上自己的 `data-thumb` 属性,样式表只认这些属性。上游要是真改了变量名,坏的是一个定位器,在一个地方、能直接查出来;不会变成满地样式无声失灵。 行为上也尽量走官方接口:关抽屉调的是 `ctx.layout.toggleSidebar()`(上游 `ILayout` 的公开方法,ui-sidebar 自己也用它),不是去模拟点击某个按钮。 ## 作用域 **布局类规则只在两个条件同时成立时生效**:视口 ≤1023px(对齐上游的 `SIDEBAR_AUTO_COLLAPSE = 1024`,避免两套断点打架)**且**侧栏被手动展开了。56px 图标条那个状态完全不碰——它本来就是好的。 ### 平板尺寸下抽屉化还值不值? 这份文件早先的版本猜测「到了平板尺寸抽屉化未必还是改善」,并建议把断点降到 767px。实测下来,那个猜测是错的。 真正该看的是**点完一个会话之后**的状态——因为切换会话必须走这条路。原生行为是侧栏赖着不走,正文停在 `宽度 − 280`;本插件会自动收起它,留下 56px 图标条,正文是 `宽度 − 56`: | 视口宽度 | 原生 | 装了本插件 | 收益 | |---:|---:|---:|---:| | 393px | 113px | 337px | **+198%** | | 480px | 200px | 424px | +112% | | 604px | 324px | 548px | +69% | | 768px | 488px | 712px | **+46%** | | 900px | 620px | 844px | +36% | | 1023px | 743px | 967px | **+30%** | 这张表可以用 `node test/measure-widths.mjs` 重新生成;那个文件里也写清了为什么量的是「最后剩给你多宽」而不是「抽屉盖住了多少」——后者正是第一次量错的地方。 收益随宽度递减,这符合预期——但在整个支持区间里从未反转。降断点等于在它本该起作用的尺寸上,关掉一个仍值 30–46% 的东西。1023px 这个断点保持不变。 **正文密度规则是唯一有意的例外**:它在 ≤1023px、抽屉关着的状态下生效,因为读正文本来就是抽屉关着的时候。实测那个会话从 610px 压到 490px,其中 60px 只来自消息之间的间距(16px → 10px),这一项没有任何交互代价。正文字号 16px/28px → 14px/21px,操作按钮 28px → 24px;按钮本来就低于 iOS 建议的 44px 触摸目标,所以最后这项是个权衡,样式表顶部的 `--thumb-hit` 可以调回去。 桌面端已验证零变化:侧栏 280px、中间栏 1160px、`position: static`、无遮罩、设置面板仍是 800px 两列。 ## 已知限制 - **hover tooltip 仍可能溢出右边缘**(悬停工作区行时那个黑色卡片)。有意不修:它的 class 语义(`_card` / `_copyable`)太泛,稳妥的修法得在每帧扫描所有 `position: fixed` 浮层再逐个夹回视口,误伤面不明,而它只是视觉瑕疵、不挡任何操作。真要修的话,正确入口是上游给 tooltip 加触屏判断,不是从外面兜。 - **实测过 393、480、560、604、640、700、768、900、1023px 以及桌面 1440×900**,数据见[作用域](#作用域)一节。手机尺寸是每天在用的,其余只是量过一次,没有长期使用。 - **设置面板的导航项仍是竖排**,没有变成横向滚动条——那条 `flex-direction: row` 打在了一个并非真正承载这些项的容器上,结果是面板顶部多占一点垂直空间。有意不追:面板已经从「没法用」变成「能用」,再去精确定位那一层属于打磨不属于修复。 - **AI 回复下面那排操作图标仍是 28px**,只有用户消息那侧压到了 24px。它套在另一层 flex 容器里,`height`、`align-self`、`min-height` 三样都推不动它,而扫遍样式表也找不到任何一条给这些 class 设过高度的规则——也就是说撑住它的东西不在「按 class 名找规则」这个探测范围内。有意不追:三轮会话上只值 12px,约占正文总高的 2%。 - **上游改了列布局的实现方式就得跟着改**。定位器在 `src/client.js` 顶部的 `LOCATORS`,四条,改起来是分钟级的事。 ## 验证 ```bash node test/smoke.mjs # 21 项断言,需要一个跑着的 dsh ``` 脚本在手机视口和 1440×900 各驱动一遍真实 dsh:定位器打标、抽屉打开时聊天区是否占满宽度、四个密度数值、作用域是否越界、平板宽度、关闭开关,以及桌面回归(断言桌面正文仍是 16px)。其中一条不是查属性而是端到端的——同一段会话加载两次、一次带 `?thumb=0`,要求本插件那次的正文至少矮 15%。 到目前为止它接住了两个真回归,两个都值得记下来。 **关闭开关**,第一次跑就红了。`?thumb=0` 一直以来只关掉了 React 组件,在密度规则落地之前这就够了——那时每条规则都挂在一个只有活组件才会设置的抽屉属性上。密度规则是抽屉关着也生效的,样式表本身就成了承重件,开关够不到它。已在 `ensureStyle` / `stampFrame` 里直接检查开关修掉。 **作用域越界**,而这个测试**没**接住,是从手机上报回来的。中间栏挂的不只是对话,还有轨迹视图,而轨迹页的工具栏是一排文字按钮。`[class*="_actions"] > button` 把它们统统压成了 24px 见方,`Duration / Turns / Calls` 叠成一团没法读。现在每条密度规则都走 `FLOW` 作用域——它靠"这一列里装着消息条目"来认对话列,而不是靠 class 名。真正的教训不是选择器写宽了,而是**一套只打开过一个 tab 的测试,会在隔壁视图坏掉的时候一路绿灯**。所以补了那条越界断言,并且把 bug 放回去验过它确实会红、还会点名那三个按钮。 复现那个的注意事项:手机宽度下 Playwright 点不动轨迹 tab。先在桌面尺寸点开,再把视口缩小——同一个已挂载的视图、窄屏布局,中间不发生跳转。 跑出来的截图没有随仓库发布——上面有真实的工作区名和会话标题。下面三个坑才是复现时真正需要的东西。 复现时会踩到的三个坑,一并记在这: 1. **ESM 不读 `NODE_PATH`** —— 全局装的 playwright 要用绝对路径引入,而且它是 CommonJS,得走 default import。 2. **Chrome 会吃系统代理**,`ts.net` 地址直接超时 —— 走 `http://127.0.0.1:3080` 加 `--no-proxy-server`。 3. **`waitUntil: 'networkidle'` 永远等不到** —— dsh 有常驻实时连接,用 `domcontentloaded` 加固定等待。 ## 开发笔记 两个坑记在这里,因为都属于"看起来在别处、实际在自己身上"那类: **插件不激活、整页白屏。** `package.json` 的 `dsh.client.inject` 和 `client.js` 导出的 `inject` 同名同形但语义不同:前者是**包名**(模块加载顺序),后者是 **cordis 服务名**(`['slots', 'layout']`)。把后者写成包名,插件会一直 pending,外壳报 `web boot: 1 entry did not activate`,而**整个页面渲染不出来**。参照 `@deepseek-ai/dsh-client-ui-sidebar` 的 `inject` 值就对了。 **永远关不掉的抽屉。** 判断抽屉是否展开,一开始读的是侧栏的渲染宽度(>56px 即展开)——而抽屉 CSS 恰恰把侧栏钉在 320px。**判据被自己的副作用污染**,于是它永远为真,收起按钮、遮罩、自动收起全部"失灵",看起来像 `ctx.layout.toggleSidebar()` 在窄屏下不工作(它其实一直是好的)。现在读的是 AppFrame 写在 frame 上的 inline `grid-template-columns` 第一列——那是上游的意图,本插件不碰。**规矩:不要用一个自己会覆盖的量去做判据。** ## License MIT