
Security News
Deno Goes the Way of the Dinosaur
Cloudflare is acquiring Deno’s team, which will provide another year of bug fixes and security updates before ending support for the runtime.
dsh-pocket-ui
Advanced tools
DSH Web UI 移动端适配(轻量核心版):窄屏下侧栏变抽屉、对话框变底部 sheet、刘海安全区避让、输入区不重叠;鼠标操作的桌面端任何宽度都完全 no-op。
DSH Web UI 的移动端适配插件 · 轻量核心版
窄屏好用,宽屏不打扰:手机竖屏下侧栏变抽屉、对话框变底部 sheet、刘海安全区避让、输入区不再打架;鼠标操作的桌面端任何宽度都是完全 no-op。
DSH Web UI 是桌面优先的 React SPA。它的外壳 CSS 里一条宽度断点都没有(整个 dsh-web-frontend 只有 3 条 prefers-reduced-motion),没有 viewport-fit=cover,没有一处 env(safe-area-inset-*)。唯一的响应式能力是 JS 里的一个常量:
const SIDEBAR_AUTO_COLLAPSE = 1024 // 低于此宽度把侧栏折成 56px 图标条
在 390px 的手机上,这远远不够:设置面板是 800px 宽的居中弹窗、会话区被 56px 图标条挤掉、刘海遮住顶部、输入区的模型选择器把发送键顶出屏幕。
本插件补齐这些。它不重写 UI,只驯化宿主已有的 DOM——往宿主插槽里塞自己的组件、注入一份改写布局的样式表、用一个协调层给宿主节点打标记。所以宿主的每一次功能更新你都能照常拿到。
| 功能 | 说明 |
|---|---|
| 侧栏变抽屉 | 窄屏下侧栏收进覆盖式抽屉(宽 min(84vw, 320px)),会话区全宽;面板内点击一律不拦截(点会话行就是选会话),收起用宿主自带的「收起侧边栏」按钮;点面板外的遮罩也可关,Esc 同样有效 |
| 抽屉开关按钮 | 左侧贴边的小把手:宽 22px、高 44px,左无圆角、右 8px 圆角,描边与三条杠共用 #4176e6;只在抽屉关闭时出现(打开时由遮罩接管)。纵向位置是量出来的——居中于「页头下沿 → 输入框上沿」这段真正空着的走道,所以空会话(宿主把输入框居中)和长会话(输入框贴底)都不会被遮住。避让安全区 |
| 弹窗变底部 sheet | 设置等 aria-modal 对话框改为贴底全宽 sheet,左侧竖排导航变成顶部横向滚动标签条 |
| 右栏全屏化 | 文件树 / 预览面板在窄屏下铺满,而不是挤在 300px 里 |
| 安全区避让(实测,不盲信) | 补上 viewport-fit=cover 并读取 env(safe-area-inset-*),但先判断这块空间是否真的属于本文档:见下面《顶部空白是怎么来的》 |
| 内置探针 | 页面加 ?pocket=probe 即在设备上显示一份可复制的 DOM 几何报告(门控原因、每个盒子的内边距、env() 实测值、顶部空白的归属),用于手机端排障 |
| 输入区适配 | 覆写宿主的 composer 令牌收窄边距、限制模型选择器宽度、输入框字号提到 16px(避免 iOS 聚焦强制放大且无法缩回) |
| 会话头紧凑化 | 头部高度 97px → 56px,标签条改为横向滚动而不是逐字竖排 |
| 桌面端完全 no-op | 见下节 |
| 设置入口 | 设置 → 通用 里有一行显示状态与版本,并提供「检测更新 / 升级」 |
| 在线更新 | 内置 registry 检查与一键升级(Host 半区),离线静默降级 |
| 互斥保护 | 检测到 dsh-web-mobile 存在时自动让位并在控制台说明,避免两套移动端布局互相打架 |
三层保证,缺一不可:
(max-width: 1023px) and (pointer: coarse)。pointer: coarse 不是可选项——只按宽度判断会让桌面分屏窗口、未最大化窗口、系统显示缩放 125–200% 都误启移动端 UI(1920 物理像素 @200% 缩放 = 960 CSS 像素)。断点 1023px 刻意压在宿主自己的 1024px 之下,两者永不打架。html[data-pocket="on"] 之下。门控关闭时零条规则匹配,没有需要撤销的东西。这条不变量由冒烟测试结构化断言(见下)。PocketChrome 在未激活时 return null,且协调层不会给宿主打任何标记。手机上最容易看错的一类问题:页面顶部多出一条空白带,没人认领。
根因是 env(safe-area-inset-top) 说的不是"本文档"的事,而是"这块屏幕"的事。一个已经位于系统状态栏下方的 WebView 照样会报出这个值(Android WebView 实测约 45px)。此时按它补内边距,等于给一段本来就没被任何 chrome 覆盖的区域再留一次位——于是顶部多出一条谁也不属于的空白,看起来就像多了一个标题栏。
宿主不会背这个锅:DSH 的 --dsh-frame-top-clearance / --dsh-frame-chrome-top 只在桌面壳(data-platform=darwin / data-windows-titlebar,由 Electron preload 写入)下发布,文档原话是 "Plain browser documents publish none of these values." 普通浏览器/WebView 文档拿到的是零。
所以判断只能由插件自己做,两条独立的闸门(任何一条成立就不补顶部安全区):
frame.top > 2)说明这块余量已经被别人花掉了,再补一次就是重复计算。浏览器标签页(位于地址栏下方)、把页面放在原生标题栏之下的任何容器都命中这条。display-mode: browser 意味着页面不是以独立/全屏应用呈现的,也就不可能与系统栏重叠。App 内置 WebView 同样报 browser——而那恰恰是 inset 说谎的场景。独立 PWA / 全屏应用报 standalone / fullscreen,保留自己的 inset。
matchMedia 不可用(unknown)时按保守处理:保留 inset——漏掉一个真实的刘海会让侧栏第一行被时钟盖住,比多一条空白带严重得多。底部 inset 反向夹紧:viewport - frame.height 放不下就直接归零,否则就会变成幽灵外层滚动("输入框被顶到折叠线以下"那个经典 bug)。
判定结果全部写进 window.__pocket.boot.insets,?pocket=probe 会原样打印。策略在这里是显式的、可观测的——一个沉默的策略和一个"看不见设备"的插件,从现象上无法区分。
?pocket=probeApp 内置 WebView 没有控制台,所以诊断信息必须能自己长在屏幕上:
http://<你的 NAS>:3080/?pocket=probe # 打开探测覆盖层
覆盖层给出:门控为什么开/关、--pocket-safe-t 与 env(safe-area-inset-top) 的实测值、顶部空白的像素高度与归属元素、每个宿主地标盒子的 padding/margin、/pocket/hello 返回了什么,以及 host 请求实际打到了哪个 URL(document.baseURI、document.origin、__DSH_TRANSPORT__ 是否存在、解析后的 /pocket/hello)。右上角有「复制全部 JSON」,直接粘给助手即可。
最后那一组是"插件装上了但 host 半区连不上"的关键证据:/pocket/hello 不回答和它被发到了哪个地址是两个完全不同的问题,而后者往往一眼就能看出答案(例如地址里缺了反向代理的前缀,或者打到了一个根本没路由的 origin)。
lib/probe-src.js(可 lint、可 diff、有独立单测),由 host 半区在 /pocket/probe.js 按需读取并提供——所以它不会进 bundle,也不影响正常页面加载。?pocket=probe 会强制开启移动端布局:在手机上量到的必须是手机上真正在跑的那套布局。?pocket=off 优先级更高,显式关闭时不会装探针。先看服务端日志(dsh web 的 stdout)里有没有这一行:
[dsh-pocket-ui] host half active, version vX.Y.Z, routes under /pocket
没有这行 → host 半区压根没被加载。最常见的原因是装完/更新完没有重启:host 半区是被 import 进 Node 进程的,没有卸载路径,而 client 半区在 link: 安装下是直接读磁盘的,所以会出现"行出来了、host 还不在"的错位状态。重启即可。设置行在连不上时也会直接把这句话写在脸上。
有这行 → host 半区活着,问题在请求地址。用 ?pocket=probe 看它实际打到哪个 URL,对照:
/dsh/)→ 路由必须相对 document.baseURI 解析,不能是根绝对路径;dsh-app://app/,协议处理器只截静态资源,其余路径会连同宿主 cookie 一起转发给 Host,所以解析基准就是 document.baseURI。另可显式指定 profile 目录(自动探测失败时用):
DSH_POCKET_PROFILE_ROOT=~/.dsh/profiles/web dsh web
这只影响"这个安装是不是 link: 源码目录"的判定(免得在线升级覆盖掉你的工作副本),不影响路由。
dsh plugin --profile web add dsh-pocket-ui
装完重启 dsh web 生效。
本地开发:
dsh plugin --profile web add link:/path/to/dsh-pocket-ui
从
dsh-web-mobile迁移:两者会抢同一批 DOM,请先移除旧插件 (dsh plugin --profile web remove dsh-web-mobile)。本插件内置互斥检测,即使两个都装也只会有一个生效并在控制台提示。
这是最容易搞混的一点。DSH 的 web profile 里有两个 HMR 插件,但它们默认只有一个是开的:
- id: hmr # @deepseek-ai/cordis-plugin-hmr
disabled: true # ← 插件树 / 配置的热加载:默认关闭
- id: client-hmr # @deepseek-ai/dsh-client-hmr
# 没有 disabled → 默认开启
dsh-client-hmr 每 500ms stat 轮询每个插件 bundle 的 (mtimeMs, size),一变就重新哈希,
并经 /plugins/events SSE 推给浏览器。
| 你改了什么 | 怎么生效 | 为什么 |
|---|---|---|
已装插件的 lib/client.js | 自动热替换(连刷新都不用) | client-hmr 轮询到文件变化 → 新 rev → SSE 推送 |
已装插件的 lib/index.js(Host 半区) | 必须重启 | 模块已被 import 进 Node 进程,没有卸载路径 |
| 新装 / 卸载插件 | 必须重启 | 插件树在启动时合成;cordis-plugin-hmr 是 disabled: true |
改 profile 的 cordis.patch.yml | 必须重启 | 同上 |
所以本插件没有构建步骤是刻意的:lib/client.js 本身就是源码,改完存盘,浏览器里的
页面会自己更新。这比「改 TS → 构建 → 刷新」快得多,也没有「忘记重新构建」的坑。
实测(headless Chrome + CDP):页面加载后完全不碰浏览器,只改磁盘上的
lib/client.js, 数百毫秒后活着的页面里注入的样式表内容就已经变了。注意
hmr那行是 base 层就带disabled: true的,不是被后续层覆盖——所以 「装完新插件刷新一下就能用」对全新安装并不成立,那一步一定需要重启。
标准做法是 patch 层禁用(不需要卸载):
# ~/.dsh/profiles/web/cordis.patch.yml
- id: pocket-ui
disabled: true # 注释掉或删除即恢复
彻底卸载:
dsh plugin --profile web remove dsh-pocket-ui
调试用 URL 参数(桌面浏览器上预览移动端布局):
| 参数 | 效果 |
|---|---|
?pocket=mobile | 强制启用移动端布局(忽略指针类型) |
?pocket=off | 强制关闭 |
本插件的宿主形状知识全部集中在 lib/client.js 的 findFrame() / parentOfSlot() 与样式表的选择器里,宿主升级只需检查这几处:
[data-shell-overlay] 仍存在,且仍是 AppFrame 的直接子元素grid-template-columns(我们的覆盖依赖 !important)[data-slot="sidebar"|"main"|"rightbar"] 仍存在(槽位出口是 display:contents,我们选它的父元素)[role="presentation"] > [role="dialog"][aria-modal="true"]SIDEBAR_AUTO_COLLAPSE 仍是 1024(决定我们的断点)--dsh-composer-side-clearance 等令牌仍存在不使用任何宿主 class 名——它们是内容哈希(pI_x6G_frame、wSkVaW_header),每次宿主构建都会变,选择器会静默失配。这条由冒烟测试断言。
已对 0.2.0-rc.2 核对过、没有变化的项(逐条读过安装树里的源码/文档):
sidebar / main / rightbar / shell.overlay 仍在(0.2.0 新增 shell.leading,是 macOS 桌面壳的窗口位,与本插件无关)<div data-slot="<key>" style="display:contents">ctx.layout.toggleSidebar 仍在(四个方法 selectPanel / toggleSidebar / openRightbar / closeRightbar 一个没动)exports.inject(0.2.0 的 dsh.client.inject 是包依赖边的声明,字面写着 "Informational package-name dependencies, not Cordis service injection",不是服务注入)data-platform / data-windows-titlebar / data-fullscreen 仍只由桌面壳 preload 写入;普通文档依旧不发布 --dsh-frame-* 任何值——这正是《顶部空白是怎么来的》一节的依据npm run smoke # 四套全跑(103 项,全离线)
node scripts/smoke-host.js # 20 项:路由 + 探针脚本 + 在线升级全链路 + link 安装的 profile 定位 + manifest 自洽性 + 进程落后于磁盘
node scripts/smoke-client.js # 52 项:bundle 契约、样式表不变量、安全区夹紧算术、host 路由解析、stale 行的优先级、把手落位的走道算术
node scripts/smoke-mount.js # 16 项:客户端半区真正挂到桩 DOM 上,含 teardown 不残留、按页面 base 发请求
node scripts/smoke-probe.js # 15 项:探针本身(含"读的是插件自己的判定"、探针与 bundle 解析一致)
node scripts/verify-cdp.mjs --url '<dsh web 打印的带 token URL>' --screenshot ./shots
容器 / CI 里跑
verify-cdp.mjs需要POCKET_CHROME_FLAGS='--no-sandbox':没有内核 user-namespace 支持时 Chrome 会以sandbox initialization failed: Operation not permitted(exit 133)退出,而且在写出调试端口之前就死了,报错看起来像 "chrome did not expose a debugging port",不像沙箱问题。默认不开——只在被测页面可信时才该去掉沙箱。
四套无浏览器套件与 CDP 探针分工明确,这个划分是踩出来的:
findFrame() 拿不到 [data-shell-overlay]、querySelectorAll 不认识逗号列表、桩 document 没有 baseURI),三次现象都长得像插件坏了,实际是桩不忠实。当前 93/95 通过。唯二两条失败是设置行的
gap与邻行不一致(mine=8px neighbour=normal)——v0.1.4 起就存在, 与把手落位无关,也没有随本次改动变化;它是已知的视觉细节,没有假装成"全绿"。
CDP 覆盖:
headless Chrome 没有任何指针设备,三个
(pointer: …)查询全为 false。必须用Emulation.setTouchEmulationEnabled才能让(pointer: coarse)命中——Emulation.setEmulatedMedia对指针特征静默无效。带
?pocket=probe的页面会挂上探针覆盖层,断言几何的 CDP 用例不要带这个参数。第一次跑会撞上宿主的 Internal Testing Notice。关掉它时必须一并解除它对页面的锁: 宿主用
#root[inert]锁住其余文档,只删弹窗不解除,之后每一次命中测试都只会返回<body>——看起来就像"插件的按钮被什么东西盖住了",实际是inert。探针现在会打印 命中的元素、元素栈、祖先链(pe/overflow/clip/transform+ 盒子)与inert链, 这类问题一次就能定位。设置行的
gap与邻行不一致(8pxvs 宿主normal)是长期存在的(v0.1.4 起相同), 属视觉细节,未改;CDP 里仍会报出来。
主题是抽屉按钮改成一枚贴边把手,并且落位是「量」出来的而不是「定」出来的。
+(那个控件止于 y 803);被切掉的是卡片左下角,并且和 9 轮 456 步 状态行共处一行。量不大,但按钮是 position: fixed(z-index 40),不在输入框自己的层里,所以还顺带抢走那一角的点击。选择工作区 在 y 387–415,而锚在 50% 的把手会落在 y 400–444,同时压住两者。390×667 与 390×932 在 45–46% 处出现同样的碰撞——所以这是宿主布局的性质,不是某一块屏的巧合。syncTabSeat() 每次 reconcile 量这两个边并把结果写进 --pocket-tab-top。有历史会话时输入框贴底,把手落在约 45%(几乎就是正中);空会话时走道短,它升到约 23%——仍然是它所在空间的正中。两种状态都不遮挡,而且宿主挪动任一条边时会自己跟上。选择工作区,所以卡片的顶边比整块的顶边低约 36px,拿它当上界会漏掉那一行。块级的锚点是 [data-composer-seat](composerStack 元素)。top: var(--pocket-tab-top, 30%)):首次测量之前、或页面上找不到那两个锚点时用它,取值按空会话(更难的那个状态)实测无碰撞。--dsw-radius-sm 那一档),读起来像屏幕边缘长出来的把手而不是一张飘在页面上的卡片;阴影也据此做成单向的。刻意不用 --dsw-radius-md(12px)——在 22px 宽的盒子上会被夹成 11px 的整半圆,把手感就没了。--dsw-alias-bg-layer-2 在浅色模式下与页面底色同色,所以看得见完全靠描边与阴影;描边取 l2(10% 黑 / 12% 白)而不是 l1(4% / 6%),后者在会话底色上会消失。ResizeObserver:reconciler 是 MutationObserver 驱动的,且没有观察 characterData,输入第二行改的只是同一个文本节点,不产生任何它看得见的 mutation。observer 的共享状态声明在 factory 作用域——这条不是形式主义,见下。left 带安全区 inset 而不是写死 0:竖屏解析为 0、真正贴边;横屏则让开摄像头挖孔。纵向不需要同样的修正,因为那条走道是从实时矩形算出来的,顶部 inset 早已把页头推下来了。observeTabBand() 排在一轮 reconcile 的 syncSafeArea() 与 syncDrawerWithHost() 之间,所以它在拿不到 ResizeObserver 时直接 return——一个 throw 会把排在后面的安全区同步与抽屉同步一起带崩,整页布局停在上一帧。deactivate() 里还留着一句 lastFabSeat = null。ESM 严格模式下这是 ReferenceError,意味着每次 teardown 都会抛——而且只在激活之后、只在真机上发生。observeTabBand() 里对 ResizeObserver 没有保护(见上)。测试:
top 必须是带百分比兜底的测量值,且兜底本身要同时满足两个约束(低于 45% 那条碰撞线、高于 56px 的页头);syncTabSeat() 的真跑:贴底 → 377px、空会话 → 194px,并在 12 组走道组合上扫「永不出界、永不吃掉间距」;走道短到装不下时钉在下界(宁可靠页头,不可落进输入框);页头/锚点/高度任一读不到就移除已发布值而不是留一个过期值。tabSeatObserver / tabSeatObserved / lastTabSeat 必须声明在 apply() 之前。pocket-fab / syncFabSeat / observeComposer / lastFabSeat / fabSeat 等标识符不得再出现。上面那个 ReferenceError 正是被它抓住的——删一半的代码是这类改动最典型的坏法。getComputedStyle().getPropertyValue,并且从插件自己的样式表里解析 token 来回答。原先桩缺少这个方法时,代码会走「读不到 token」的降级分支——套件照样全绿,但主路径一行都没跑。这条教训已写进《DSH插件开发流程.md》踩坑 18。gap 视觉差异,与本次改动无关)。新增的断言包括:
resize 唤醒 reconciler → 把手确实跟着下移,且仍等于新走道的中心、仍然不遮挡。锚在常量上的实现不可能通过这一段,所以这是隔离验证而非重复验证;button / a[href] / [role=button] / input / textarea / select),确认没有任何一个落在把手底下——正是这条在锚 50% 时抓出了 选择工作区。把手贴在左侧中部,横向必然与会话区重叠——这是「贴边」的代价,也正是纵向必须让干净的原因。
主题是让「进程里跑的不是磁盘上那份」这种状态自己说出来。
VERSION 是模块被 import 时读一次的,因此它描述的是正在运行的代码,而不是用户装了什么;client 半区却是从磁盘现取、会热更的。更新完不重启就会出现「行上写着 v0.1.4、检出却是 v0.1.7」——谁也解释不了,而它还会去点一个注定失败的升级按钮。/pocket/meta 现在多返回 installedVersion 与 stale(两者不一致即为真)。
这一处必须用
fs.readFileSync重新读,不能用createRequire():req()对.json走的是 CJS 加载器缓存,第二次拿回的正是这份函数要绕开的缓存。
升级 不再以一句无法执行的报错收场。原先定位不到安装位置时抛出 cannot locate the profile install root——是真话,但用户没法照着做。现在两种情形都给出可执行的出路:
ok: 'stale',什么都不装,直接让你重启;ok: 'fail',并写出该执行的命令(dsh plugin --profile <profile> add dsh-pocket-ui@<版本>)。测试:
package.json 之后必须报 stale,并断言包管理器一次都没被调用过;以及「定位不到 profile」时消息里必须含可执行命令、且不再是那句旧死路。version=0.1.7 / installedVersion=0.1.7 / stale=false → 把磁盘改成 0.1.8 → version=0.1.7 / installedVersion=0.1.8 / stale=true → POST /pocket/upgrade 返回 ok: 'stale';还原后回到 stale=false。主题是补上一个从 v0.1.0 起就名不副实的声明——package.json 写着类型声明,那个文件却从来不存在。
lib/index.d.ts。package.json 从模板继承了 "types": "lib/index.d.ts" 与 exports["."].types,但该文件从来没有被创建过(git log --all -- lib/index.d.ts 为空——不是某次误删,而是从未存在),v0.1.0 起一直如此。下游 TypeScript 项目按包名引用本包时会撞上 TS7016: Could not find a declaration file for module 'dsh-pocket-ui',或在 strict 下静默退化成 any。现在按 host 半区真实导出补齐:name 用字面量类型、inject 用 readonly ['webServer']、apply(ctx: unknown);/pocket/* 的响应体也一并声明为 PocketHello / PocketMeta / PocketUpgradeResponse / PocketUpgradeStatus——那是双半区之间唯一真实的契约。(files 已含整个 lib,无需额外改动即可随包发布。)scripts/smoke-host.js):收集 main / types / exports 里出现的每个路径,断言文件真实存在、且被 files 白名单覆盖(package.json 豁免,npm 总是发布它),并断言 .d.ts 里确实声明了 name / inject / apply。把 lib/index.d.ts 删掉时,该断言会精确报出 package.json declares "lib/index.d.ts" but no such file exists——即把这次的问题本身变成一条会红掉的测试。安装 0.1.7 时会撞上一个与本次改动无关、但会让人误以为"插件没修好"的坑:pnpm 11 起
minimumReleaseAge默认 1440 分钟(24 小时)。本版发布后 24 小时内执行pnpm add,pnpm 会在满足 semver 的候选里挑"最新的够老版本",也就是 0.1.4——输出+ dsh-pocket-ui ^0.1.4,而命令是成功的。要立刻用上本版,在 profile 的pnpm-workspace.yaml里放行:minimumReleaseAgeExclude: - dsh-pocket-ui
测试:
主题是**"插件装上了,但设置行说连不上 host 半区"**这一类问题——把结论从"猜"变成"看"。
fetch('/pocket/meta') 只在"应用正好挂在 origin 根"时才成立。两种正常部署里它不成立,而现象都是那句"host 半区无响应":
https://nas/dsh/):路由属于那个挂载点。dsh-app://app/。壳的 protocol.handle 只截静态资源(/、/index.html、/assets/*、/favicon.svg、/manifest.webmanifest),其余路径一律经 forwardWebRequest 转发给 Host,并保留 pathname、附上宿主 cookie——所以 dsh-app://app/pocket/meta 本来就能到达插件,正确的解析基准就是 document.baseURI。
现在统一按页面 base 解析(document.baseURI 优先)。注意这与平台自己的 remoteStreamUrl() 顺序相反,不要照抄:那一条加载的是 WebSocket mux(ws: 不受 CORS 约束,壳还专门为它重写了 cookie 与 origin),而把跨源 fetch() 打到 streamBaseUrl 会因为既没有 cookie、也没有 access-control-allow-origin 而直接 "Failed to fetch"。探针自带的同名解析函数与 bundle 口径一致,并有单测钉住。检查更新 / 升级 以前失败时 catch 里什么都不做——用户看到的就是"点了没反应"。现在行内直接写出失败的那个 URL 和原因。import 进 Node 进程的、没有卸载路径,所以新装或更新后必须重启 dsh web;而 link: 安装下 client 半区是直接读磁盘的,于是很容易出现"行出来了、host 还不在"的错位。行内现在会写「刚安装或更新过?重启 dsh web 后生效」。link: 安装时模块解析到的是源码目录,从 import.meta.url 往上走永远走不到 profile,findProfileRoot() 于是返回 null——"别把源码工作副本覆盖掉"的守卫因此形同虚设,状态行也说不出"本地安装"。现在多两条兜底:在 $DSH_HOME/profiles/* 里找 node_modules/<name> 真实指向本包的 profile;失败再退回唯一的 dependencies 匹配。另支持显式指定 DSH_POCKET_PROFILE_ROOT=<dir>。
实测:同一个
link:安装,旧代码/pocket/meta返回"localInstall": null,新代码返回"localInstall": "link:/…/dsh-pocket-ui"。
[dsh-pocket-ui] host half active, version vX.Y.Z, routes under /pocket)。原先走 ctx.logger,在 dsh web 的 stdout 里根本看不到,于是"host 半区到底加载了没有"无从判断——这正是最难区分的两种情况之一。已是最新 / 无法访问 npm registry / 已关闭更新检查 / 本地安装 · 已是最新 各自有独立文案,不再用一句"未检测更新"冒充结论。测试:
link: 安装的 profile 定位、探针与 bundle 解析一致)。#root[inert]),结果此后每一次命中测试都只返回 <body>,看起来就像"插件的按钮被盖住了"。探针现在会打印命中的元素、元素栈、祖先链几何与 inert 链。gap 与邻行不一致(8px vs 宿主 normal)。v0.1.4 起就是这样,属视觉细节,CDP 里仍会报出来。env(safe-area-inset-top) 在位于状态栏下方的 WebView 里照样返回状态栏高度(实测 ~45px),插件照着补内边距就凭空多出一条空白带。现在安全区实测后再决定是否使用:外壳已被推下(frame.top > 2)或显示模式不是独立/全屏(display-mode: browser,App 内置 WebView 正属此类)时不补顶部 inset;matchMedia 不可用时保守保留。底部 inset 也反向夹紧,避免幽灵外层滚动。?pocket=probe(lib/probe-src.js + /pocket/probe.js):把门控原因、实测 inset、顶部空白的像素高度与归属元素、宿主地标盒子几何打印成可复制的覆盖层。App 里没有控制台,这是唯一能在真机上取证的方式。webServer 就绪;原来只 fetch 一次、失败即静默吞掉,于是页面活着多久这句话就挂多久。现在带退避重试,并区分"正在连接 / 无响应"。未检测更新 不再冒充结论:latest 为空既可能是"已是最新",也可能是"根本没连上 registry",行内文案按 host 半区返回的 updateError / updateDisabled 区分显示。registry.npmmirror.com(国内 NAS 直连 npmjs 经常不通,这正是"未检测更新"的另一半原因)。dsh.client 声明补上 immediately: true,让外壳更早拿到这份 UI 补丁。#4176e6,图标改用独立的 16 单位网格、1.25 笔画(原先 20 单位网格配 1.6 笔画,缩到 28px 会糊成一坨)。描边和图标颜色由同一个 --pocket-accent token 驱动。11 轮 639 步 · 133M tok)同处一条带,实测与 composer 卡片左下圆角重叠 22×8px(状态文字本身未被遮挡)。[role="treeitem"] 行树,所以每一次点击都命中,面板实际是只读的。整条启发式已删除——面板内点击不再被拦截。aria-label="收起侧边栏" 按钮:宿主折叠侧栏后,协调层采纳这个状态来关抽屉。顺带修掉一个隐藏缺陷——原先那个捕获监听器比宿主按钮自己的处理器先跑,会先折叠一次、再被宿主 toggle 回来,导致按钮看起来像坏了。点面板外的遮罩关闭仍然保留。assets/ 收紧到四张 README 用图,避免验证脚本的截图混进 npm 包。html[data-pocket="on"],而设置槽位在任何宽度都会被宿主渲染,于是桌面端没有内边距、没有分隔线、说明文字是 14px。现在行样式不门控,度量照抄宿主官方行(.5px solid var(--dsw-alias-border-l2)、16px 0、标题 14/22、说明 12/18)。修复两个线上缺陷:
webserver: duplicate prefix route "/pocket"。webServer.register() 是服务方法,返回的 disposer 框架不会自动跟踪,之前被丢弃了,路由因此活过卸载。已改为 ctx.effect(() => …) 包裹。同批把更新检查的定时器也从 ctx.setInterval 换成 ctx.effect 里的裸 setInterval(前者绑定在定时器服务的 fiber 上,会活过插件卸载)。row 翻成 column 后,内容列缺了 min-height: 0,无法收缩、撑破面板被裁掉,内层滚动容器拿不到受限高度。同时把 max-height 升级为 88vh + 88dvh 双写。首个版本。
:has() 需要 Chromium 105+;更老的 WebView 上设置 sheet 的定位规则会静默失效(抽屉不受影响)。MIT
FAQs
DSH Web UI 移动端适配(轻量核心版):窄屏下侧栏变抽屉、对话框变底部 sheet、刘海安全区避让、输入区不重叠;鼠标操作的桌面端任何宽度都完全 no-op。
The npm package dsh-pocket-ui receives a total of 591 weekly downloads. As such, dsh-pocket-ui popularity was classified as not popular.
We found that dsh-pocket-ui demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Security News
Cloudflare is acquiring Deno’s team, which will provide another year of bug fixes and security updates before ending support for the runtime.

Security News
Anthropic’s free OSS Scanner uses Mythos and other Claude models to find open source vulnerabilities and send maintainers reports with proposed fixes.

Security News
On Front Lines’ Early Adopters: CISO podcast, Andrew Becherer shares lessons from Datadog, weighs the long-term costs of AI-built tools, and explains how founders can actually gain a CISO’s trust.