dsh-testhud
自动化测试屏幕进度 HUD 插件 适合运行自动化测试时,需要直观监控测试步骤、结果和耗时的用户。
安装
npx -p @deepseek-ai/dsh dsh plugin --profile web add github:yijiezhong/dsh-testhud说明文档
阅读完整 README ↗Configuration
Optional row config in your profile's cordis.patch.yml overrides the plugin row:
- id: testhud
config:
announceToAgent: true # inject the short "report long tests on screen" convention into every session
defaultAnchor: auto # auto | center | left | right
topInset: 155 # how far the top anchors drop below the screen top, in points
Where the panel goes is decided in this order:
- Do not cover the thing under test. The panel scores three positions against the frontmost app's windows and takes the one that overlaps least. This outranks everything below.
- Centre of the screen, when that blocks nothing.
- Otherwise the side — left before right.
Vertically it always sits high: the top edge sits topInset points below the screen top (155 by default), the bottom edge stops above the status bar / Dock (Look.bottomInset, 44 pt). Horizontally it stays off the edges (Look.sideInset, 28 pt) so it cannot sit on a sidebar or a scrollbar. Anchors are auto | center | left | right; the old top-left / top-right names still map to left / right.
topInset exists because the panel usually floats over a browser: the top ~150 pt of the screen are the tab strip, the address bar and the bookmarks bar, and a panel there would cover them. The default (155) starts the panel just below that chrome — level with the page's own header. 0 degrades to "clear the menu bar, then keep a 14 pt screen margin". The value is measured from the screen top (positioning uses the full frame, not visibleFrame), so what you pass is what you get. DSH_TESTHUD_TOP_INSET overrides it for the CLI.
Size and overflow
Both dimensions follow the content (HUD.layout()):
- Width is the widest line at its natural (unwrapped) width plus the left and right insets (
Look.inset, 16 pt), clamped to a floor of 460 pt (Look.minWidth) and a ceiling of the screen's usable width minus 28 pt on each side (Look.sideInset). - Height follows the content, and is exactly the content height while it fits; the ceiling is the span from the panel's top edge (
topInsetbelow the screen top, 155 by default) down to the bottom of the usable area minus 44 pt (Look.bottomInset) — no longer 62% of the screen height. Yellow collapses the panel to just the band; green expands it to that ceiling and to the full usable width.
The height has a ceiling, so with enough content the upper part is pushed outside the visible area. The panel's content area then carries a transparent drag hit layer (the ScrollHandle window plus its ScrollGrip view): press and pull downwards to bring the upper content back (natural scroll direction), and on release it eases back to the bottom over 0.28 s ease-out — "the last line returns to the bottom of the window". The hit layer only exists when content is actually cut off (its height is 0 otherwise), so a panel whose content fits stays fully click-through. The cursor shows an open hand over it and a closed hand while held, reverting on release. Observe it at ~/.dsh/dsh-testhud/scroll.json (scrollY / maxY / atBottom / panning / bouncing / cursor — the last one because screenshots do not contain the mouse pointer, so the cursor can only be verified this way).
Colours come from the Apple swatch only
Every colour on the panel must exist in the Apple swatch — the 22 colours on page 1 of the swatch (Apple色板/1Apple配色色卡.key, on this machine under $HOME/PARA/8.Code/AIDoc/设计规范/): 9 neutrals + 3 blues + 10 functional. The single source of truth in code is ApplePalette in hud/testhud.swift, all built in sRGB (calibrated* constructors shift the values).
Chameleon V2 (2026-10-09): the panel's ground is fully transparent — no fill, no backdrop snapshot; only text and a hairline border remain. Whatever is underneath stays visible, and the panel's own text is kept legible on any backdrop by three things (stroke + shadow + colour):
| Part | Light appearance | Dark appearance |
|---|---|---|
| Panel ground | translucent (alphaFloor 0.50) | same |
Border (1.0 pt, Look.panelBorderWidth) | black #000000 | white #FFFFFF |
| Body (title / step name / conclusion / expect / actual — one colour) | picked adaptively, see below | same |
| Text shadow | off by default (textShadowBlur = 0) | same |
off by default (strokeWidthPercent = 0) | same | |
| Step marks | ✓ #34C759 / ✗ #FF3B30 / ⏳ #FF9500 / • same as body | same |
The body colour is picked adaptively (Theme.pickTextColor, 2026-10-09) — the backdrop is classified into four cases, and within a case only one pairwise contrast comparison is made:
| Backdrop | Picked |
|---|---|
| itself coloured (site blue, warm orange, deep green…) | whichever of black/white contrasts more (a coloured candidate tends to blur into a same-hue backdrop) |
| neutral but mid-luminance (pure mid grey) | black/white (blue only reaches 2.5:1 on mid grey) |
| neutral, bright | the better-contrasting of Apple blue #0071E3 and indigo #5856D6 |
| neutral, dark | the better-contrasting of bright blue #2997FF and mint #00C7BE |
Across eight measured backdrops (white-with-black-text / black-with-white-text / pure white / pure black / mid grey / site blue / warm orange / deep green) it picks indigo / mint / indigo / mint / black / black / black / white, each confirmed legible by eye. Hierarchy no longer rides on grey levels: it is all font weight (one size, four weights).
The shadow takes "the backdrop's pole" (white in the light scheme, black in the dark one): it blends into the backdrop, and its job is to push the backdrop's own strokes out from under the panel's letters. The stroke is off by default (strokeWidthPercent = 0) — three variants were compared side by side: a stroke makes 18 pt glyphs look fat and grubby, whereas a pure shadow stays clean and reads just as well. The capability is kept (set a negative value to enable it).
The band ignores the light/dark split and follows the state: running = red #FF3B30 + white text, done = green #34C759 + black text; the band's opacity is 0.75. The three traffic lights are swatch values too (red #FF3B30, yellow #FFCC00, green #34C759) with a 1.0 pt white stroke. The band is the only opaque piece left.
The step marks are ✓ / ✗ / ⏳ / • and not ✅ / ❌: colour emoji ignore foregroundColor and paint colours of their own, outside the swatch, so the glyph had to change. By the same token, emoji the caller (an agent) writes into a step name, expect or actual are not governed by ApplePalette.
Only one thing is still computed: which appearance to use. It reads the median luminance of the sampled rectangle (not the mean — dense text lifts the mean, which made a dark backdrop with white text look "light" and produced black panel text). The threshold 0.60 is empirically calibrated: what ScreenCaptureKit returns has been tone-mapped (measured: pure white 1.000 comes back as 0.886, the dark grey #1D1D1F at 0.106 comes back as 0.373), so comparing against the textbook 0.5 inverts the decision. The backdrop is sampled every 0.7 s — the rectangle the panel is about to cover, and only that rectangle, plus immediately whenever the frontmost app changes or the panel resizes or changes content.
Two earlier palettes remain in the source unused (banner() / tinted() / textColor(), plus the "translucent ground + backdrop snapshot" route); ask the user before reviving them.