dsh-session-requirement
Per-conversation session requirement / constraint box for DeepSeek Harness.
It lives in the conversation title row and lets you write one standing
requirement for this conversation, plus pick skills with @, and have all of
it attached to every message you send.
The box is off until you switch it on. The switch lives in Settings → General → 本对话的整体要求; flip it and the box appears in the title row immediately — no reload, no restart. Flip it back and the box is gone, and nothing is attached to your messages either.
Collapsed, it is one line at the right end of the title row:
… 标准模式 ┌────────────────────────────────────────┐
│ 输入本对话的限制、约束或整体要求(可选)(●)│
└────────────────────────────────────────┘
Focused, it lifts out of the row as a floating card — the header never grows:
┌──────────────────────────────────────┐
│ @office-docx × @office-xlsx × │
│ 只输出中文,表格必须带单位 │
│ … │
│ ┌────────────────────────────────┐ │
│ │ @office-docx Word documents │ │
│ │ @office-xlsx Excel workbooks │ │
│ └────────────────────────────────┘ │
└──────────────────────────────────────┘
Behaviour
| Rule | Detail |
| --- | --- || GUI switch | one toggle in Settings → General, stored globally in the state file; off on a fresh install |
| Show/hide | off renders nothing at all — no empty gap and no slot in the title row; on mounts the field in every conversation at once |
| Placement | conversation.session.header.utilities, order: 0 — the right-aligned utility area of the Session title row (the 创建/标准模式 line), left of the corner controls |
| Size | a fixed 380px × 28px slot, so the title row never reflows. On focus the field lifts out of flow as a 470px floating card (position: absolute) over the tab row, because the header must not grow |
| Blank conversations | the shipped header hides its whole title-row chrome while a conversation is still blank, so the same field also registers in conversation.input.dock (order: -100) and shows there until the title row appears. A module-level seat registry keeps exactly one copy on screen; the dock copy is the full-width, in-flow variant |
| Collapsed | one line; expands to four lines on focus; collapses on blur without losing text |
| Scope | text, chips, and the per-conversation switch are stored per conversation; a new conversation starts empty and off |
| Effective | requirementText.trim().length > 0 \|\| skillIds.length > 0 |
| Switch | grey while there is no content and clicking it focuses the input; turns on by itself on the first effective content; turning it off keeps text and skills but stops attaching them; clearing everything greys it again |
| Attach | only when the GUI switch is on, the per-conversation switch is on, and the content is effective |
| @ picker | opens on @ at line start or after whitespace; filters by name, description and whenToUse; ↑/↓ move, Enter picks, Esc closes, click picks; empty result and load failure show an inline message instead of breaking the input |
| Chips | a picked skill becomes a non-editable @name × chip; × removes it; Backspace in an empty input removes the last one; duplicates are ignored; the cap is maxSkills (default 10) |
| Skills source | the live harness skill registry through ctx.remote.skills.list — nothing is hardcoded. Subagent sessions have no catalog and say so instead of showing a fake list |
| Skills execution | none. This plugin only selects, stores and forwards skills; the model receives the skill body as context and decides what to do |
How attachment works
conversation.send() on the client is a pure pass-through — there is no
outgoing-prompt transform to hook. The durable, model-visible seam is the host's
agent/pre-step; when the claimed message batch contains a real user message
(a fresh send, not a tool-loop follow-up) this row appends one extra
user/message:
<session_requirement>
The user set a standing requirement for this whole conversation. Apply it to every reply until the user changes it.
<requirement>…</requirement>
<selected_skills>office-docx, office-xlsx</selected_skills>
</session_requirement>
<skill_content name="office-docx">…</skill_content>
It is committed through the harness's own surfaceOp: "append" path with
source.kind = "session-requirement", so it lands in the transcript like the
built-in /skill invocation injection — it never impersonates a user gesture.
HTTP surface
| Route | Purpose |
|---|---|
GET /api/session-requirement?sessionId=… |
read one conversation's state (also reports visible) |
GET /api/session-requirement?mode=settings |
read the GUI switch alone, with no conversation |
POST /api/session-requirement |
write one conversation's state: { sessionId, requirementText, requirementEnabled, skillIds } |
POST /api/session-requirement { visible } |
flip the GUI switch alone |
State lives in <DSH_HOME>/session-requirement.json, written atomically
(.tmp + rename):
{
"version": 1,
"settings": { "visible": false },
"sessions": {
"session-…": { "conversationId": "session-…", "requirementText": "", "requirementEnabled": false, "skillIds": [], "updatedAt": 0 }
}
}
Requests are rejected with 400 rather than silently corrected when sessionId
is malformed, the text exceeds maxTextLength, or skillIds is not a
deduplicated list of at most maxSkills skill names.
Command
/requirement prints the current conversation's state; /requirement clear
clears and disables it.
Requirements
- DeepSeek Harness 0.2.x (desktop or web), Cordis v4.
- No npm dependencies. The host half imports only Node built-ins; the browser
half is a hand-written CJS bundle that takes
reactfrom the harness loader.
Install
git clone https://github.com/sunlei1214/dsh-session-requirement.git
Point your profile at the checkout:
// <DSH_HOME>/profiles/<profile>/package.json
"dependencies": { "dsh-session-requirement": "link:C:/path/to/dsh-session-requirement" },
"dsh": { "profile": { "bundles": [ …, "dsh-session-requirement" ] } }
Then reinstall the profile and restart DeepSeek Harness — the browser half ships as a static client bundle, so a page refresh alone is not enough.
Use
link:, notfile:. pnpm copies afile:directory dependency and caches it, so later edits never reach the profile.link:creates a junction (Windows) or symlink, so the profile always reads the current source.
Uninstall
Remove the dependency and the bundle entry, reinstall, restart. The host row
drops its route, its agent/pre-step listener, and flushes pending state on
dispose; the browser half removes its <style data-plugin="dsh-session-requirement">
node, its document listeners, its debounce timers, its caches, and aborts
in-flight requests. Deleting <DSH_HOME>/session-requirement.json is optional.
Configuration
| Option | Default | Meaning |
|---|---|---|
maxSkills |
10 |
maximum chips per conversation |
maxTextLength |
8000 |
maximum requirement length |
Tests
node test/host-check.mjs
node test/client-check.mjs
(npm test runs both; if the PowerShell execution policy blocks npm.ps1, run
the two files directly.)
Two offline harnesses run against the real source files, with no harness install required:
test/host-check.mjs— a fake context, web server and skill registry. Covers both routes, the GUI switch (default off, flip, persist, and that a hidden row never attaches), every validation refusal, the atomic JSON sidecar, the pre-step injection (fresh send vs. tool-loop follow-up, disabled, empty, text-only, unknown and non-user-invocable skills, aborted signal), the/requirementcommand, and route removal on dispose.test/client-check.mjs— a minimal React hook runtime plus a fake DOM, driving the actual components: the GUI switch gating the row (absent while off, appearing live when flipped), collapsed/expanded rows, the grey toggle focusing instead of enabling, auto-enable on first content, manual off/on, chips, the@picker (filtering, arrows,Enter,Esc, click, empty state, load failure), dedupe, the cap,Backspace, per-conversation isolation, a late read not clobbering typed text, and a teardown that must leave no style node, listener, or pending write.
test/verify-live.mjs checks a running app after installing and restarting:
node test/verify-live.mjs # read-only
node test/verify-live.mjs --write # round-trip a write, then restore
It reports the state route, the GUI switch route, the boot manifest entry, and
the validation refusals. A 401 means the host half did not load — restart the
app.
License
MIT © 2026 sunlei1214
Not affiliated with DeepSeek. "DeepSeek Harness" and "DSH" refer to the host application this plugin extends.
No comments yet. Be the first to write one.