A document preview / edit / management plugin for DeepSeek Harness. It provides a VSCode-style "Document Center" workbench inside the DSH Web GUI: a file tree on the left, click-to-preview, editable code & Markdown with Ctrl/Cmd+S save; and exposes three document tools (doc_read / doc_edit / doc_create) to agents, letting models read and write workspace documents through natural language.
Install
# from GitHub (first run asks for allowBuilds approval — follow the hint, retry)
dsh plugin --profile web add github:Che-Year/dsh-unidoc
Any plugin you install runs third-party code with your own permissions — it can read your files, use your credentials, and reach the network, and tool approvals don’t sandbox it. GitHub-sourced plugins also run build scripts at install time — pnpm blocks those until you allow them, so an install can stop with ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED or ERR_PNPM_IGNORED_BUILDS; dsh prints the exact key to add under allowBuilds in your profile’s pnpm-workspace.yaml, and the install works on the next run. Allowing a build is a trust decision: only install sources you trust, and pin a commit (github:owner/repo#sha).
README
🌐 中文 | English
A document preview / edit / management plugin for DeepSeek Harness. It provides a VSCode-style "Document Center" workbench inside the DSH Web GUI: a file tree on the left, click-to-preview, editable code & Markdown with
Ctrl/Cmd+Ssave; and exposes three document tools (doc_read/doc_edit/doc_create) to agents, letting models read and write workspace documents through natural language.
Feature Overview
1. File Preview & Editing (acceptance criteria mapping)
| Category | Formats | Implementation |
|---|---|---|
| Office documents (read-only) | .docx .xlsx .pptx |
Metadata + friendly "preview not supported yet" notice card (Office preview kernel not loaded) |
| Code & config | .py .java .go .rs .cpp .c .js .ts .jsx .tsx .json .yaml .yml .toml .xml .ini .conf etc. |
Lightweight syntax highlighting (keywords / strings / comments / numbers) + editing + Ctrl/Cmd+S save + Tab indentation + bracket auto-pairing |
| Markup & rich text | .md .html |
Markdown: edit/preview dual mode — preview renders headings, lists, code blocks, tables and images, with support for relative images and relative links; HTML: sandboxed preview (CSP disables scripts + iframe sandbox attribute, double isolation) + source view + open in new tab (unidoc.openExternal) |
| Static assets & layout | .png .jpg .jpeg .gif .svg .webp .pdf |
Images scale to fit; PDF embedded browser viewer (paging / zoom provided natively by the browser) |
| Data science (exploratory) | .ipynb |
Read-only notebook preview: Markdown cell rendering + code cell highlighting + text output |
| Plain text fallback | .log .csv .txt and any unclassified text |
CSV rendered as a table; everything else opens as read-only plain text — unknown extensions never crash |
| Explicitly unsupported | Audio/video (.mp4 .mp3 etc.), iWork (.pages .numbers .key), CAD (.dwg), OpenPencil (.op) |
Friendly "preview not supported" notice with file info |
2. UI Entry Points
- Sidebar bottom icon-only button (
sidebar.footer.action, Font Awesomefa-file-pen) — toggles the workbench; - Fullscreen workbench (
shell.overlay):- Top toolbar shows the title and the current workspace root path (auto-detected from the DSH session; switching agents/sessions is sensed — on open the root is refreshed first and the tree reloads, then a 5s poll keeps them in sync; every call reports the currently selected session's workspace as the authoritative
hintCwd, so any workspace — including an old historical session — is hit precisely with no stale root; after a switch the tree is fully reset: cache cleared, expanded state, selected path and scroll position reset to the root, preview closed, so the top path and the tree always match); - Left file area: the file tree shows the workspace root at the top; lazy loading, click-to-expand directories, file sizes, Font Awesome file icons by extension; "Expand All" recursively opens every directory including hidden ones (
.git,.github,.vscode,node_modules…), loading asynchronously in batches without freezing the page; "Collapse All" collapses everything and frees the cache; refresh / expand-all / collapse-all / options / close buttons sit below the tree (bottom-left); - Right preview/edit panel: every toolbar has an "Open Externally" button (click → editor picker menu → choose → open, remembering your last choice); HTML preview also has an "Open in New Tab" button (
unidoc.openExternal);
- Top toolbar shows the title and the current workspace root path (auto-detected from the DSH session; switching agents/sessions is sensed — on open the root is refreshed first and the tree reloads, then a 5s poll keeps them in sync; every call reports the currently selected session's workspace as the authoritative
- Runtime card (
tool.view.cordis): shows the plugin's activation state with a one-click open button; - Toast feedback: loading state, save success/failure notices;
- Options panel (session-level in-memory config): code editing toggle, Markdown dual-mode toggle, "not supported" notice card toggle, and an editable external editor list (add / remove / rename; defaults: VS Code, Sublime Text, Atom, Notepad++, Vim, Neovim, Typora).
3. Agent Tools
| Tool | Description |
|---|---|
doc_read |
Read a document/code file by path (supports offset/limit for line-based reads of large files; binary files return file info) |
doc_edit |
Replace the unique occurrence of old_string with new_string in a file and save atomically (0 or multiple matches both fail with a clear message) |
doc_create |
Create a new file in the workspace (no overwrite by default; overwrite=true allows overwriting) |
All paths are relative to the Document Center root (the current session workspace) and
are validated with fs.contains to prevent directory traversal.
Technical Architecture
Dual-end structure (DSH dynamic Cordis plugin)
- Host side (
src/host.js, runs in the DSH Node process)- Dependency declaration:
inject: ['fs', 'webServer', 'sandboxPolicy'] - Root directory resolution (priority, highest first):
- Client
hintCwd(authoritative) — the client reads the currently selected session's workspacecwdfrom the DSH client runtimesessionsservice (sessions.manager.selected→sessions.list.getSnapshot().byId[id].cwd) and sends it with everyunidoc.root(hintCwd)call; the host uses it directly (after verifying it is a directory) — no matter whether you switched to a brand-new session or an old historical one, the current workspace is hit precisely, so the tree can never keep showing a stale workspace; - The initiating agent's session
cwd(agents.currentInitiator()→session.header.cwd) — only valid in agent tool-call contexts; browser RPCs run outside the initiator boundary and yieldundefined; - Session
cwdfrom the online agent list (agents.list(), registration order — old first, new last) — iterated newest-registered first; the just-activated session is most likely the current workspace; - live sessions from
sessionQuery.listSessions()ordered bycreatedAtdesc — excludes persisted "ghost" sessions (a historical session'screatedAtcan be the largest while it is no longer the current workspace); - All
sessionQuery.listSessions()records (including persisted) ordered bycreatedAtdesc; - Fallback to
sandboxPolicy.workspaceRoot(re-read dynamically each time). Tool execution additionally honors the caller agent's (exec.agent) sessioncwdto precisely target the current workspace;unidoc.rootacceptsrefresh: trueto drop the cache and re-resolve, letting the client sense workspace switches.
- Client
- Write policy: the plugin-context fs backend's default sandbox root is not the session workspace, so all write paths (save / create / edit) explicitly pass a
SandboxExecutionPolicy(workspaceRoot= resolved workspace); tool calls respect session-mode overrides, e.g.read-onlysessions reject writes; - Client RPC:
unidoc.root/unidoc.list/unidoc.read/unidoc.save/unidoc.create/unidoc.openExternal(returns a raw-route URL for opening in a new tab) /unidoc.openWithEditor(child_process.spawnfor the external editor:editorCmdstrictly validated, path guarded byfs.contains,detached+stdio: ignore+unrefso the host is never blocked) - HTTP routes (random prefix, auto-reclaimed via
ctx.effect):GET <rawPrefix>?p=<relative path>serves raw bytes for images / PDF / HTML, attachingContent-Security-Policy(no scripts / no connections) andX-Content-Type-Options: nosniffto HTML responses - Registers 3 dynamic tools via
harness.defineTool+harness.registerTool, mounted on the plugin Fiber (ctx.effect) and auto-unregistered on stop / update
- Dependency declaration:
- Client side (
src/client.js, runs in the browser page)- Dependency declaration:
inject: ['slots', 'timer'] - Pure
React.createElement(no JSX, no bundler); styles injected viastyles.insertusing--dsw-alias-*theme tokens (auto-adapts to light/dark themes) - Self-built lightweight Markdown renderer and code tokenizer/highlighter (inline parsing fully escaped, XSS-safe)
- File-tree icons embed official Font Awesome 6 Free Solid SVG paths mapped by extension (no FA font required in the GUI); the entry icon is
fa-file-pen - Workspace awareness: syncs via
unidoc.root(refresh)on open and every 5s, fully resetting the tree (cache, expanded state, selected path, scroll position) and reloading the current workspace's files on switches; every call carries the currently selected session's workspacecwdashintCwd(from the runtimesessionsservice), so the host hits the current workspace precisely even when you switch to an old historical session — no stale root can survive; without a hint the host falls back to candidates (online agents newest-first → live sessions → persisted sessions → fallback root); "Expand All" loads recursively in async batches (hidden directories included) without freezing on huge repos - All file I/O goes through
host.callto the host side; never touches page globals directly
- Dependency declaration:
Lifecycle
- On plugin stop / update / removal: tool registrations, HTTP routes, slot registrations, styles and timers are all auto-reclaimed (Cordis Fiber effects & disposer mechanism);
- The Document Center's open state and options are session-level in-memory state, cleared when the plugin unloads (dynamic plugins are not persisted to disk).
Installation & Running
This repository is the source & documentation repo for dsh-unidoc; the plugin is published as a DSH
static Cordis plugin package (lib/ build artifacts are committed with the repo), and can also be
installed directly as a DSH profile dependency:
# Install as a DSH profile dependency (lib/ ships in the package; prepare also builds automatically)
npm install git+https://github.com/Che-Year/dsh-unidoc
Development (source → artifacts):
# 1. Install build deps (esbuild)
npm install
# 2. Syntax smoke check (isomorphic with DSH define-time preflight)
npm run check
# 3. Build artifacts into lib/ (esbuild bundles the host + custom bundler for the client)
npm run build
# 4. Deploy into a session: submit both sides' source with cordis_define (code.host / code.client),
# then activate with cordis_run (client-side activation requires approval on first run)
After activation:
- An icon-only entry (Font Awesome file-pen) appears at the bottom of the sidebar;
- The
doc_read/doc_edit/doc_createtools appear on the agent side.
Persistent deployment: to auto-load with Harness startup, migrate both sides' source into a static plugin package (dsh-web-ui family style), or place it into the corresponding preset under
~/.dsh/.agent-presets.
Configuration
External editors are configured as a list (session-level in-memory state, cleared when the plugin unloads):
- Open the Document Center → "⚙ Options" (bottom-left) → "External editor list";
- Built-in defaults: VS Code (
code), Sublime Text (subl), Atom (atom), Notepad++ (notepad++), Vim (vim), Neovim (nvim), Typora (typora); - Add / remove / rename entries freely: edit name & command per row, ✕ removes, the bottom "+" adds a new editor;
- Clicking "Open Externally" on any toolbar pops up the editor picker; choosing one calls
unidoc.openWithEditorand remembers your last choice as the default for next time; - Command constraints: a bare command name or an executable path only (no spaces, no shell metacharacters), and it must be on the system
PATH(e.g. VSCode'scoderequires "Install 'code' command" first); target file paths are always guarded byfs.containsagainst directory traversal.
Changelog
| Version | Highlights |
|---|---|
| v0.3.6 | Fixed: tree / root not refreshing after a workspace switch (stuck on an old workspace) — the client's authoritative signal (sessions.list.getSnapshot().current) is now retried after a delayed startup so it always reaches the host; the no-hint fallback no longer always hits the session with the largest createdAt when multiple existing sessions coexist; added a hintCwd receipt log and a 42-assertion automated suite (tests/root-resolution.test.mjs) |
| v0.3.5 | Fixed v0.3.4 regression: sidebar plugin icon disappeared (client-half crash) — the DSH client runtime has no timer service, so v0.3.4's blanket ctx[name] forwarding made the Cordis proxy throw (cannot get property "timer" without inject) and crashed the whole client apply; the timer bridge is restored (checked first) and the remaining services are forwarded safely (try/catch, undefined on absence) while keeping the v0.3.4 hintCwd workspace-isolation capability |
| v0.3.4 | Fixed: "always showing the old workspace A" (authoritative-signal fix) — the client reads the currently selected session's workspace cwd from the runtime sessions service and sends it as unidoc.root(hintCwd); the host prefers it, and the no-hint fallback now prioritizes live sessions over persisted "ghost" records, so switching to an old historical session no longer leaves a stale root |
| v0.3.3 | Fixed: tree still showing the old workspace after a switch (root cause) — browser RPCs run outside the agent initiator boundary, so agents.currentInitiator() was unavailable and agents.list() hit a stale online agent from the workspace you just left; the host root resolution was reworked to "recent session first" (newest session → online agents newest-first → dynamic fallback root), and the tree / path state is fully reset on refresh and workspace switches |
| v0.3.2 | Faster workspace-switch sensing + full tree reset — runtime polling shortened to 5s; after a workspace switch the tree is fully reset (cache cleared, expanded state, selected path and scroll position reset to the root, preview closed) with a "workspace switched, tree refreshed" toast |
| v0.3.1 | Fixed: file tree not refreshing after a workspace switch — reopening the Document Center after switching agents/sessions now resets and reloads the tree with the new workspace's files, with no stale data left behind; the top path and the tree stay consistent |
| v0.3.0 | Workspace detection & display; file-tree "Expand All / Collapse All" (hidden dirs included); external editor picker menu with an editable editor list; icon-only sidebar entry with the Font Awesome fa-file-pen icon |
| v0.2.0 | Open HTML preview in a new tab; external editor integration (RPC + command config); Font Awesome file icons by extension in the tree; fixed missing lib/ on git install that broke startup |
| v0.1.0 | Initial release: Document Center workbench (file tree + multi-format preview/edit + save), agent tools doc_read / doc_edit / doc_create |
Full details in CHANGELOG.md.
Development & Testing
node scripts/check.js: syntax smoke test for both sides' source;node tests/root-resolution.test.mjs: automated suite (42 assertions) — root resolution & workspace isolation (hintCwd authoritative signal / candidate order / path safety / agent tools);tests/verification.md: manual E2E verification checklist (mounting, file tree, per-format preview, saving, Toasts, tool calls, edge cases);- Development conventions: never modify any official source under
~/.dsh/source/current/; mount capabilities only through the official dynamic-plugin mechanism; reuse official Service/Slot capabilities (fs,webServer,slots,timer).
Origin & License
This plugin builds on / reuses the architecture of dsh-better-sidebar; thanks to the original author.
- This plugin is released under the MIT License; the
LICENSEfile retains the full copyright notices and license terms of the upstream projects (dsh-better-sidebarand the DSH core framework, both MIT-licensed); - This repository never modifies, copies, or mixes in any official source under
~/.dsh/source/current/; capabilities are only mounted at runtime through the official DSH dynamic-plugin mechanism, avoiding derivative-work confusion and compliance risks.
Links
More in this category
Tencent/WeKnora#dsh-weknora★ 32466
Four read-only tools over a WeKnora knowledge base: list knowledge bases, hybrid passage search, reassemble one document's chunks in order, and WeKnora's own cited RAG or ReAct-agent answer with a resumable session id.
superdesigndev/treg★ 4760
Tool catalog for agents: search ~2,600 external endpoints (SEO and SERP, backlinks, social, people and company enrichment, ad libraries, scraping) by the task you want done, read each one's parameters and per-call price, then call it with the credential injected server-side. Ships the skill plus an MCP row that stays disabled until TREG_TOKEN is set.
TencentCloudBase/CloudBase-AI-Toolkit#dsh-plugin★ 1132
Tencent CloudBase backend for DeepSeek Harness — scaffold and deploy full-stack apps from chat, render query results as table cards with paging, sorting and CSV export, preview a deployment on its domain, and call the CloudBase MCP toolset (`mcp__cloudbase__*`) with device-code login.
gitroomhq/postiz-agent#dsh-postiz★ 506
Connects DeepSeek Harness to Postiz over MCP: list connected social media channels, fetch per-platform posting rules, and schedule, draft, or publish posts to X, LinkedIn, Instagram, Facebook, Threads, TikTok, YouTube, Reddit, Bluesky, Mastodon, Discord, Slack, Telegram and more; adds a postiz workflow skill.
EthanYoQ/Invoice-Downloader#dsh-invoice-downloader★ 500
Local IMAP invoice download, OCR, archive, and Excel reimbursement summaries for DeepSeek Harness.
anysearch-team/anysearch-dsh★ 450
AnySearch-powered real-time web and vertical search provider for DeepSeek Harness.
Community comments
Comments are public GitHub Discussions. Loading them connects to GitHub and Giscus; a GitHub account is required to post.