dsh-screenshot
A screenshot-and-ask plugin for DeepSeek Harness (DSH).
One click in the composer captures the screen, a frozen frame appears with a box you can drag and then fine-tune, and the crop lands in your message as an ordinary image attachment — ready for the question you were about to type.
Unlike most capture tools, the frame excludes the Harness window itself, so you can point at whatever is behind it.
Why this exists
Asking a question about something on screen usually means: take a screenshot with the system tool, find the file, drag it into the chat, then type. Four steps and a detour through the filesystem. This collapses that to one click.
The overlay is an in-page surface, not an OS window, so nothing is left behind: no tray entry, no floating window, no files to clean up.
Features
- One click, no dialog — the camera button sits in the composer's tool row.
- Adjustable selection — after the first drag the frame stays, with eight handles (four corners, four edges) and a draggable interior. Move it until it is right; nothing is cropped until you confirm.
- Confirm by button or key —
Enteror the confirm button;Esc, right-click, or the cancel button discard the whole capture. - Live size readout in real pixels, not screen pixels.
- Harness window excluded — the app's own windows are minimized for the freeze and restored immediately after, so the frame holds only other applications.
- Fast — a resident capture helper keeps a warm process, the window list is cached, and the freeze settles in ~45 ms instead of 320 ms, so a whole capture is roughly 130–180 ms. See Performance.
- Smooth — the selection is one element with a
box-shadowdim, and pointer moves are coalesced to one commit per animation frame, so the frame tracks the cursor at display rate instead of refreshing the DOM on every event. - Nothing to clean up — the crop goes straight into the draft as a browser attachment; no screenshot directory, no orphan files.
Requirements
- Windows — the capture helper uses PowerShell 5.1 with WinForms and GDI+.
- DSH with the Web GUI (
dsh webor the desktop app). The plugin needs thewebServerHost service and the Conversation Client services, both present in a standard web profile. - A model that accepts images, if you want the model to see the crop.
Install
dsh plugin --profile <profile> add github:<your-account>/dsh-screenshot
Then restart DSH so the Host half registers its route and starts the helper.
For local development, install the directory instead:
dsh plugin --profile <profile> add ./dsh-screenshot
Usage
- Click the camera button at the left of the composer tool row.
- The screen freezes. Drag to sketch the area you want to ask about.
- Release. The frame stays — drag its edges, corners, or interior to adjust.
- Press
Enteror click Confirm.Esc, right-click, or Cancel discards. - The crop appears as an image attachment in your draft. Type your question and send.
How it works
Two halves, one flow.
Host half (index.js) owns a long-lived PowerShell helper
(scripts/capture.ps1) and two exact-match same-origin HTTP routes,
/api/dsh-screenshot/capture and /api/dsh-screenshot/crop. The helper reads
newline frames on stdin:
| Frame in | Meaning |
|---|---|
<id>|ping |
health check |
<id>|shot|<hideApp>|<hideDelayMs> |
capture one frame |
<id>|crop|<srcPath>|<l>,<t>,<w>,<h>|<dstPath> |
cut one rectangle out of a frame |
and answers OK\|<id>\|<w>\|<h>\|<bytes>\|<path>\|<sx>\|<sy>\|<sw>\|<sh>
or ERR\|<id>\|<reason>.
Before freezing, the helper minimizes every visible top-level window owned by
this app, waits for the frame to settle, captures the virtual desktop, then restores
each window to its previous state and foreground. The routes require the
application's own auth cookie, so an unrelated page on the machine cannot read
the screen through them. Capture answers with the raw PNG bytes plus
x-shot-width, x-shot-height and x-shot-id headers; crop answers with the
raw PNG bytes of the rectangle plus x-crop-width and x-crop-height.
Frames stay on disk between the two calls (at most six, oldest evicted) so the crop never has to round-trip the pixels through the browser. Each frame is consumed by one crop and deleted with it.
Client half (client.js) registers two slots:
conversation.input.left— the camera button.shell.overlay— the frozen-frame selection surface, mounted only while a capture is pending.
On confirm, the selection is mapped back to the screenshot's own pixels through
the same object-fit: contain letterbox math used to display it, and the Host
cuts that rectangle out of the frame it already holds. The result is handed to
the composer as a File through the Conversation service's draft attachments.
It behaves exactly like a pasted picture.
Performance
A capture was profiled step by step, because "it feels slow" turned out to be four separate costs rather than one. Measured on a 2560×1600 desktop at 150% DPI:
| Step | First cut | Now |
|---|---|---|
Get-Process window-owner sweep |
79 ms | 0 (cached, 20 s TTL) |
EnumWindows |
25 ms | 0 (cached, verified with IsWindow) |
| Wait for the minimize animation | 320 ms | 45 ms |
CopyFromScreen 2560×1600 |
45 ms | 45 ms |
| PNG encode | 45 ms | 45 ms (buffer reused) |
| Post-restore settle sleep | 80 ms | 0 (restore is async) |
| Total | ≈520–600 ms | ≈130–180 ms |
The other half was the Client:
- The route used to answer with the PNG base64'd inside JSON, so the browser
had to parse a multi-megabyte string and then base64-decode a
data:URL before the first pixel could appear. It now answersimage/pngand the Client handsURL.createObjectURLstraight to<img>, revoking it on teardown. - The overlay used to be mounted only after
requestFullscreen()resolved, which put Chromium's full-screen animation in front of the user. The freeze is now shown first and the window widens behind it (measure()re-runs onfullscreenchange). - The dim was four separately positioned translucent rectangles repainted on
every pointer move. It is now a single
box-shadow: 0 0 0 100vmaxspread on the selection frame itself, with the eight handles as CSS-positioned children, so one element's geometry change is the entire per-frame cost. getBoundingClientRect()was called on every pointer move (a forced layout). The overlay isposition: fixed; inset: 0, so viewport coordinates already are layer coordinates.
And one cost that only showed up after confirming, as a delay before the picture appeared in the composer:
- The crop used to run in the browser —
drawImageof a four-megapixel frame into a canvas, thencanvas.toBlob('image/png')— both on the renderer's main thread, in the same breath as the composer repaint and the window leaving full screen. The pixels were already on disk, so the crop moved there: the warm helper cuts the rectangle out with GDI+ and streams the PNG back. Attaching was never the slow part; the composer only builds an object URL and defers the bytes to send time.
Configuration
No configuration. The plugin registers no Config schema and reads no files
outside the workspace.
Layout
dsh-screenshot/
├── index.js # Host half: helper lifecycle + capture route
├── client.js # Client half: camera button + selection overlay
├── scripts/capture.ps1 # Resident PowerShell helper (WinForms + GDI+)
├── cordis.patch.yml # Bundle patch declaring the plugin row
├── locale/{en,zh}.json # Plugin manager title/description
├── icon.svg # Plugin artwork
├── package.json # dsh.bundle + dsh.client manifest
└── LICENSE
Notes and limits
- Windows only. The Host half has no capture backend for macOS or Linux, so the route answers with an error there.
- One screen, whole desktop. Multi-monitor setups are captured as one virtual desktop image; the selection frame spans all of them.
- Independent PowerShell consoles may still appear in the frame even when hidden, depending on how their host renders them.
- Hiding the app's windows momentarily changes focus; the previous foreground window is restored right after the capture.
Security
- The capture route requires the app's auth cookie and is exact-match only.
- The screenshot is processed in memory and handed to the composer as an attachment; the helper's temporary PNG is deleted as soon as it is read.
- No network calls, no bundled keys, no external services.
No comments yet. Be the first to write one.