oneirictouch/dsh-explorer-editor

dsh-explorer-editor

左侧边栏的“资料浏览器”和主工作区的“文本编辑器”,页签方式展示,适合讨厌工作区被过度分割的人。

Uncategorized

dsh-explorer-editor logo

GitHub · Installation · Theme import/export · FAQ · Issues · Releases

English · 简体中文


dsh-explorer-editor

A VS Code-style file manager plugin for DeepSeek Harness Web: browse the current conversation's workspace from the sidebar and edit files in the center column.

DeepSeek Harness 的 VS Code 风格文件管理器插件:在 Web 侧边栏浏览当前对话工作区的文件,在中间主区域编辑。

Screenshot

dsh-explorer-editor in action: sidebar file tree, Monaco editor in the center column

Browse the workspace in the sidebar tree; clicking a file opens it in the center-column "Files" view (Monaco editor with syntax highlighting).

Installation

# Run from inside the cloned dsh-explorer-editor directory (not its parent)
cd /path/to/dsh-explorer-editor
dsh plugin --profile web add .

dsh plugin add pnpm-links the package into the profile and appends it to dsh.profile.bundles. Restart dsh web to take effect (client plugin metadata is cached by name; it is rescanned after a restart).

DSH Desktop install

The desktop client is the deepseek-harness-desktop project (package dsh-plugin-desktop). It uses a separate profile (desktop) from dsh web (web), and plugins are not shared between them — a plugin installed only into the web profile will not appear in the desktop client, nor in its Settings → Plugins list:

# Run from inside the dsh-explorer-editor directory as well
cd /path/to/dsh-explorer-editor
dsh plugin --profile desktop add .

After installing, fully quit and relaunch the desktop app (quit the application, not just close the window); dsh-explorer-editor will then show up in the sidebar "Workspace / Files" tabs and in Settings → Plugins.

Note: do not add plugins to ~/.dsh/profiles/desktop/cordis.yml — the desktop client rewrites it to an empty list [] on every startup. The correct entry point is dsh.profile.bundles + dependencies in the profile's package.json (which is exactly what dsh plugin add does).

Install from npm

dsh plugin --profile web add dsh-explorer-editor

Or download the tarball from Releases and install it locally (use --profile desktop for the desktop app):

dsh plugin --profile web add ./dsh-explorer-editor-0.11.1.tgz

Configuration

The root in cordis.patch.yml is only the fallback root when there is no session (defaults to process.cwd()). When the file manager opens, the browser resolves the current conversation's workspace directory and re-pins the root via setRoot, so usually nothing needs to change:

- insert:
    - id: dsh-explorer-editor
      name: 'dsh-explorer-editor'
      config:
        root: !!js process.cwd()   # fallback root only, before the file manager pins the session workspace
        allowArbitraryRoot: true   # whether setRoot may escape root (see below)

Security boundary: allowArbitraryRoot defaults to true, so setRoot follows the current conversation's workspace (which may live outside root). To restrict setRoot to directories under root (preventing the RPC from reading files outside the workspace), set it to false. All file operations still respect the workspace boundary — any path escaping the currently pinned root is rejected (including symlink escapes).

Features

  • "Workspace / Files" tabs in the sidebar header: the tab strip renders in the workspace browser's header row (replacing the "Workspace" label); clicking "Files" switches the sidebar body to the file manager (file tree), clicking "Workspace" returns to the workspace/session list
  • Workspace follows the active conversation: opening the file manager resolves the current session's workspace directory (SessionHeader.cwd) and re-pins the gateway root via setRoot — no longer the directory dsh web was launched from
  • Center-column editor (view tab): the editor is registered as a conversation.view view ("Files" tab, alongside Chat/Trajectory). Clicking a file opens it inside the session scroll area of the page (not a popup): Monaco Editor (the same kernel VS Code uses, loaded from CDN) with extension-based syntax highlighting; falls back to a plain textarea when the CDN is unreachable
  • Markdown preview: .md files open in source mode (editable) by default; a VS Code-style preview/source toggle button sits next to the "Theme" button in the toolbar (shown only for Markdown files) to switch to a read-only rendered preview (marked + GFM: headings, lists, tables, task lists, code blocks). The chosen mode is remembered (localStorage) and reused on the next open
  • Theme settings (VS Code style): the "Theme" button in the editor toolbar opens a settings panel — light by default, presets selected via a dropdown (Light/Dark/One Dark/GitHub), plus custom background / foreground colors and font size (10–28px), applied live to Monaco and the editor chrome (toolbar/status/tabs follow the background), persisted to localStorage
  • Theme import/export: export the current theme to a JSON file and import it back, just like VS Code, to migrate your colors between environments (see Theme import/export)
  • Edit & save: Ctrl+S or the "Save" button in the editor, dirty marker (●); multiple open files switch via the top tab strip, each tab has a ✕ close button
  • File operations: create file, create directory, rename, delete (delete requires confirmation; non-empty directories are rejected)
  • Keyboard navigation: the file tree is keyboard-navigable — ↑/↓ move the selection, → expands a collapsed directory, ← collapses an expanded one, Enter/Space opens a file or toggles a directory (VS Code Explorer-like)
  • Right-click context menu (VS Code style): right-click a file/directory for Cut / Copy / Rename / Delete / Copy Path / Copy Relative Path; right-click a directory or blank tree area to additionally Paste (cut → move, copy → recursive copy, existing targets are not overwritten). Delete asks for confirmation and rejects non-empty directories. Cut sources are dimmed; the clipboard survives panel switches (lost on page reload)
  • Encoding auto-detection: text files are read as UTF-8 first, falling back to GBK/GB2312 when the bytes are not valid UTF-8 (common for Windows-generated logs/exports; falls back further to latin1 on non-full-ICU builds) — no more garbled Chinese. Saving the file normalizes it to UTF-8
  • Session restore (survives reloads): open tabs and unsaved edits persist to localStorage automatically — refresh the page and the last tabs (with unsaved changes) come back. Files larger than 256KB restore their tab only and re-read content from disk; tabs never leak across workspaces; storage is per-browser
  • Live tree refresh (SSE push): the host watches the workspace with recursive fs.watch and pushes filesystem changes to the browser over SSE — the tree reloads only the affected directories (VS Code Explorer-like). Refocusing the window triggers a full backstop refresh
  • Workspace boundary: every path resolves against the currently pinned root; escaping paths are rejected by the host (including symlink-escape protection)
  • Bilingual UI: every UI string is localized through the locale dictionary (dshFile namespace, zh/en) and follows the DSH language — no more mixed Chinese/English

Theme import/export

The theme panel (the "Theme" button in the editor toolbar) can export the current theme to a JSON file or import one back — the same idea as VS Code theme files, handy for moving your colors across machines or environments.

Export a theme

  1. Open the file editor (the "Files" view in the center column).
  2. Click the Theme button in the toolbar to open the settings panel.
  3. Click Export theme — the browser downloads a dsh-explorer-editor-theme-YYYY-MM-DD.json file.

The exported JSON carries both the plugin's flat fields and VS Code workbench colors:

{
  "name": "dsh-explorer-editor · One Dark",
  "type": "dsh-explorer-editor-theme",
  "version": 1,
  "background": "#282c34",
  "foreground": "#abb2bf",
  "fontSize": 13,
  "colors": {
    "editor.background": "#282c34",
    "editor.foreground": "#abb2bf"
  }
}

Import a theme

  1. Open the theme settings panel.
  2. Click Import theme and pick a JSON file.

Accepted formats:

  • This plugin's export format (background / foreground / fontSize);
  • VS Code theme JSON: reads colors["editor.background"] and colors["editor.foreground"] (tokenColors are not applied yet — syntax highlighting keeps Monaco's built-in colors).

On success the colors apply immediately and are persisted to localStorage; invalid JSON or missing valid colors shows an error in the panel.

Architecture

The plugin has two halves sharing the package name dsh-explorer-editor:

Host half (Node process)Client half (browser React)
Sourcesrc/index.tssrc/client/
Build outputdist/index.js (tsc, keeps standard decorators)dist/client.js (esbuild, ModuleLoader bundle)
ResponsibilityFilesystem RPCSidebar file tree + center-column editor view
Key APIclass FileManagerGateway extends TypertRemoteService + @Remote()ctx.slots.register(), ctx.remote.$mount()

Host ↔ Client communication (Typert Remote)

Browsers cannot touch the filesystem directly, so the host half exposes file operations as RPC endpoints (namespace fileManager: listDir / readText / readDataUrl / writeText / createFile / createDirectory / rename / copy / delete / stat / resolve / getRoot / setRoot). The client mounts the call surface with ctx.remote.$mount(TYPERT_REMOTE) and resolves the service via ctx.get('remote.fileManager'). setRoot re-pins the gateway root to the current session's workspace directory.

Key constraint (SRC descriptor contract): the Typert gateway derives wire parameter names from method signatures via Function.prototype.toString — host methods must use flat parameters (listDir(path: string), not listDir(input: {...})); the parameter names are the wire fields the client sends. Both halves must use identical names.

Panel toggle mechanism

The sidebar main area is the single-seat sidebar.workspaces slot (occupied by the workspace browser at priority 0). The plugin registers its own shadow entry at priority: -1 when the button is clicked — a single-seat slot renders the lowest-priority live entry, so the file manager wins the cell; closing disposes the entry and the workspace browser returns. After clicking a file in the tree, the editor renders in the "Files" view registered in conversation.view — the session scroll area of the center column (alongside chat / trajectory), entered via the "Files" tab in the session header, never a popup.

Base-package patch: the sidebar.workspaces.tabs slot must be hosted by the workspace browser bundle (@deepseek-ai/dsh-client-ui-workspace/lib/client.js) — declare the sub-slot in the children of the sidebar.workspaces registration, inject the slot's subscribe/getSnapshot in browserInjected, and render the slot in place of sectionLabel in the WorkspaceBrowser header using useSyncExternalStore to check hasTabs. Upgrading dsh overwrites that file, so the patch must be re-applied after an upgrade (local backups live under dsh-tools/backup/ as dsh-client-ui-workspace.client.js.bak-*). Without the patch the plugin falls back to the sidebar-footer "Files" button.

Dependency resolution (important)

The @deepseek-ai/* packages must not be installed as copies inside the plugin's own node_modules: @Remote decorator markers live in a module-level WeakMap, and if the plugin and the api-gateway each hold a separate dsh-typert-protocol instance the markers are invisible to each other (RPC returns 404). Node must resolve to the same instance as the dsh installation:

# Local development (when dsh is installed locally via npx):
ln -s ~/.dsh/profiles/node_modules/@deepseek-ai node_modules/@deepseek-ai

At startup dsh maintains a flat symlink fallback at $DSH_HOME/profiles/node_modules (healProfilesModuleFallback) pointing at every package in the dsh installation. For production releases the plugin declares @deepseek-ai/* as peerDependencies, provided by the profile.

Desktop app (deepseek-harness-desktop) notes:

  • On startup the desktop app re-points ~/.dsh/profiles/node_modules/@deepseek-ai at the packaged Desktop.app directory (/Applications/DSH Desktop.app/.../app.asar.unpacked/node_modules), which strips .d.ts files — keeping the symlink above pointing at profiles guarantees the plugin loads the same runtime instance as the desktop api-gateway (RPC works).
  • The stripped types break tsc. tsconfig.json uses paths to map the compile-time lookup of @deepseek-ai/* to the global dsh install (which ships full .d.ts); runtime resolution is unaffected (Node still walks the node_modules symlink → profiles → desktop instance). Adjust the path per the comment in tsconfig.json if your global dsh lives elsewhere.
  • Do not run npm install inside the plugin directory: npm dereferences the node_modules/@deepseek-ai symlink into a real directory and corrupts the profiles symlink structure, making dsh fail with "exists and is not a symlink". If you must add dependencies, re-run the ln -s above afterwards.

Development

npm install                       # esbuild + typescript + types
node build.mjs                    # build host (tsc) + client bundle (esbuild)
node build.mjs --watch            # watch host (tsc --watch) + client (esbuild)

Build outputs:

  • dist/index.js — host half (Node ESM; compiled with tsc to keep the standard stage-3 decorators; esbuild would lower @Remote to the legacy form and crash at runtime)
  • dist/client.js — client half (window.__ModuleLoader__.load({id, factory}) format; react and other seed words stay external)

Debugging

dsh --profile web --dump-config | grep -A4 dsh-explorer-editor   # confirm the plugin layer is composed
# Test RPC (requires a running dsh web)
curl -X POST http://127.0.0.1:3080/api/fileManager/getRoot \
  -H 'Content-Type: application/json' \
  -d '{"type":"client-request","rpcId":"t","method":"fileManager/getRoot","payload":{"args":{}}}'

FAQ

  • RPC returns not found: almost always the @deepseek-ai/dsh-typert-protocol dual-instance problem — check whether the plugin's node_modules/@deepseek-ai is a symlink (ls -la node_modules/@deepseek-ai); if not, create the link as described above and restart.
  • Blank editor: Monaco loads from a CDN (jsDelivr → unpkg → Fastly, tried in order; set the localStorage key dsh-explorer-editor:monaco-mirror to use a private mirror). If all mirrors are unreachable it falls back to a plain textarea.
  • Wrong directory opened: verify the current session's workspace directory (the sidebar title shows the directory name). The file manager auto-runs setRoot to the current session's cwd; without a session it falls back to cordis.patch.yml's root.
  • Plugin changes have no effect: host-half changes require restarting dsh web; client-half bundle changes only need a page refresh (a rev change triggers a reload).

License

MIT