dsh-at-file
FSMargoo/dsh-at-fileCodex 风格的 `@file` 文件引用,输入框里直接搜索并引用工作区文件。
VS Code 风格文件管理器:侧边栏文件树浏览当前对话工作区,中间列用 Monaco 编辑文件,支持主题导入导出与 Markdown 预览。
GitHub · Installation · Theme import/export · FAQ · Issues · Releases
English · 简体中文
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 侧边栏浏览当前对话工作区的文件,在中间主区域编辑。
Browse the workspace in the sidebar tree; clicking a file opens it in the center-column "Files" view (Monaco editor with syntax highlighting).
# Run from inside the cloned dsh-file directory (not its parent)
cd /path/to/dsh-file
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).
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-file directory as well
cd /path/to/dsh-file
dsh plugin --profile desktop add .
After installing, fully quit and relaunch the desktop app (quit the application, not just close the window); dsh-file will then show up in the sidebar footer "Files" button 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 isdsh.profile.bundles+dependenciesin the profile'spackage.json(which is exactly whatdsh plugin adddoes).
dsh plugin --profile web add @rose43/dsh-file
Or download the tarball from Releases and install it locally (use --profile desktop for the desktop app):
dsh plugin --profile web add ./rose43-dsh-file-0.2.1.tgz
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-file
name: 'dsh-file'
config:
root: !!js process.cwd() # fallback root only, before the file manager pins the session workspace
SessionHeader.cwd) and re-pins the gateway root via setRoot — no longer the directory dsh web was launched fromconversation.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.md files open as a read-only rendered preview by default (marked + GFM: headings, lists, tables, task lists, code blocks). A VS Code-style preview/source toggle button sits next to the "Theme" button in the toolbar (shown only for Markdown files); the chosen mode is remembered (localStorage) and reused on the next openroot; escaping paths are rejected by the host (including symlink-escape protection)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.
dsh-file-theme-YYYY-MM-DD.json file.The exported JSON carries both the plugin's flat fields and VS Code workbench colors:
{
"name": "dsh-file · One Dark",
"type": "dsh-file-theme",
"version": 1,
"background": "#282c34",
"foreground": "#abb2bf",
"fontSize": 13,
"colors": {
"editor.background": "#282c34",
"editor.foreground": "#abb2bf"
}
}
Accepted formats:
background / foreground / fontSize);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.
The plugin has two halves sharing the package name dsh-file:
| Host half (Node process) | Client half (browser React) | |
|---|---|---|
| Source | src/index.ts | src/client/ |
| Build output | dist/index.js (tsc, keeps standard decorators) | dist/client.js (esbuild, ModuleLoader bundle) |
| Responsibility | Filesystem RPC | Sidebar file tree + center-column editor view |
| Key API | class FileManagerGateway extends TypertRemoteService + @Remote() | ctx.slots.register(), ctx.remote.$mount() |
Browsers cannot touch the filesystem directly, so the host half exposes file operations as RPC endpoints (namespace fileManager: listDir / readText / writeText / createFile / createDirectory / rename / 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.
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.
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:
~/.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).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.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.npm install # esbuild + typescript + types
node build.mjs # build host (tsc) + client bundle (esbuild)
node build.mjs --watch # watch client only (rerun for host changes)
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)dsh --profile web --dump-config | grep -A4 dsh-file # 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":{}}}'
@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.setRoot to the current session's cwd; without a session it falls back to cordis.patch.yml's root.dsh web; client-half bundle changes only need a page refresh (a rev change triggers a reload).