dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
侧边栏记事本:带标题与正文的记事,可插入图片(拒收视频);点标题把正文复制到剪贴板;输入框里可用 @ 引用某条记事;可选在新会话时自动打开记事本。
A notebook in DSH's official right sidebar.
+ a note → title + body → paste images → Done files it under its title.
Click a title to copy the body · type @ to reference a note · Edit reuses the same container.
dsh-plugin · deepseek-harness · notebook · notes · sidebar
English · 中文
What's new
dsh-better-sidebar carrier and the plugin's own fallback shell are gone, openOnStart is honoured by the official sidebar, and the peer ranges had to move to 0.2.0-rc.2 before the bundle would load at all.Jump to · Features · Install · Usage · Compatibility · Settings · Under the hood · Limitations · FAQ · Development
Suggested GitHub topics:
dsh-plugindeepseek-harnessnotebooknotessidebar
dsh-notebook is a Web plugin for DSH (DeepSeek Harness) that puts a small notebook in DSH's own right sidebar — one tab registered through the official tab registry, no companion plugin involved.
It does one thing: jot down a note with a title, a body and a few images, then use it from the composer — click a title to copy the body, type @ to reference a note.
No rich-text editor, no cloud sync, no version history. Notes are global (not per conversation) and images are stored as real files on the host disk.
| Feature | Detail |
|---|---|
+ creates an entry | Top-right of the panel; the editor opens inside the panel — no new window, no second tab |
| Title + body + images | Single-line title, content-sized body, image thumbnail strip in one scrollable container |
| Images yes, videos no | Paste, drag & drop and “Insert image” share one validation path: video/* and any known video extension (mp4/mov/webm/mkv/avi/m4v/ogv/mpg/mpeg/3gp/flv/wmv) are refused inline |
| “Done” files it under its title | The list shows one row per note, newest first, with “time · N images” as secondary metadata |
| Click the title to copy | Copies the body only — never the title — with images as [image: name] lines and a “body copied (N chars)” toast. It never writes into the composer |
@-reference a note | Typing @ lists notes under the localized “Notebook” heading (title + snippet, newest first, max 8). Picking one inserts an atomic chip; only the body reaches the model |
| “Reference” row button | Same as an @ pick, from the row itself; with no reachable composer it says so instead of pretending |
| Select text → “To notebook” (v0.2.0, on) | A floating action beside the selection files it verbatim as a note titled 未命名1, 未命名2, … Selections inside the composer or in the panel itself are not offered |
| Every answer → “save to notebook” (v0.2.0, on) | One icon at the end of the answer's action row saves the whole reply, titled with the session's own title |
| Both captures are switchable | selectionToNotebook / messageToNotebook in settings; a change applies on the next render, no reload |
| Auto-open for new sessions (off by default) | Opens the Notebook whenever a session becomes current — including the one a page load restores |
| “Edit” reuses the same container | The DOM never holds more than one editor |
| Images land on disk | Uploaded as dataURL, decoded by the host into $DSH_HOME/storages/notebook-attachments/<noteId>/; deleting a note deletes them too |
| Atomic writes, no silent loss | Temp file → fsync → .bak → rename, serialized by a mutex. If $DSH_HOME is not writable the host degrades to memory and every response carries degraded: true, which the UI shows as a banner |
| Keyboard | Cmd/Ctrl+Enter = Done, Esc = Cancel (a dirty draft asks first) |
Longer, deeper notes on the design live in Under the hood.
No screenshots on this page right now. The images that used to sit here were captured on the pre-0.3 shells, which no longer exist, and the official right sidebar has not been re-photographed since the migration. Until it is, the Features table is the ground truth: the panel is the note list with
+in its header, plus one reusable editor container holding title, body, image thumbnails and the inline video refusal.
| DSH | >=0.2.0-rc.2 |
| Node | >=20 |
| Package manager | pnpm — this repo does not support npm |
| Companion plugins | none — the page renders in the sidebar DSH ships |
From the repository:
dsh plugin --profile web add github:wyzh0117/dsh-notebook
From source (development):
git clone https://github.com/wyzh0117/dsh-notebook.git
cd dsh-notebook
pnpm install
pnpm build # emits lib/index.js, lib/client.js, lib/types/**
dsh plugin --profile web add "link:$PWD"
The manual equivalent — in ~/.dsh/profiles/web/package.json:
{
"dependencies": { "dsh-notebook": "link:/abs/path/to/dsh-notebook" },
"dsh": { "profile": { "bundles": [ /* … */, "dsh-notebook" ] } }
}
Then pnpm install inside the profile directory. dsh.profile.bundles must contain dsh-notebook, otherwise the plugin is never loaded. If it is listed and the row still shows as disabled, the version gate skipped the bundle — see If the plugin does not load at all.
While developing, never restart the DSH instance you are working in (e.g. the Web GUI on port 3080 — restarting it kills the current session). For a real end-to-end check, start an isolated environment:
DSH_HOME=/tmp/dshnb-home npx -y --package @deepseek-ai/dsh dsh web --port 3099
Expand the right sidebar and add Notebook from its + list — the row carries the plugin's title, description and icon. The sidebar keeps one surface per session, so a freshly created session starts without it; “Open the notebook for new sessions” does the picking for you.
Click + in the top-right — the editor opens inside the panel.
Write a title and a body. The body box follows its content (up to 60 % of the window height, then it scrolls inside). Add images by pasting, dropping, or “Insert image” — videos are refused; one image is capped at 10 MB and a note at 20 images (both adjustable).
Click Done (or Cmd/Ctrl+Enter) — the note is filed under its title.
Click the title text to copy the body to the clipboard.
To let the model read a note, type @ in the composer and pick it, or click Reference at the end of its row. Only the body is sent; re-insert a reference after a page reload.
Edit reloads a note into the same container; Delete asks in the panel's own dialog first.
Let the conversation write notes for you (v0.2.0):
Both confirm with the same toast and can be switched off in settings.
| DSH | >=0.2.0-rc.2 — the version line whose official right sidebar (ctx.sidebarRightTabs) this plugin registers into |
| Node | >=20 |
| Companion plugins | None. The host half only needs webServer, cordis and schemastery; nothing else has to be installed for the tab to appear |
| Optional DSH seams | Without conversation, inputTriggers or sessions, the matching integration simply turns off and the plain notebook remains |
| Older shells | Every feature registers through optional seams and degrades instead of breaking: a missing slot means the capture entry never appears, an unreadable locale falls back to the plugin's own zh dictionaries, and the panel keeps rendering without window.confirm or any dialog API |
There is one carrier: the sidebar DSH ships. The tab is registered in two phases, body first:
sidebar.right.pane.tab, under key: 'dsh-notebook';ctx.sidebarRightTabs.register({ id: 'dsh-notebook', kind: 'dsh-notebook', priority: 'extension', title, guide: [{ id, order: 60, title, description, icon }] }).Three constraints the registry enforces and does not negotiate:
key must equal the stage-1 id — the seat dispatches a tab to the entry keyed by the type in force for its kind. A list-shaped { id, order } descriptor on a keyed slot is not a near miss, it is a failed registration, and the sidebar's answer is a Notebook tab that can never render;guide entry needs its own stable id, or the registry throws duplicate guide entry id;The tab body reads its visibility through the slot's injected useTabInfo() hook, because the seat renders a tab with an empty owner share and tab.visible never arrives as a plain prop. That is what lets a hidden tab skip loading and polling.
src/client/hosts/controller.ts probes sidebarRightTabs and, when the service has not arrived yet (cordis serves plugins in dependency order, not sidebar-first), waits for it once through ctx.inject(['sidebarRightTabs'], …). There is no fallback shell: if the service never appears nothing registers, and the reason is logged. Every registration sits inside ctx.effect(() => { …; return dispose }), so HMR and disable are clean.
The open IS the reveal. ctx.sidebarRight.openTab(kind) expands the column as part of opening the tab, and throws while no Session is on screen. isExpanded() answers from the surface the seat bound at its last committed render — so it still says false inside the same synchronous block as a successful open — while toggleExpanded() flips the live value. Reading one and “correcting” with the other collapses the column the open just revealed; that was v0.2.2's bug, and nothing may follow the call.
dsh 0.2.0-rc.2's dsh-app-boot reads every package.json peerDependency named @deepseek-ai/dsh or starting with @deepseek-ai/dsh- and runs semver.satisfies(runtimeVersion, range, { includePrerelease: true }). On any mismatch it skips the whole bundle: the module is never imported, the plugin row shows as disabled, and the log says
dsh: skipping profile bundle "dsh-notebook": … is incompatible with dsh 0.2.0-rc.2: peerDependencies { … }
Two things that follow from this and surprise people:
dsh.plugin.json's engines.dsh is not enforced in rc.2 — only the peer ranges decide, so a correct engines field does not save a stale peer;webServer, cordis and schemastery. What stopped it loading on 0.2 was the version gate in package.json, not the code.The escape hatch is dsh plugin allow-version, which writes an exact-version exemption into <profile>/compatibility.json; the matching disallow command reverses it. Use it to look at a mismatch, not to live with one — an unmet peer range normally means a real API difference.
One definition, one seat. The source of truth is the host's NotebookDoc.prefs — reads and writes always go through PATCH /notebook/api/prefs — and all eight switches are offered in DSH's global settings page, because the official sidebar has no per-tab settings page.
| Key | Type | Default | Meaning |
|---|---|---|---|
sortOrder | 'updated' | 'created' | 'title' | 'updated' | List ordering |
copyImagesAsName | boolean | true | Render images as a [image: <name>] line when copying |
maxImagesPerNote | number | 20 | Image cap per note (1–100) |
confirmDelete | boolean | true | Ask before deleting, in the panel's own dialog; off deletes on the click |
openOnStart | boolean | false | Expand the sidebar and open Notebook when DSH starts |
autoOpenOnNewSession | boolean | false | Open the Notebook whenever a session becomes current |
selectionToNotebook | boolean | true | Show the floating “to notebook” action over a text selection |
messageToNotebook | boolean | true | Show the “save to notebook” icon at the end of every answer |
The section is registered by the sidebar host itself (hosts/settingsSeat.ts → settings.section, id dsh-notebook, order 100), and registering it is a no-op wherever the slot is not declared — a composition without a settings dialog is never an error. Both startup switches drive the same openTab gesture a click performs: openOnStart fires once the host registers and again when the preferences come back from the host, and is a harmlessly swallowed no-op while no session is on screen.
Everything below is for people reading or changing the code. Skip to Known limitations if you just want to use the plugin.
┌─────────────────────────── browser ───────────────────────────┐
│ lib/client.js (CJS module-table factory, id = "dsh-notebook")│
│ controller: probe sidebarRightTabs → native host │
│ NotebookView ─ NotebookEditor(one instance) ─ clipboard │
│ capture (v0.2.0): selection pill + answer icon → capture.ts │
│ body sizing (v0.2.1): textarea → autoGrow.ts │
│ dialogs (v0.2.2): in-panel prompts → locales.ts │
└──────────────────────────────┬────────────────────────────────┘
fetch JSON │
┌──────────────────────────────┴────────────────────────────────┐
│ lib/index.js (cordis plugin, ctx.webServer prefix) │
│ routes.ts ─ store.ts (atomic write + mutex) ─ attachments.ts │
└──────────────────────────────┬────────────────────────────────┘
│
$DSH_HOME/storages/notebook.json
$DSH_HOME/storages/notebook.json.bak
$DSH_HOME/storages/notebook-attachments/<noteId>/<attachmentId>.<ext>
src/client/autoGrow.ts is the only place the body box's geometry is decided: it measures the <textarea> against its content and writes height / maxHeight / overflowY, while NotebookEditor decides when (mount, text change, width change, viewport-height change).useTabInfo() hook (the seat hands it an empty owner share), so a hidden tab really does skip loading and polling.FileReader) and decoded by the host — no multipart parser dependency.Registered on ctx.webServer (kind: 'prefix', path: '/notebook/api'), JSON over HTTP, with Cache-Control: no-store on every response.
| Method | Path | Request | Response |
|---|---|---|---|
GET | /notebook/api/state | — | { doc, degraded, degradedReason? } |
POST | /notebook/api/notes | { title, body, attachments: [{ name, mime, size, dataUrl }] } | { note } |
PATCH | /notebook/api/notes/:id | { title?, body?, attachments? } | { note } |
DELETE | /notebook/api/notes/:id | — | { ok: true } |
PATCH | /notebook/api/prefs | Partial<NotebookPrefs> | { prefs } |
GET | /notebook/api/attachments/:noteId/:file | — | image bytes + Content-Type |
GET | /notebook/api/health | — | { ok, version, degraded } |
req.socket.remoteAddress ∈ 127.0.0.1/::1) and Origin/Host must be an allowed local origin, otherwise 403; path parameters are traversal-checked; request bodies are capped at 32 MB (413).400/403/404/413/415/500 all return a structured { error: { code, message } }.415.| Path | Contents |
|---|---|
$DSH_HOME/storages/notebook.json | NotebookDoc (version: 1, notes[], prefs), written atomically |
$DSH_HOME/storages/notebook.json.bak | The last known-good version; tried first when the JSON is corrupt |
$DSH_HOME/storages/notebook-attachments/<noteId>/<attachmentId>.<ext> | Image bytes |
$DSH_HOME resolution order: explicit injection > process.env.DSH_HOME (whitespace-only counts as unset) > ~/.dsh. If .bak is corrupt too, the store starts from an empty document and renames the broken file to notebook.json.corrupt-<ts>.
Every seam here is public and entirely optional: the client bundle may only require() the platform seed modules — today that is exactly react and react/jsx-runtime — and never imports a DSH UI package, so every member is duck-typed and every call is guarded.
@ references. The plugin registers a @ source named dsh-notebook through ctx.inputTriggers.registerSource (order: 20), so typing @ lists notes next to files and sessions. Candidates are filtered case-insensitively on title and body, newest first, at most 8 rows; an aborted query or a failed read yields no rows rather than breaking the menu. Picking a row inserts an atomic chip whose clipboard form is @[title](dsh-notebook:<noteId>). At send time the note's body only is serialized (the title is deliberately never sent), each image marker becoming one [image: <name>] line: a deleted note contributes nothing, while a real read failure blocks the send with a visible error instead of silently downgrading the mention.
Auto-open. The watcher fires when the current session becomes a different one; a re-published snapshot of the same session does not count, and neither does the session already current at activation. openTab can throw while the seat holds no binding, so the open is retried at 0 / 200 / 500 / 1200 / 2500 ms and abandoned the moment the session changes again. Since the plugin activates before the session list arrives, the selection a page load restores also counts as “becoming current”.
The composer attachment bridge is implemented, unit-tested and deliberately unwired. Clicking a title must only copy, never write into the composer, so its only entry point was removed (product decision, 2026-09-15). The seams are the expensive part and are already covered, so composer.ts and its tests are kept on purpose, waiting for an explicit action of their own. No UI can trigger it today.
Why the open is the reveal (the v0.2.2 bug, the v0.2.3 fix). sidebarRight.openTab(kind) expands the column as part of the open (its first planned store op is planSetExpanded(state, true)), while isExpanded() answers from the surface bound at the seat's last committed render — so a read in the same synchronous block as openTab() is one React commit behind. v0.2.2 read that stale false and then “corrected” it with toggleExpanded(), which flips the live value and collapsed the column the open had just revealed. DSH publishes no idempotent setExpanded(), so there is no safe correction from outside: the plugin now calls openTab alone, and test/auto-open.test.ts models the host's real semantics and asserts the panel ends expanded with zero isExpanded / toggleExpanded calls.
Two ways for the conversation to fill the notebook, both living in the shell rather than in the Notebook tab (so they are offered whether or not the tab is open) and both on by default. They share one write path — client/capture.ts — so title numbering, save ordering and the toast cannot drift apart.
selection ──► selectionAction.ts ─┐
├──► capture.ts ──► POST /notebook/api/notes ──► NotebookDoc.notes
assistant answer ──► answerAction.ts ─┘ (title minting · serialized writes · subscribers · toasts)
| Select text → notebook | Answer → notebook | |
|---|---|---|
| Entry point | A floating pill beside the selection (shell.overlay) | One extra icon at the end of a finalized message's action row |
| Preference | selectionToNotebook | messageToNotebook |
| Title | The numbered default 未命名n — the smallest n no existing title uses, so deleting 未命名2 hands the next capture that slot | The session's own title; with none yet, the numbered default instead of a placeholder the user never chose |
| Body | The selection verbatim (no trimming) | Every text block in order, joined by a blank line; reasoning, tool-call and image blocks are left out |
| Refuses | Empty/whitespace-only selections; selections inside the composer, any editable control, or this plugin's own panel; selections outside the conversation | Nothing to offer without a readable snapshot, or on a row with no usable message id (an interrupted answer carries none) |
Where the action lands in the row. The DSH slot renders inside the message row's extension band, between the hardcoded Copy and Branch buttons. order: 20 puts this plugin after the shipped good-response pair, so the icon is the last entry the slot can express — it cannot be placed after Branch.
Numbering is owned by the capture service. A title is chosen from the notes the host holds plus the titles this activation already minted, and is held until its request settles: consecutive captures never collide even when the note list is unreadable, while a failed save releases its number so a retry mints the same one. Saves are serialized, so that read-modify-write cannot interleave.
Selection scope, honestly. DSH exposes no selection service, so the feature watches the document itself and decides what counts as “in the session” from the shell's semantic DOM hooks ([data-chat-flow], [data-conversation-scroll], then [data-slot=…]). A future shell that renames them degrades to “anywhere outside the composer and outside our own panel” instead of silently never firing.
Deliberately out of scope for v1 / v1.1 / v0.2.0–v0.3.0.