dsh-image-annotate
Image Annotate
DSH 网页插件:点击编辑器中的待处理图像进行标注(画笔、方框、箭头以及可移动/调整大小的文本框),并将标注后的 PNG 作为新附件插回。
插件会安装到这里;不确定时保持 web。
npx -y @deepseek-ai/dsh plugin --profile web add github:janewas/dsh-image-annotate#f17ce39fc5564265c53e93dcb44496a9d310f30d


dsh-image-annotate
DSH 网页插件:点击编辑器中的待处理图像进行标注(画笔、方框、箭头以及可移动/调整大小的文本框),并将标注后的 PNG 作为新附件插回。
插件会安装到这里;不确定时保持 web。
npx -y @deepseek-ai/dsh plugin --profile web add github:janewas/dsh-image-annotate#f17ce39fc5564265c53e93dcb44496a9d310f30d



Image Annotate 以 dsh-image-annotate 发布,当前版本为 0.4.1。Plugin Hub 会校验它的 manifest,并保存精确安装来源,便于复现安装结果。
A DeepSeek Harness (dsh) web plugin: click a pending image in the composer to annotate it, then insert the annotated copy back as a second attachment. The original stays.
English | 中文
Sending a screenshot to the agent usually means circling something first. The composer's pending images were preview-only, so the circle had to happen in another app and the file had to be re-attached. This plugin moves that step into the composer.
Minimal footprint. Nothing in DSH's own UI is restyled: the attachment rail, drop target, file cards and the original-image lightbox stay exactly as shipped — the plugin only takes over the click on a pending thumbnail. One browser-side file, an empty host stub, no host routes, no dependencies, no configuration files, and no work at all until you click an image.
<name>-<suffix>.png, next to the image it came from. The annotated file is an ordinary attachment — it can be opened and annotated again.Result: the annotated copy is added next to the original
Requires DSH 0.1.5-rc.1 or a compatible release (dsh web, profile web); verified on that version.
dsh plugin --profile web add github:janewas/dsh-image-annotate
This adds the package to the profile's bundle stack, so the plugin's own cordis.patch.yml mounts it. The bundle list is read at startup, so restart dsh web afterwards, then refresh the page.
Download dsh-image-annotate.tgz from Releases, then:
tar -xzf dsh-image-annotate.tgz
cd package
bash install.sh # pass a profile name for anything other than web
install.sh is idempotent. It links the directory into ~/.dsh/profiles/web/node_modules/ and appends the mount row to that profile's cordis.patch.yml, backing the file up first. Profile patches apply live, so just refresh the page — no restart. Keep the extracted directory where it is; the profile links to it.
Both routes end in the same mount. The row written by install.sh disables itself automatically if the package is later installed as a bundle, so following both does not double-mount.
| Tool | Interaction |
|---|---|
| Pen | drag to draw freehand |
| Box | drag from one corner to the other |
| Arrow | drag from tail to head |
| Text | drag a frame, type inside it; Enter confirms (Esc cancels, clicking elsewhere confirms, Shift+Enter adds a line) |
| Move / resize a frame | with the text tool: drag the frame body to move, drag one of the eight handles to resize. The text re-fits live, and the frame stops shrinking when the text would no longer fit at the minimum readable size |
| Edit / delete a frame | double-click inside it to change the text (clearing the text deletes the frame); select it and press Delete. Ctrl/Cmd+Z undoes a move, a resize, a text change or a delete |
Undo and Clear sit in the toolbar (Ctrl/Cmd+Z also undoes). Both stay available while you are typing inside a frame: Undo discards the text you have not committed yet, Clear discards it and wipes the committed annotations; they are greyed out only when there is genuinely nothing to undo or clear. Close with Esc, the 关闭 button, or a press on the backdrop.
Settings → 图片标注, after the built-in sections.
| Setting | Default | Effect |
|---|---|---|
| 启用点击编辑 (Enable click-to-edit) | on | Master switch. Turned off, clicking a thumbnail opens DSH's original-image lightbox again and the plugin does nothing. |
| 默认工具 (Default tool) | pen | Tool pre-selected when the dialog opens. |
| 默认颜色 (Default colour) | red | Starting colour for pen, box, arrow and text. |
| 默认线宽 (Default width) | medium | Line width for pen, box and arrow. Text size is not set here — the frame decides it. |
| 文字框形状 (Text frame shape) | rounded | Shape of the frame a new text box is drawn with. |
| 文字框底纹 (Text frame fill) | tinted | Transparent keeps only the border and the text; tinted and solid fill the frame so the type stands out. |
| 插入方式 (Insert mode) | append | append keeps the original; replace removes it once the annotated copy is in. |
| 标注图文件名 (Output file name) | 标注 | <name>-<suffix>.png. Leave empty to only change the extension. |
「恢复默认」 resets everything. Settings live in the browser's localStorage under dsh-image-annotate.settings.v1: they apply to that browser and origin only, so a different origin (a LAN or Tailscale address, say) starts from the defaults. The plugin writes no host configuration.
Two halves, in lib/:
lib/index.js — the host half, deliberately empty. Its only job is to give the loader an entry, so dsh-client-modules scans the package's dsh.client declaration and puts lib/client.js in the browser plugin graph.lib/client.js — the browser half; everything else lives here.Key points for anyone changing it:
conversation.input.overlay, a session-scoped list slot. Its standard props supply sessionId and inputActions; the component renders null until a thumbnail is clicked, and installs one capture-phase click listener. The editor is portalled to document.body, because the composer card establishes a containing block for position: fixed.click whose target is an <img> inside [data-composer-card] and whose src equals a draft attachment's previewUrl. Pending thumbnails use their own object URL, so the match is exact and never catches a transcript image. A hit calls stopPropagation(), which suppresses DSH's own lightbox for that click only.conversation.createDrafts(sessionId, [file]) registers the file, then inputActions.addAttachments(ids) appends it through the same admission path the paperclip uses; a refusal (a submission in flight) rolls back and reports inside the dialog.toBlob('image/png'). Display size is CSS only, so the exported bitmap is always the original resolution.<textarea> is then laid over it, styled to match the committed result. Line breaking and the font size come from one solver shared by the preview and the export: fitTextInBox wraps the text token by token (CJK characters break anywhere, Latin stays whole and is hard-split only if a single word cannot fit) and binary-searches the largest size whose wrapped lines still fit inside the frame — if even the minimum size cannot fit, the frame grows taller instead of clipping. Enter/blur commits {type:'textbox', x, y, w, h, text, color, shape, fill}; the frame is filled and stroked, and the text is centred inside with a contrasting colour over a solid fill and an outline over anything lighter.settings.section and stored with a small localStorage + useSyncExternalStore store that the dialog reads each time it opens.react, react-dom); the host half imports nothing. is therefore empty and there is no load-order constraint.Remove the mount row (id: image-annotate for the bundle route, id: image-annotate-manual for install.sh) from ~/.dsh/profiles/web/cordis.patch.yml, drop the node_modules/dsh-image-annotate link if install.sh created one, and refresh the page. For dsh plugin installs, run dsh plugin --profile web remove dsh-image-annotate and restart.
MIT
dsh.client.inject