Web 增强
A Web-enhanced plugin for DeepSeek Harness: task board with cron scheduling, git graph, preview/files/SCM right panel, and DeepSeek API balance line. 🔌 Ecosystem: the repo carries the #dsh · #dsh-plugin topics — welcome to be listed by @dsh-plugin.
이것은 DeepSeek Harness(DSH) 플러그인입니다. 이 사이트는 GitHub README, 설치 정보, 유지보수 상태, 공개 보안 시그널을 모아 보여줍니다.
업스트림에서 중국어 README를 제공하지 않아 저장소 원본 내용을 표시합니다.
dsh-web-enhanced
English · 简体中文
A Web-enhanced plugin for DeepSeek Harness: task board with cron scheduling, git graph, preview/files/SCM right panel, and DeepSeek API balance line.
🔌 Ecosystem: the repo carries the
#dsh·#dsh-plugintopics — welcome to be listed by @dsh-plugin.
Developed and built independently of the deepseek-harness repo — the plugin only consumes the officially published @deepseek-ai/* packages and the existing Web client slots. No harness source is modified.
Features
| Feature | Description |
|---|---|
| Task board | Sidebar entry opens a board with five columns (Planned / To do / Running / Done / Failed). 「Run」opens a real DSH agent session that executes the task prompt — composed from the deployment's agent preset (so it has bash / read_file / write_file) and attached to the task's project — and the status and result write back automatically when it finishes. 「View session」jumps to the execution session. Each card has an inline edit form (title / prompt / cron / column — done or failed tasks reopen via planned/todo). Supports 5-field cron scheduling (e.g. 0 23 * * *): runs automatically at the due time, catches up after a host restart, and recovers interrupted runs. |
| Git graph | Sidebar entry opens a graph overlay; branch lanes + commit history rendered as SVG (first-parent continuous lanes + horizontal merge links). The header's branch dropdown filters which commits the graph DRAWS (all branches, or one) and changes nothing in the repository; clicking a commit expands its full hash, parents, author and email, date, message body, and per-file added/removed line counts. The branch strip above the composer is the other operation — it checks a branch out. |
| Workspace view | A Workspace tab in the conversation's view ring, beside Chat and Trajectory, with three panes (Files / Preview / Changes). The file tree expands, searches by name, and opens files in preview; preview supports markdown (GFM tables, HTML tables, and inline HTML) / HTML (sandboxed iframe) / code / diff (line-highlighted unified diff) / CSV / images / PDF / text / Office docx & xlsx (host-side structural conversion) with source / split (editor + preview side by side) / view modes and save. The Changes pane is backed by real git status with stage / unstage / discard and per-file diffs. The active pane and the open directories persist per workspace. |
| File mentions | 「Mention file」and「Mention folder」in the composer's + menu: a flat, locally filterable list of the project's entries, with a first row「Browse elsewhere…」that opens the plugin's own file browser and walks any directory outside the project (breadcrumbs / parent / home / filter by name). Picking one inserts its @path into the draft (paths with spaces are quoted). |
| Balance line | Shows the DeepSeek API balance (GET /user/balance) below the composer, with a refresh button and a muted error state. Only while the session's model route actually bills that account — switching to another channel (or repointing deepseek-official at a private gateway) hides the whole line, because the number would then be about somebody else's account. |
| Settings page + plugin management | One more row in the Settings nav, "Web Enhanced" (registered into settings.section). Its Plugins tab lists what the current profile has installed — name, version, dependency spec, whether it is an active layer — and offers Update and Remove. What it lists is the profile package.json's dependencies, because that is the set pnpm can act on; template layers (@deepseek-ai/dsh-base and friends) are shown apart with no buttons, since no dependency provides them. Every operation takes effect on the next start (the layer stack is composed at boot) and the UI says so. Removing this plugin itself is not blocked — the confirmation just spells out what it costs. |
Screenshots
Captured from the real UI by scripts/e2e.mjs --capture (no model key needed):
| Task board | Git graph |
|---|---|
![]() | ![]() |
| Floating panel | Balance line |
|---|---|
![]() | ![]() |
Installation
The plugin is a bundle combo package (dsh.bundle) installed into a Web profile:
dsh plugin --profile web add git+https://github.com/banlanzs/dsh-web-enhanced.git # recommended
# or:
# dsh plugin --profile web add ./dsh-web-enhanced-0.6.0.tgz
# dsh plugin --profile web add dsh-web-enhanced
lib/ is committed, so there is no prepare step and installing from git needs no toolchain and no allowBuilds prompt.
Install it, do not
link:it. Every@deepseek-ai/*package is a peer dependency and must resolve to the single copy the profile provides. Node resolves a symlinked package from its REAL path, so alink:-installed plugin resolves those specifiers inside its ownnode_modulesinstead — a second@deepseek-ai/dsh-typert-protocolinstance. The@Remotedecorator records its markers in that module's private state, so the host gateway (holding the other instance) then sees no descriptors at all and every/api/webEnhanced/*answers 404 while the client half still loads and renders. Verify a suspicious install with:node -e "console.log(require.resolve('@deepseek-ai/dsh-typert-protocol',{paths:['<profile>']}))" node -e "console.log(require.resolve('@deepseek-ai/dsh-typert-protocol',{paths:['<plugin>/lib']}))"The two paths must be identical.
Then start:
dsh --profile web
One-click script
After cloning, just run it — it checks the prerequisites (dsh / pnpm / repo reachability), installs via the public git URL, and prompts you to restart:
git clone https://github.com/banlanzs/dsh-web-enhanced.git
cd dsh-web-enhanced
./scripts/install.sh
Updating
No uninstall-then-reinstall needed. dsh plugin is a pnpm forwarder: it hands your arguments verbatim to pnpm in the profile directory, then reconciles the bundle layer list against the installed state. So an update is one command, then a restart:
dsh plugin --profile web update dsh-web-enhanced
dsh --profile web
The thing to know: install will not pick up new commits, update will. A ref-less spec like github:banlanzs/dsh-web-enhanced tracks the default branch, but pnpm pins whichever commit it resolved into the profile's lockfile:
dsh-web-enhanced: github:banlanzs/dsh-web-enhanced
→ codeload.github.com/banlanzs/dsh-web-enhanced/tar.gz/<commit>
pnpm install honours that lock and reinstalls the same commit; update re-resolves the branch head and rewrites it.
Reconciling by installed state rather than by dependency diff is deliberate: it is what lets update activate a package that only started declaring dsh.bundle in a newer version.
If an update ever fails to move (pnpm can hold on to a cached git resolution), the fallbacks in order are --force, and only then remove + add:
dsh plugin --profile web update --force dsh-web-enhanced
# last resort
dsh plugin --profile web remove dsh-web-enhanced
dsh plugin --profile web add git+https://github.com/banlanzs/dsh-web-enhanced.git
Developer iteration
link: is NOT usable for this plugin (see the note above — it duplicates the
harness packages and silently disables every host capability). Iterate by
reinstalling from a packed tarball instead:
cd dsh-web-enhanced
pnpm install && pnpm run check && npm pack
dsh plugin --profile web remove dsh-web-enhanced
dsh plugin --profile web add ./dsh-web-enhanced-0.6.0.tgz
On Windows, tarball installs need real symlink permission (pnpm's
importPackage step). If it fails with EPERM ... symlink, either enable
Developer Mode or install from the git URL, which does not take that path.
Configuration
Plugin-row config fields (all have defaults):
| key | default | meaning |
|---|---|---|
cronIntervalMs | 30000 | Scheduler tick interval |
balanceApiKeyEnv | DEEPSEEK_API_KEY | Env var for the balance query API key |
balanceCacheTtlMs | 60000 | Balance view cache duration |
balanceBaseUrl | https://api.deepseek.com | Balance endpoint base URL |
balanceProviders | [deepseek-official] | Model routes the balance line is shown for; a route with its own configured baseURL must also share the endpoint's host |
skipDirs | [node_modules] | Directories skipped by the file tree/search (.git is always skipped) |
readMaxBytes | 1 MiB | Text read cap (truncated with a marker beyond it) |
writeMaxBytes | 2 MiB | File write cap |
binaryMaxBytes | 5 MiB | Binary preview (base64) cap |
gitOutputMaxBytes | 256 KiB | Single git stream output cap |
gitMaxCount | 100 | git log row cap |
searchMaxDepth / searchMaxEntries | 8 / 200 | File search depth and entry caps |
officeMaxBytes | 5 MiB | Office preview (docx/xlsx) file size cap |
browseMaxEntries | 500 | Entry cap of one directory level in the mention browser |
pluginOpTimeoutMs | 300000 | Deadline for one pnpm operation (update/remove) |
profileDir | empty | Profile directory; empty walks up from this module. Only for a deployment whose profile is not an ancestor of the loaded plugin |
Architecture
-
Zero harness changes: the client UI only registers into existing slots.
sidebar.footer.action— the task-board and git-graph entry buttons.shell.overlay— the board and the graph themselves. This is the frame-wide floating layer: above every column, outside their scroll containers, additive (a list slot), and click-through until an entry opts into pointer events.conversation.view— the Workspace tab, one entry in the view ring beside Chat and Trajectory. The ring renders one view at a time at full column width, so this surface owns no geometry: no docking, no drag-to-resize, no collapse. Those belong to the frame.conversation.input.dock— the branch strip (above the composer), aligned to the input card through the column's shared width variables.conversation.composer.dock— the balance line (below the composer).
Nothing registers into the layout's
detailsslot: that is asingleslot already occupied by ui-conversation'sDetailsPanel, so registering there would replace the tool-details column and remove theconversation.details.toolseat it declares. Beside the slots, two client commands are registered throughctx.commandUi.register— the file and folder mention pickers in the composer's+menu. -
Optional services are read uninjected:
agentPresets,llm,settings,credentials,modelDirectories,commandUi, andconversationall come fromctx.get(). A deployment composed without one degrades exactly that surface instead of leaving this plugin's entry waiting on a service it may never get. -
One request object per remote method: the Typert gateway maps
descriptor.parameterspositionally onto the host method (Reflect.apply) and both halves reject a mismatched argument count, so a descriptor's parameter list is the host signature. Every method here declares exactly onerequestparameter;tests/contribution.spec.tsguards it. -
Hand-written remote contribution: host methods use the
@Remotedecorator (Typert SRC mode; the host gateway auto-discovers thectx.webEnhancedservice); the client mounts a hand-declared src-json contribution inapply— no typert generation pipeline. -
Cross-scope shared state: the overlays are
root-scoped and the branch strip and balance line aresession-scoped, so a single slot-store handle cannot serve both ("one handle, one scope"). Shared state lives inapplyas plain observables and reaches components through each registration's injecthookscompartment. -
Task execution:
agentPresets.resolve()names the deployment preset, it is recorded onmeta.agentPresetand mounted insidesetup(the host's ownensureSessionorder), thenworkspace.attachSessionrecords the run's session on its project; the run itself isfollowup+whenIdle+sessions.flushand the result is written back from theturn/endreason. A deployment with no preset roster still runs tasks — its sessions just carry whatever the host root registered. -
Persistence: task records live in the
ctx.storageDomaindomainweb_enhanced(JSON backend); restart recovery settlesrunning→failed(host-restart). Panel geometry (width, collapsed, expanded directories) persists tolocalStoragekeyed per workspace. -
Path safety: every fs/git path is validated against the workspace root (absolute paths,
.., and backslashes are rejected); a single-ref argument rejects a leading-,..ranges, and whitespace or globs, so one argument can never become two or become an option; git output is collected with bounds; file reads have byte caps and binary sniffing. Office files are converted on the host (fflate) into bounded structural blocks — headings, paragraphs, list items, tables (≤ 2000 blocks, ≤ 200×50 table) — never raw HTML. -
The one exception,
fsBrowse: it lists any absolute directory and is deliberately not workspace-scoped, because a mention produces a path STRING and the path the user wants may sit outside the project. It returns names, kinds, and sizes only; reads, writes, and previews all stay behind the workspace root. -
Plugin management modifies no host file: the settings page registers into the existing
settings.sectionslot, and the inventory rides this plugin's own Typert gateway — so unlike DSH-vision it needs no edit to the api-proxy's settings allowlist (that patches the host's published output insidenode_modules, which every upgrade overwrites). Remove and update only run pnpm in the profile directory and rewrite that profile'sdsh.profile.bundles, exactly the pathdsh plugintakes.@deepseek-ai/dsh-app-boot, which owns those routines for the CLI, is deliberately NOT a peer dependency: it belongs to the dsh installation rather than the profile, so peer resolution would fail in precisely the deployment this code runs in. -
Preview safety: Markdown, CSV, diff, tables, and Office previews render as React elements, never
dangerouslySetInnerHTML. HTML inside Markdown maps through an allow list to real elements; an unknown tag loses its markup and keeps its text, andscript/stylelose both.javascript:/data:link targets degrade to literal text (adata:image/*picture is the exception), and HTML file previews load in asandbox=""iframe (no scripts, no same-origin access).
Development
pnpm install
pnpm run check # typecheck + full tests + build (212 tests)
Build outputs:
lib/index.js— node half: theweb-enhancedfunction plugin (mounts theWebEnhancedGatewayTypert service: task*/git*/fs*/balanceGet + cron scheduler + restart recovery)lib/client.js— browser half: module-loader closure format (window.__ModuleLoader__.load), declared by thedsh.clientmanifestcordis.patch.yml— bundle patch: inserts theweb-enhancedrow (one row carries both the node and browser halves)
Real-device e2e (no model key)
The full real chain: start a temporary dsh web → install the plugin → open the sidebar board/graph, a session's floating panel and balance line — all asserted in a real browser, nothing mocked:
# needs the host build: DSH_ROOT (default ~/.dsh/source/current) with pnpm run build done
node scripts/e2e.mjs --smoke --install link --port 3190
node scripts/e2e.mjs --capture # also refresh assets/*.png used by this README
Prereqs: dsh/pnpm on PATH, and the main repo's web build output (playwright resolves from the main repo). On PASS it exits 0; failure keeps e2e-fail-*.png screenshots and prints the dsh-web.log tail.
Known limitations
- The workspace surface is a view tab, not a side-by-side column: it replaces the transcript while active rather than sitting next to it, and it owns no width or collapse of its own.
- HTML inside Markdown renders through an allow list:
<table>is read structurally and inline tags map to real elements, everything else keeps only its text.<details>, inlinestyle, and custom elements are not reproduced. - The mention pickers' in-project list is one bounded pass of the host search (
searchMaxEntries, 200 by default); the popup's own search filters that batch locally rather than re-querying per keystroke. Past that cap, and past the project boundary, is the first row's「Browse elsewhere…」. - The mention browser is an in-app file manager, not an operating-system dialog: the host's
host.pickDirectorypicks directories only and only under thenativecapability, and a browser's<input type="file">withholds absolute paths by design. On Windows the drive list comes from 26 concurrentstatprobes (Node exposes no drive table without a native binding), so a disconnected network letter can cost a second or two; a UNC share not mapped to a letter (\\server\share) is not reachable yet. - Office preview is structural: docx headings/paragraphs/lists/tables and the first xlsx worksheet are rendered; inline styles (bold, colors), images, and multi-sheet workbooks are not. Legacy
.doc/.xlsbinaries are not previewable. - Scheduled tasks are best-effort: 30s tick granularity; windows missed while the host is down are caught up once at startup, no backlog is kept.
- The balance key shares its source with the model provider (env var); when unconfigured it shows an error state rather than failing. On a route outside
balanceProvidersthe line is hidden entirely. - The graph lanes use a simplified algorithm (first-parent continuity), not git's full topology coloring; a commit's file list is the first-parent diff, so a merge shows only what it brought in.
- Plugin management does not reload the running process: Cordis composes the layer stack at boot, so an update or removal describes the next start. For the same reason it offers no enable/disable — that edits the profile's
cordis.patch.yml, a different thing from installing. - Plugin management needs
pnpmon PATH and the profile directory on this module's ancestor chain (true of any normal install; a source checkout or a test reports "nothing to manage" rather than an error). One pnpm operation runs at a time — a second request is told so rather than queued.
License
MIT
보안 및 설치 증거
이 점수는 공개 저장소 메타데이터와 이 사이트에 등록된 설치 증거에만 기반하며, 코드 보안 감사와 다릅니다.
공개 플러그인 카탈로그에서 왔으며, 공개 GitHub 저장소로 연결됩니다.
저장소가 MIT 라이선스를 선언했습니다.
최근 180일 내 코드 업데이트가 있습니다.
재현 가능한 정확한 설치 메타데이터가 아직 등록되지 않았습니다. 저장소 설명에 따라 직접 확인하세요.
검사한 패키지 메타데이터에 설치 라이프사이클 스크립트가 선언되지 않았습니다.



