Bundle
dsh-image-annotate
DSH web plugin: click a pending image in the composer to annotate it (pen, box, arrow, text) and insert the annotated PNG back as a new attachment.
- Source
- janewas
- License
- MIT
- Updated
- Updated yesterday
Readme
# dsh-image-annotate
A [DeepSeek Harness](https://github.com/deepseek-ai/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** | [中文](README.zh.md)
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.

## Features
- **Click a pending image** in the composer to open the annotation dialog instead of the plain original-image lightbox. The dialog shows the image large *and* lets you draw on it.
- **Four tools**: pen (freehand), box, arrow, and text. Text is typed in place — click a spot, type, press Enter; no mouse lettering.
- **Text comes with a label**: new text is drawn as a text-coloured pill with contrasting type, so it stays readable on any photo and is still visible in the 64 px thumbnail. The toolbar's 底框 button turns that off, leaving outlined plain text.
- **Six colours** (red, yellow, green, blue, white, black) and **three widths** (thin / medium / thick). While the text tool is active the same three buttons select the font size (small / medium / large). Width and font size are stored as a fraction of the image's short side, so they look the same on a phone screenshot and on a 4K capture.
- **Undo** (button or Ctrl/Cmd+Z), **clear**, Escape or backdrop press to dismiss.
- **Insert as a new attachment**: the result is added to the composer as `<name>-<suffix>.png`, next to the image it came from. The annotated file is an ordinary attachment — it can be opened and annotated again.
- **Settings page** (Settings → 图片标注) with the plugin's defaults; see below.
- Works in light and dark themes, and does not restyle any of DSH's own UI.

## Install
Requires DSH `0.1.5-rc.1` or a compatible release (`dsh web`, profile `web`); verified on that version.
### From GitHub (recommended)
```sh
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.
### From the release tarball (no pnpm, no restart)
Download `dsh-image-annotate.tgz` from [Releases](https://github.com/janewas/dsh-image-annotate/releases/latest), then:
```sh
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.
## Usage
1. Add an image to the composer as usual (drag it in, or use the paperclip).
2. Click its thumbnail. The annotation dialog opens with the image at full size.
3. Draw: pick a tool, a colour and a width; for text, click a spot and type.
4. Click 「插入到输入框」 (Insert into composer). The annotated PNG appears at the end of the attachment rail, and the original is still there.
| Tool | Interaction |
|---|---|
| Pen | drag to draw freehand |
| Box | drag from one corner to the other |
| Arrow | drag from tail to head |
| Text | click a spot, type, Enter to confirm (Esc cancels, clicking elsewhere confirms) |
Undo and Clear sit in the toolbar (Ctrl/Cmd+Z also undoes). Close with Esc, the 关闭 button, or a press on the backdrop.
## Settings
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; doubles as the font size (small / medium / large) for the text tool. |
| 文字默认加底框 (Text label on by default) | on | Whether new text is drawn as a filled pill. Still switchable per dialog. |
| 插入方式 (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.

## How it works
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:
- **Mount point**: `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`.
- **Hit test**: a `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.
- **Inserting**: `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.
- **Canvas**: a base canvas holds the image at its natural size and is painted once; an overlay canvas holds the annotations and is repainted whole. Shapes are stored in image pixels with width/size as a short-side fraction, and exporting composites both onto one canvas for `toBlob('image/png')`. Display size is CSS only, so the exported bitmap is always the original resolution.
- **Text**: clicking with the text tool places a `<textarea>` styled to match the final pill; Enter or blur commits `{type:'text', x, y, text, color, textFactor, boxed}`. While it has focus, Escape and Ctrl+Z go to the textarea rather than the dialog.
- **Settings**: registered into `settings.section` and stored with a small `localStorage` + `useSyncExternalStore` store that the dialog reads each time it opens.
- **No dependencies**: the browser half requires only the platform seed modules (`react`, `react-dom`); the host half imports nothing. `dsh.client.inject` is therefore empty and there is no load-order constraint.
## Known limitations
- Pending composer images only. A transcript image still opens DSH's lightbox.
- Export is always PNG: animated GIFs lose their animation, and very large images (tens of megapixels) use a lot of memory.
- Text is single-line and cannot be moved or re-edited after it is committed (undo and retype instead).
- No crop or rotate.
- One image at a time.
## Uninstall
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.
## License
MIT
Install
dsh plugin --profile web add github:janewas/dsh-image-annotate
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-image-annotate from the hub
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.