dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
暂缓发送一条消息,直到主智能体空闲、没有子智能体在运行、也没有后台任务,然后再发出。每条暂缓可以设置发送时间,并选择排队、等待空闲或立即插话,消息与附件都保存在宿主端,重启也不会丢失。
Hold a drafted message until the session is actually finished, then send it.
The harness already queues a message you send while the agent is working, and it
releases that queue as soon as the main agent stops talking. That is not the same
as being done: a subagent and a background job are invisible to the composer, so
the queue hands your message over while real work is still in flight. dsh-hold
adds the missing option, wait until nothing is running at all, and it holds
the message on the host, so closing the tab does not lose it.
This is a standalone plugin. It reads and writes only its own file and depends on no other plugin.
dsh plugin --profile web add github:TetraSsky/dsh-hold
Then add dsh-hold to dsh.profile.bundles in $DSH_HOME/profiles/web/package.json
and restart. The package's cordis.patch.yml inserts its own row.
A Hold button sits in the composer tool row, next to the send action. It uses
the harness' own Button and queue glyph, so it looks like the controls around it.
Type a message, click Hold, and a window opens over the composer with two independent choices:
| Axis | Control |
|---|---|
| Send at | A switch. Off, which is the default, means no earliest time, so the message goes as soon as it can. On reveals a calendar and two time dropdowns. |
| If the AI is still working | A dropdown: Queue (Default) · Wait idle · Send now (Steer) |
The date is chosen from a calendar rather than typed, and the hour and minute are picked from two dropdowns, so a typo cannot produce a nonsense instant. Days before today are disabled, which is the guard-rail. There is a second one at confirmation: a chosen time that has already passed is refused in the window and never sent.
The two time lists are capped rather than left to the shipped Menu's own bound,
which is the viewport: 24 hours or 60 minutes at that height is a full-page column, not
a picker. The cap is one small stylesheet the bundle installs, keyed to a marker the
window carries only while it is open, so nothing else on the page is affected. Rows are
the compact variant, which is what fits the most values in the capped list.
The axes combine. Next month, the 15th at 09:30 with Wait idle means: at that
time, if the session is still going, keep waiting until it is complete, then send. An
unset time with Wait idle is the headline case, the one the harness' own queue
cannot express.
The window's controls are harness primitives (Button, Switch, Menu) and the
calendar is built from those same buttons. The message field is a plain multi-line
textarea, because the shipped Input is single-line. No native <select> or date
picker is used anywhere, because those draw their popup with the operating system's
theme instead of the shell's.
The held list and the window take the composer's own width, the same recipe the shipped goal bar and queue strip use, so they are as wide as the chat content and follow the conversation's drag handle when it is moved. That width sits inside the resize handles rather than across them, so no handle has to be hidden while the window is open.
Confirming takes the message out of the composer and into the held list. The three behaviours are the harness' own vocabulary:
Three things must be true, all read from in-memory registries at the moment of evaluation:
| Check | Source |
|---|---|
| No main agent running | agent.status. running spans the whole turn, every tool call included, so idle really does mean the agent stopped talking |
| No live subagent | live agents owned by this session whose status is still running |
| No background job | ctx.jobs.list(agent), excluding completed / killed / failed |
A continuable subagent is deliberately judged by its agent's status, not by
whether it is resident: a settled child stays resident after its turn ends, so
residency would hold a message forever.
Nothing waiting on you is already covered. A pending approval, question, or plan
review keeps the turn open. dsh-user-approval documents that "the request requires
an open turn", and ask_user_question and exit_plan_mode are ordinary tool calls
whose result the driver is still awaiting. So the main agent reads running for the
whole time it is blocked on you, and no separate check is needed.
A session with no live agent is never finished: the message is kept, not dropped, and delivered once that session is running again.
Held messages for the current session appear above the composer. There is no limit on
how many you hold, so the list uses the shipped queue strip's own guard rail: one
hold renders as itself, and two or more fold into a {n} held messages header with
the queue glyph and a chevron. Opening it lists every hold, and that open list is
capped at 180px and scrolls rather than growing, so twenty holds are one header and a
scroll box, never twenty stacked rows.
Every row offers:
Each row also says what it is waiting for ("Waiting on the main agent, a background job") or that it is ready, and counts down when it has a time.
Order. Several holds that come due together go out one at a time, oldest first. A Wait idle hold that has just been delivered puts the session back to work, so the next one waits for the next completion instead of landing on top of it. A Queue (Default) or Send now (Steer) hold is not gated on anything, so those all go out in the same pass, still oldest first. Two holds set for the same instant behave the same way: the time axis decides when a hold becomes ready, not how many go at once.
The window follows the interface language, English or Chinese, and so does every refusal it shows. The host answers in English, so a code the browser has a sentence for is replaced by that sentence, and only a code it does not recognise is shown as the host worded it.
A hold carries what the composer has staged, images and files alike. The window lists them above the time axis, and any of them can be dropped before holding.
admitPromptContent call its own prompt
endpoint uses.Once a hold owns an attachment it is removed from the composer's rail, so it cannot ride along with the next message sent from there. Editing a hold keeps its attachments and lets you drop one. Adding new ones means staging them in the composer and holding again.
Holding is refused, with the reason, while a file is still uploading or after an upload has failed, the same states in which the composer itself will not send.
@ references need no special handling and never did: @file is literal text the
system prompt explains, @session is resolved by a host pre-step hook over any direct
user message, and the reference codec is identity, so the draft a hold captures is
the exact text a send would have used.
The queue is one JSON file at $DSH_HOME/hold-queue.json, written temp-and-rename.
It stores what to send, where, when, and how, never delivery state. On restart the
queue is restored. Unloading the plugin clears the in-memory view and leaves the
durable records alone: unloading is not cancellation.
There is nothing to configure, and the plugin registers no settings namespace. It always waits for all three checks, and every choice is made per message in the window.
dsh
process sends nothing and the queue is delivered after the next start.agent.followup, or agent.steer while running), so it
renders as your own bubble and carries no marker saying it was held.@deepseek-ai/dsh-schedule's job.npm test # 203 tests
npm run build # regenerate client.js after editing src/client.js
The suite includes tests/integration.test.mjs, which drives the real generated
browser bundle against the real host gate through a JSON-serialized RPC, the way
Connection carries it: compose a hold, watch it wait for a running agent and a
background job, then release it and see it arrive as an ordinary user message.
tests/harness.mjs holds the shared test scaffolding: a React stand-in, stand-ins
for the shipped primitives, and the tree walkers.
The browser bundle is generated: scripts/build-client.mjs inlines the shared
modules into client.js, which is what lets the browser load one file with no import
graph. tests/bundle.test.mjs fails if the committed bundle has drifted from src/.
Layout:
| Path | Role |
|---|---|
index.js | Host entry: queue, gate, and the RPC route |
src/hold.js | Pure model: validation, the busy reasons, the release decision |
src/world.js | One synchronous snapshot of a session's live work |
src/gate.js | The hold engine: tick, deliver, edit, cancel, restore |
src/queue.js | Durable queue, temp-and-rename |
src/deliver.js | Message construction and the queue/steer verb |
src/rpc.js | /dsh-hold route behind Connection's request fence |
src/client.js | Browser half: the button, the window, the held list |
src/i18n.js | English and Chinese strings |
MIT