dsh-files-pane
In-browser files pane for the DeepSeek Harness Web GUI: a Session View tab that browses the host's files and reads them in place. · DSH 网页「文件」视图标签:两栏目录导航 + 就地阅读文件
- Stars
- 0
- Language
- JavaScript
- Created
- Sep 10, 2026
- Updated
- Sep 10, 2026
Introduction
dsh-files-pane
An in-browser files pane for the DeepSeek Harness Web GUI:
a Session View tab (对话 / 轨迹 / 文件) that browses the host's files and reads them in place —
instead of handing a path to a native desktop opener, which a headless host cannot honour.
在 DSH 网页面板里内置一个「文件」视图标签:左侧两栏目录导航,右侧阅读文件内容。不改动主窗体, 复用聊天窗自己的 Markdown 与代码渲染,点击会话里的任意文件即在其中打开。
Why it exists
dsh web runs on the machine that holds the files; the browser usually does not. Clicking a file
reference in a conversation calls remote.session.openWorkspacePath, which on a headless host
spawns xdg-open into nothing. This plugin intercepts that one call and renders the file in the
page instead.
Features
- Files as a Session View tab — a third tab beside Chat and Trajectory, full width, one view at a time. No floating window, no sidebar takeover.
- Click-through from the conversation — tool cards,
@pathmentions and produced-file chips open here, and the tab is selected for you. - The chat window's own renderers — Markdown goes through the shipped
MarkdownText, code through the shipped shikiCodeBlock, so both look exactly like the conversation. - Relative images in Markdown —
is read from the host and rendered beside the Markdown text (MarkdownTextitself only rendershttp(s)images). - Two-column navigation — a parent column (siblings, directories only, current one highlighted) beside the current directory, so hopping sideways never needs a trip up.
- Editable path — click the bold name in the header, type a path, press Enter.
~is expanded; a file path lands on its content, a directory path on its listing. - Reading-oriented chrome — one merged header row (ancestor path + pinned current name, fish-style
~and shortening only when the row would overflow), a collapse toggle that puts the listing away entirely (remembered across reloads), and loading feedback that never blanks the pane it replaces. - Text is capped at 400 KB per file, images at 4 MB; binary files are announced rather than dumped.
Install
The package is plain ESM with a hand-written browser bundle: no build step, no dependencies.
From a checkout — clone it, then mount it as one row in your profile's patch layer
(~/.dsh/profiles/<profile>/cordis.patch.yml):
git clone https://github.com/liooil/dsh-files-pane ~/.dsh/plugins/dsh-files-pane
- insert:
- id: files-pane
name: '/home/<you>/.dsh/plugins/dsh-files-pane/lib/index.js'
The loader accepts a path-like entry name, and dsh-client-modules walks up from it to this
package's package.json for the dsh.client declaration and the ./client bundle. The bundle is
then served at /plugins/dsh-files-pane/client.js.
As a package — the repo declares a dsh.bundle manifest (cordis.patch.yml), so a published
copy mounts by name instead:
dsh plugin add dsh-files-pane
⚠️ Use one of the two. Mounting the same package from two sources at once fails the client
module scan (resolves from multiple active Loader sources), so remove the local-path row before
installing the package (or vice versa).
Changing the plugin set (including this first mount) takes effect on the next dsh start:
systemctl --user restart dsh # or however your host is supervised
Layout
| Path | Half | Owns |
|---|---|---|
lib/index.js | Host (Node) | Two same-origin POST routes, /dsh-files-pane/list and /dsh-files-pane/read, reading the filesystem through the deployment's own fs service. |
client/client.js | Browser | The View tab, the header action, the file-open interception, and all presentation. |
package.json | — | The dsh.bundle manifest (installable), the dsh.client declaration (platform: web), and the ./client export the module scanner reads. |
cordis.patch.yml | — | The bundle patch that mounts this package by name when it is installed as a package. |
The browser half is a hand-written bundle in the client module-table format
(window.__ModuleLoader__.load({id, factory})) — the same shape the shipped client packages ship —
so it can require("react") and require("@deepseek-ai/dsh-client-ui-primitives") directly.
Two seams worth knowing
Both are reached through Cordis services and both degrade safely, but both are internal enough to deserve a note:
- Opening the tab. The active View is not owned by a service: the session header and body both
read it from the
conversation.sessionslot store (actions.setView). Selecting this tab programmatically therefore goesuiSession.adapter.resolve(sessionId)→slots.resolveStore(entry.store, binding)→store.actions.setView('files'), which is the same cached per-session instance the header reads. If a future release renames either hop, the failure is logged and the plugin falls back to manual tab switching. - Taking over the file opener.
remote.session.openWorkspacePathis replaced on the Remote namespace service (saved descriptor, restored on unload) and answers with the Remote envelope{ok, value}the caller expects. This is what makes a click in the conversation land here.
The two host routes sit outside /api (no gateway involved), so they carry their own guard: POST
only, plus an Origin/Host same-origin check.
Development
There is no build step: edit client/client.js and reload.
-
Browser half only — ask the host to re-hash the bundle in place, then refresh the page:
ctx.get('clientModules').rebuilt('dsh-files-pane')(A tiny Cordis plugin calling this is enough; a page refresh then fetches the new revision.)
-
Host half or
package.json— restartdsh.
The client HMR driver only helps while pnpm run dev:web is rebuilding bundles from a checkout; this
package is served from its own directory, so the two steps above are the whole loop.
License
MIT — see LICENSE.