dsh-batch-review
Per-item review cards for DeepSeek Harness, built on the host's own question UI.
One batch_review tool call hands the agent's checklist to the Web UI — native question cards for a single item, a one-screen panel in the input dock from 2 items up — and returns one structured result: a verdict and a note per item, plus a ready-to-read report. The panel is served by the host's own web server under /api/batch-review/*, so there is no separate app to open and no extra port — and a blank item never counts as approved.
agent → batch_review({ title, items: [6 items] })
→ one-screen panel (≥2 items, panel mounted) or native cards
→ { items:[{verdict,note,filled}], summary, report_text }
Why
An agent that needs a verdict on six checklist items has two bad options today:
- six
ask_user_questioncalls — six interruptions, and the agent must stitch the answers back together; - asking in prose — the user replies "1 ok, 2 ok, 4 broken", and the model re-parses text into state.
batch_review is the third option: one call, the host's own cards, one machine-readable result. The failure mode it exists to prevent is the quiet one — an unanswered item being treated as "approved".
What it does
batch_reviewtool. The agent sends the whole batch in one call. Each item becomes oneAskUserQuestionItemon the host's native card; the answers come back asitems[].{id,title,type,verdict,note,filled}plussummaryand areport_textthe model can paste into its reply.- Fail-closed blanks. An item the user skipped or left blank returns
filled: false. The report lists it under "未填" with the explicit note that it is not a pass. A cancelled session returnscancelled: trueand no items at all — never a silent pass. - Five item types.
confirm(通过 / 不通过 / 待定),bool,choice,text,scale. - Multi-select on request. Set
multiSelect: trueon any item that accepts more than one label; the answer keeps every label inverdict(joined with、) and in the newselected[]array. - Bundled skill. The package also registers the
batch-review-tableskill (decision-channel routing: when to useask_user_questionvsbatch_reviewvs a full HTML page) so one install covers both halves. - HTML fallback. For large reviews or when a persisted result is needed, the skill ships a self-contained
review.html+build_review.py+collect_server.py.
Two surfaces, one result shape
| Review size | Surface | What you see |
|---|---|---|
| 1 item | Native card (same component ask_user_question uses) |
One question at a time, paged |
| 2+ items, panel mounted | One-screen panel in the composer dock | The whole batch at once: every item, verdict buttons, its own note field, one submit |
| 2+ items, no client | Native card fallback | Paged, never waits for a panel that cannot answer |
The panel polls GET /api/batch-review/state only while it is mounted; if it is not (the user is on another page, the client bundle is not loaded, or the host has no web server), batch_review degrades to the native card instead of blocking. The returned surface field says which one answered.
Both surfaces return the same contract — items[].{verdict,note,filled}, summary, report_text — so the skill and the agent do not care which one was used.
Install
# from the repository
dsh plugin --profile <profile> add github:palx666/dsh-batch-review
# or a pinned tag (recommended for reproducibility)
dsh plugin --profile <profile> add https://github.com/palx666/dsh-batch-review/archive/refs/tags/v1.0.0.tar.gz
# or from npm once published
dsh plugin --profile <profile> add dsh-batch-review
Restart the host (dsh web / the desktop app) so the plugin loads. To verify, check that the agent's tool list contains batch_review.
Requirements: DeepSeek Harness >=0.2.1-rc.3 <0.3.0-0, Node.js >=20. No runtime dependencies, no build step — plain ESM.
Usage
{
"title": "Acceptance: file copy / download buttons",
"persist": "review/acceptance.json",
"items": [
{ "id": 1, "type": "confirm", "op": "Restart DSH and reload the page", "expect": "No white screen" },
{ "id": 2, "type": "choice", "label": "Connection mode", "options": [{ "v": "A", "t": "Direct 18080" }, { "v": "B", "t": "Via DSH subagent" }] },
{ "id": 3, "type": "bool", "label": "Keep the gateway task resident", "boolDesc": "Accept" },
{ "id": 4, "type": "scale", "label": "Overall rating", "min": 1, "max": 5 },
{ "id": 5, "type": "text", "label": "Describe anything that looked off" }
]
}
Result (truncated):
{
"cancelled": false,
"items": [
{ "id": "1", "type": "confirm", "verdict": "通过", "note": "", "filled": true },
{ "id": "3", "type": "bool", "verdict": "", "note": "", "filled": false }
],
"summary": { "total": 5, "filled": 4 },
"persisted": "/abs/path/review/acceptance.json",
"report_text": "【批阅】Acceptance: file copy / download buttons\n共 5 条 · 已填 4 条 · 未填 1 条\n…\n未填:3(没填不代表通过,需要单独追问)"
}
Rules the tool enforces for the model:
itemsmust be a non-empty array andids must be unique — answers are folded back by id, so a duplicate is rejected instead of silently overwriting;persist(optional) writes the result as UTF-8 JSON so the verdicts survive the session; the returnedpersistedfield holds the absolute path, and a write failure lands inpersist_errorwithout affecting the review result;- the tool suspends until the user answers; the model must not call it twice for the same batch;
filled: falsemust be followed up, never read as approval;cancelled: truemeans there is no verdict at all — re-ask.
Large batches
- The panel groups consecutive items by
group(falling back toheader), and every group heading carries 全通过 / 全待定 batch buttons. A 100-item batch is workable when the agent sorts it into groups first; a flat 100-item list is not. - From 50 items up the panel shows a hint pointing at CSV/Excel. 1000 items belong in the skill's CSV export (
build_review.py --format csv, opened in Excel with data validation), not in a click-through panel. - Item
groupis optional and free-form; the agent decides the split.
Routing rule ships with the plugin
lib/prompt.js registers a short system-prompt section (~250 chars) that says when to use batch_review versus ask_user_question, and that a blank item is never an approval. It is present on every request, so other users do not have to copy an AGENTS.md rule by hand.
If you would rather own that rule yourself, add your own AGENTS.md entry — the plugin section is additive, not exclusive.
What ships in the package
| Path | Purpose |
|---|---|
index.js |
Host plugin: registers the tool, the skill provider and the panel routes |
lib/tool.js |
Item validation, AskUserQuestionItem mapping, answer folding, report, panel/card routing |
lib/panel.js |
Panel channel: one waiting review, /api/batch-review routes, presence |
lib/client.js |
Browser half: the one-screen review panel in conversation.input.dock |
lib/prompt.js |
System-prompt routing section (batch_review vs ask_user_question) |
lib/skill.js |
Skill provider for the bundled batch-review-table skill |
skills/batch-review-table/SKILL.md |
Decision-channel routing rules and the HTML fallback workflow |
skills/batch-review-table/references/html-fallback.md |
HTML fallback details (protocol, result contract, CSV) — read only when needed |
skills/batch-review-table/assets/review.html |
Self-contained review page (progress, paging, “only unfinished”, copy/download JSON) |
skills/batch-review-table/scripts/build_review.py |
Render a review page (or CSV) from an items JSON |
skills/batch-review-table/scripts/collect_server.py |
Local 127.0.0.1 collector: page → POST /submit → result file + stdout |
How it compares
| Project | Shape | Difference |
|---|---|---|
dsh-plannotator |
Takes over the native plan-review seat | Anchored comments on a plan text; returns Markdown, not per-item verdicts |
dsh-clarify |
Custom interview panel | Self-built UI; collects answers, not verdicts; aimed at requirement interviews |
dsh-question-note |
Add note fields to native options | Single-question notes only; no verdict enum, no multi-item batch |
dsh-hitl |
One card per tool call, approve/modify/reject | Serial queue of single decisions; not one card of many items |
dsh-action-outbox |
Batch-approve staged tool side effects | Approves actions to execute, not questions to answer |
The gap this project fills is the combination: agent-initiated tool → host-native cards → per-item verdicts with an explicit “not answered” state.
Known limitations
- One card, one question at a time — below the threshold. A single item still uses the native card. From 2 items up, the one-screen panel takes over when it is mounted; if it is not, the paged card is still the fallback.
- The panel needs its client half. It lives in
conversation.input.dockand only polls while mounted. A host withoutwebServer, a client build that does not load the bundle, or a user on another page all mean “no panel”, and the tool degrades to the native card rather than waiting forever. - The panel only accepts a loopback Host. Its routes refuse any
Hostthat is notlocalhost/127.0.0.1/::1(or the address the socket itself is bound to), so a GUI reached through a reverse proxy or tunnel gets no panel — the tool falls back to the native card and no verdict is lost. The browser console says so once. - On the native card, the explanation is part of the title.
expect/boolDesc/detailare appended to the title with an em dash (“重启 — 不白屏”) rather than sent as the card's owndetailrow: that row is indented 2px against the title's 24px header padding and keeps the primary colour, so it read as a line sticking out to the left of the title. The panel still shows the explanation on its own indented, secondary-coloured line. - Native cards are root-agent only.
userQuestions.ask()accepts only the live runtime root, so a subagent that lands on the native card fails withDELEGATED_CALLER. The panel path does not go through that host call, so a subagent can still ask through the panel while it is mounted. - Persistence is opt-in. Nothing about a review is written to disk by default; pass
persist: "<path>"and the verdicts are saved as UTF-8 JSON (the returnedpersistedfield is the absolute path actually written).persistis confined to the host process cwd unless the profile setsallowExternalPersist: true. Withoutpersist, a session that ends mid-review returnscancelled: trueand the batch must be asked again. The Settings section keeps its own config file (see the cap entry below). - Item cap (100 by default). A review above it is rejected with a message telling the caller to split the batch; nothing renders. Tunables live in three layers, lowest first: built-in defaults, the
cordis.patch.ymlconfigblock, then~/.dsh/dsh-batch-review.config.jsonwritten by the Settings section (Settings → left-nav 「批阅面板」). A save from that page only writes the keys you changed, so the cordis layer keeps applying to everything else. Hard limits:maxItems1–500,virtualThreshold50–2000,maxBodyBytes1 KB–128 MB; the defaults are 100 / 500 / 64 MB. - Large batches skip off-screen paint. Above
virtualThresholdrows the panel appliescontent-visibility: auto, so the browser skips layout and paint for rows outside the viewport while every row keeps its real height — no fixed row height, nothing clipped. The defaults (maxItems: 100,virtualThreshold: 500) keep this path off; lowervirtualThresholdin Settings → 高级设置 to exercise it. - Chinese option labels by default.
confirmrenders 通过 / 不通过 / 待定 andboolrenders 是 / 否. Edit the constants inlib/tool.jsif you need another language. - Theme tokens only. Native cards are styled entirely by DSH; the panel's own CSS uses
--dsw-*theme tokens exclusively (no literal colors), so it follows the host's light/dark switch.
Development
npm test # node --test — mapping, folding, report and plugin registration
The HTML fallback has its own smoke path:
python skills/batch-review-table/scripts/build_review.py --data examples/items.json --out review/demo.html
python skills/batch-review-table/scripts/collect_server.py # then open review/demo.html
License
MIT — see LICENSE.
Built with
Built with DeepSeek Harness (dsh): the tool, the panel and the skill are written against its host APIs.
No comments yet. Be the first to write one.