# dsh-annotation
**English** · [简体中文](./README.zh-CN.md)
Selection-annotation plugin for DSH Web: select text → annotate → press Enter to send it along with your message; the model replies to each annotation by number.
🌐 Live Product Site — Explore dsh-annotation in DSH
Select any text in an assistant reply to annotate it (the annotation body may be left empty = just mark the passage). Annotations accumulate across messages and turns. An **Annotations ×N** chip appears next to the input box — hover to view all annotations, remove them one by one. Press Enter and the annotation block goes to the model together with whatever question is in the input box. **The annotation block never shows up as text in your own message bubble** — only the question plus the chip (content visible on hover; hidden before paint, zero flicker). The model replies with `Annotation 1: …` … `Annotation N: …`, one per annotation, and every Annotation label in the reply is a hoverable chip showing the annotated passage and your note.
Form: official **bundle plugin** (`dsh.bundle` + a `dsh.client` declaration in package.json, injected into the browser via client-modules; the Node half is an empty implementation). **Zero core changes** — no DSH files are touched; `cordis.patch.yml` only inserts its own id once, and the profile patch stays `[]`.
## Features
| Feature | Description |
|---|---|
| Select-to-annotate | Select assistant text → toolbar "Annotate" → write your note (may be empty); dismiss by clicking elsewhere or pressing Esc |
| Numbered marker + highlight | A blue numbered marker + highlight anchored to the passage, viewport-anchored with collision avoidance, never lost when scrolled out of view |
| Cross-turn collection | Any number of annotations accumulate across messages/turns, numbered from 1 |
| "Annotations ×N" chip | Small chip beside the input box; hover shows every annotation, deletable individually |
| Enter sends with your message | Annotation block + the question in the input box are sent to the model together (the model receives the full content) |
| Hidden in your bubble | The annotation block is removed from your bubble's DOM the moment you send (before the browser paints), leaving only the question + the chip (hover to view); historical messages self-heal after a refresh |
| Numbered reply correspondence | A format instruction is injected into the message so the model replies `Annotation 1: …` … `Annotation N: …` one by one |
| Reply annotation chips | `Annotation N:` in the reply renders as hoverable chips showing the passage + your note |
## Interaction flow
```
Select assistant text ──▶ Toolbar "Annotate" ──▶ Write note / save empty ──▶ Blue numbered marker + highlight
▲ │
└────────────── any number, accumulate across turns ◀────┘
│
▼
"Annotations ×N" chip beside the input (hover to view / delete)
│
Press Enter
▼
Model receives: annotation block (number + passage + note) + your question
Your bubble: question only + "Annotations ×N" chip (zero flicker)
Model reply: Annotation 1: … Annotation 2: … (hoverable chips)
```
## Install (official bundle path · the only one)
```sh
# Public npm package (works without an npm account)
dsh plugin --profile web add @changfenhuang/dsh-annotation
# Or install directly from the public GitHub source
dsh plugin --profile web add git+https://github.com/omdsh-dev/dsh-annotation.git
# local path install (development / debugging)
cd /path/to/dsh-annotation
dsh plugin --profile web add .
# restart the web service — see "Restarting the web service" below
```
To add it only as a Node dependency in an existing project:
```sh
npm install @changfenhuang/dsh-annotation
```
> `npm install` only adds the dependency; it does not register the plugin with DSH. Use `dsh plugin add` above when installing it into DSH.
### Migrating from the old `@omdsh-dev` package name
If you installed the plugin before v1.4.2, remove the old dependency before installing the renamed package:
```sh
dsh plugin --profile web remove @omdsh-dev/dsh-annotation
dsh plugin --profile web add @changfenhuang/dsh-annotation
```
If `dsh web` still fails and mentions `@omdsh-dev/dsh-annotation`, remove only the stale `dsh-annotation` entry that uses that old name from `~/.dsh/profiles/web/cordis.patch.yml`. The plugin now supplies the `@changfenhuang/dsh-annotation` entry itself.
| Do | Don't |
|----|------|
| Only `dsh plugin add` / only write `bundles` | **Never** insert the same id again in the profile/home `cordis.patch.yml` |
Self-check:
```sh
dsh --profile web --dump-config | rg "id: dsh-annotation" # must be exactly 1 line
dsh web --no-open # open the printed one-time token URL in a browser
```
In the browser console, `window.__DSH_BOOT__.entries.find(({ id }) => id === '@changfenhuang/dsh-annotation')` must return an entry with a `url`. dsh 0.1.2 no longer exposes the old bare per-plugin URL.
## Restarting the web service
Pick the command for your platform:
```sh
# macOS (launchd)
launchctl kickstart -k "gui/$(id -u)/com.dsh.web"
# WSL / Linux with systemd user services
# The unit name may differ by install method; check with:
# systemctl --user list-units | rg dsh
systemctl --user restart dsh-web
```
Environments without a service manager (e.g. some containers) often need **no restart at all**: `client.js` is served per request with no caching, so a hard refresh (Cmd/Ctrl+Shift+R) picks up plugin changes. The self-check commands above are platform-neutral.
## Architecture notes
- **Pure browser-side**: everything lives in `client.js` (a hand-written CJS bundle, no build step, served no-cache per request)
- **Message format** (the literal protocol block sent to the model; follows the DSH `locale` preference — zh or en):
```
zh: 我批注了以下 N 处内容…\n\n1. 原文\n 批注:…\n\n请用「Annotation 1:…」…\n\n提问:
en: I annotated the following N passage(s)…\n\n1. quote\n Note: …\n\nPlease respond… "Annotation 1: …"…\n\nAsk:
```
The zh delimiter is 「提问:」(ask:) rather than 「问题:」(question:) — the heading line "回答我的问题:" also contains the latter, and the bubble-hiding surgery would misfire on it; the en delimiter is `Ask:`. Hiding and reverse-parsing accept both languages plus the legacy 「问题:」 marker.
- **Bubble hiding**: user bubbles are plain-text rendered (a single MessageText node, not markdown); a MutationObserver in the microtask phase (before paint) splits at the last `\n提问:`, cuts the annotation block, and attaches the chip; a 1 s polling fallback plus historical-message repair after refresh
- **Reply chips**: after streaming settles (`data-streaming` removed), each `Annotation N:` is replaced with a hoverable chip; item data is stored on the most recent user message carrying the annotation tag (`tag.__annotationItems`) and rebuilt after refresh; **snapshot the text nodes collected by the TreeWalker before touching the DOM, then replace one by one** — replacing a child mid-walk invalidates the walker pointer and only the first node gets processed
- **Locale-aware**: UI copy and the protocol block follow DSH's `locale` service (`zh`/`en`, live switch); historical bubbles stay parseable across languages; missing locale service falls back to zh
- **IME-safe**: the Enter interception carries `isComposing` / keyCode 229 guards; never hard-edits the composer textarea's DOM; `setDraft` only assembles the annotation block at the last moment before submit and never clobbers the user's draft
- **No reliance on send-completion event chains**: bubble decoration uses MutationObserver + polling (`watchInputDraft` can be ineffective before the session is loaded at init; it is only a staging entry)
- **Focus-chat compatible**: works inside the focus conversation view of [dsh-focus-chat](https://github.com/dingyi222666/dsh-focus-chat) — assistant rows there are `[data-focus-flow]` containers with a `*_assistant` CSS-Modules class (plus `data-streaming` while running); selection, annotation, reply chips, and re-anchoring all work in the focus tab alongside the main chat view
## Version history
| Version | Highlights |
|---|---|
| v1.4.x | Locale-aware: zh/en UI copy and annotation protocol block, live switch via DSH `locale` service |
| v1.3.x | Numbered reply correspondence: format-instruction injection + hoverable `Annotation N:` chips (TreeWalker snapshot fix) |
| v1.2.x | Hidden annotation block in bubble: MutationObserver microtask zero-flicker + polling fallback + historical-message repair |
| v1.x | Self-contained annotation flow (replaces the v0.9 chip design): capture-Enter assembles the block and sends it with the message |
| v0.9.x | Early chip design (insertReference + slash codec), superseded by v1.x |
## Friendly links
- [Linux.do](https://linux.do)
## License
MIT