Bundle
dsh-top-drag
DSH Desktop client plugin (Windows only): keeps the top of the window draggable and clickable at the same time, by replacing the shipped window-drag overlay with a measuring band that yields to whatever UI is drawn under it.
- Source
- Mo3ter
- License
- MIT
- Updated
- Updated 3 days ago
Readme
# dsh-top-drag
**DSH Desktop 客户端插件(仅 Windows):窗口顶部既能拖动、又不挡点击。**
**DSH Desktop client plugin (Windows only): keep the top of the window draggable and clickable at once.**
> 这是 **DSH Desktop 客户端专用**插件 —— 它针对的是桌面端 preload 注入的那条窗口拖动条。
> 在 DSH Web / 命令行环境里它什么都不做(脚本会先判断平台,然后保持惰性)。
>
> This is a **DSH Desktop client** plugin: it targets the drag overlay the desktop
> preload injects. In DSH Web or a CLI harness it stays inert.
## 中文说明
**它修什么问题**
DSH Desktop 在 Windows 下是自绘标题栏:窗口顶部 36 像素是一条**窗口拖动条**,而程序自己的顶部内容(会话标题行、子代理按钮、插件自己画在顶部的工具栏)就压在这条上。拖动条永远是最上层元素,点击先被它吃掉,所以:
- 窗口最上方一片点不到
- 会话标题 / 面包屑、**子代理按钮**点不到
- 插件(例如记忆系统)在顶部画自己的界面时,按钮同样点不到
- 给这些按钮加 `-webkit-app-region: no-drag` 也没用 —— 上面还盖着一层
**它怎么做**
把顶部这一条切成 **16 个小格子**,每格只看自己那一条竖条:
- 那条里有可点元素(按钮、链接、输入框,以及带 `role` / `tabindex` / `aria-label` 的元素)→ **这一格让位**,只留到元素上边缘(点击优先)
- 那条里什么都没有 → **保留整条 36px**(拖动优先)
app 的布局一个字都不改,插件也不需要任何配合;以后新装的插件在顶部画界面一样会被自动避让。弹出对话框时整条让位。
**开销**
按需测量,不轮询:鼠标进入顶部 44px 内、切换主面板、缩放窗口、手动触发 `dsh-top-band-rescan` 事件,外加 10 秒一次兜底。一次测量约 **4 毫秒**(一次元素查询 + 纯几何计算)。桌面端原有的蒙层保留作兜底,只有测量带成功跑起来才会隐藏它。
**安装**
- 插件市场(收录后)一键安装
- 从 GitHub:`dsh plugin add github:Mo3ter/dsh-top-drag`
**调参**:`lib/index.js` 顶部的 `COLUMNS` / `STRIP` / `SELECTOR` / `MIN_GAP_MS` / `SAFETY_TICK_MS`。
---
## English
On Windows, DSH Desktop draws its own title bar: the window's top 36 px is a
window-drag overlay and the session header sits underneath it. The overlay wins
the hit test, so anything the app — or any plugin — draws there (session
breadcrumbs, the subagent chip, a plugin's own toolbar) cannot be clicked. This
plugin replaces the single overlay with a small **measuring band**: it yields
exactly where UI needs clicks and keeps dragging everywhere else.
Windows only. On any other platform the plugin stays inert.
## The problem it fixes
The desktop preload injects
```
#dsh-desktop-windows-drag-region
position: fixed; top: 0; left: 0; right: calc(caption + 44px)
height: 36px; -webkit-app-region: drag; z-index: 10
```
and pulls the app's own top content up into that strip (`padding-top: 6px` on
the sidebar root and the conversation header), so the session header's title row
starts at `y = 6` — under the overlay. `-webkit-app-region: no-drag` on the
buttons does not help: the overlay is the topmost element and wins the hit test.
The same happens to any plugin that paints a toolbar or panel at the top of the
window.
Narrowing the overlay fixes the clicks but makes the window impossible to grab;
shaping it into a fixed handle turns that handle into a click dead zone. Both
are the same mistake — drag area and content sharing one set of pixels.
## How it works
1. **The layout is left exactly as shipped.** No padding is added or removed.
2. **The band is split into 16 cells.** Each cell looks at its own vertical
slice of the top of the window:
* something clickable in the slice → the cell collapses to just above it
(clicks win),
* nothing there → the cell keeps the full strip (drag wins).
3. The preload's own overlay stays as the fallback and is hidden only once the
band reports ready, so a broken script degrades to the shipped behaviour
instead of losing window dragging.
"A clickable thing" is measured from real geometry: one `querySelectorAll` over
`button, a[href], input, select, textarea, summary, label, [role], [tabindex],
[aria-label], [contenteditable], [data-dsh-no-drag]`, then a rect per candidate.
No plugin has to opt in, and no per-surface patch is involved.
## Cost
Measurement needs geometry, so it is on demand only — nothing polls:
| trigger | why |
| --- | --- |
| pointer within 44 px of the top | the answer is fresh before a click or drag can land |
| main panel switch | a plugin page usually brings its own top bar |
| window resize | the columns move |
| `window.dispatchEvent(new Event('dsh-top-band-rescan'))` | manual, from any button or script |
| one 10 s safety tick | catches changes no pointer movement announces |
A sweep over 385 candidates measures ~4 ms on a long session (one query ~1.5 ms,
rects ~2.4 ms). Hit-testing per point was tried first and cost 245 µs *per
point* — ~37 ms per sweep — which is why the band measures rects instead, and
why `MutationObserver` is deliberately absent (it fires during chat streaming).
## Install
* Through the DSH plugin market, once listed.
* From GitHub: `dsh plugin add github:Mo3ter/dsh-top-drag`
* From npm: `dsh plugin add dsh-top-drag`
* Locally from a checkout: `dsh plugin add file:./dsh-top-drag`
Harness restart required for a host-plane plugin; the DSH Desktop app picks it
up on its next launch.
## Configuration
Everything lives at the top of `lib/index.js`:
| constant | meaning |
| --- | --- |
| `COLUMNS` | band cells (16) |
| `STRIP` | band height in px (36) |
| `PILL_*` | the small centred hint handle drawn when the middle column is free |
| `MIN_GAP_MS` | minimum gap between two sweeps (700) |
| `SAFETY_TICK_MS` | the safety tick (10000) |
## Caveats
* Windows only, and it targets the desktop preload's own overlay id and the
`dsh-desktop-windows-titlebar-layout` body class. If a future DSH Desktop
version changes either, the plugin simply stays inert rather than breaking
anything — it adds no padding and hides no element it did not measure.
* Dialogs (`dialog[open]`, `[role=dialog]`, `[aria-modal=true]`) suppress the
whole band, so a modal always keeps every click.
* It adds no telemetry, no network access, and no client-side bundle: the whole
plugin is one host file that contributes one `<style>` and one `<script>` row
to the served `index.html` through `webserver/index-inject`.
## Uninstall
`dsh plugin remove dsh-top-drag`, or remove the row from the profile's
`cordis.patch.yml`. Nothing is written outside the profile.
## License
MIT
Install
dsh plugin --profile web add github:Mo3ter/dsh-top-drag
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install dsh-top-drag from the hub
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.