@onthewayli/dsh-plugin-path-completion
English | 中文
Codex-style @path file completion for the DeepSeek Harness Web composer, shipped as an
out-of-tree plugin: type @ (and keep typing) in the composer and the trigger menu lists matching
files and directories from your session's project directory — arrow keys, Enter, and Escape drive
the pick through the Harness's own input-trigger menu, and a picked path lands in the draft as
@./relative/path (or the quoted @"path with spaces" form), which the Harness's prompt-side
@path expansion turns into file content on send.
The plugin carries both halves in one package:
- Host half (
lib/index.js) registers one HTTP route, /path-completion/search, on the
Harness webserver and answers with a bounded, ranked candidate list. It never sends file content
over the wire — only names, kinds, and query-shaped paths.
- Browser half (
lib/client.js) registers one @ source on the shared input-trigger pipeline
(ctx.inputTriggers), so no UI code, React, or CSS ships here: the pipeline's existing menu
renders the candidates.
Install
# from a registry (published package)
dsh plugin --profile web add -w @onthewayli/dsh-plugin-path-completion
# from a checkout of this repo
dsh plugin --profile web add -w file:/path/to/dsh-plugin-path-completion
# from a tarball
npm pack && dsh plugin --profile web add -w ./onthewayli-dsh-plugin-path-completion-0.1.0.tgz
# from a git host (needs the profile's allowBuilds approval; pin a commit)
dsh plugin --profile web add -w github:OnTheWay111/dsh-plugin-path-completion#<sha>
dsh plugin forwards to pnpm inside ~/.dsh/profiles/web; because this package declares
dsh.bundle, it is appended to the profile's dsh.profile.bundles automatically, and its
cordis.patch.yml inserts its own loader row. Then restart the Web server: the client module
system caches package metadata per name for the process lifetime, so plugin-set changes take
effect on restart.
Uninstall with dsh plugin --profile web remove -w @onthewayli/dsh-plugin-path-completion.
Configuration
Optional row config in ~/.dsh/profiles/web/cordis.patch.yml:
- id: path-completion
config:
# Candidate-search bounds (defaults shown).
maxResults: 50 # candidates one response carries
maxVisited: 20000 # directory entries one query may visit
budgetMs: 400 # wall-clock budget for one query's walk
ignoreDirs: [node_modules, .git, dist, build, ...] # never descended into
# Extra bare `host[:port]` authorities allowed to call the route (LAN use).
trustedHosts: []
Trust model
The route answers only when the request reaches it as the GUI's own origin:
Host must be a loopback authority (127.0.0.1, localhost, [::1]) or an entry of
trustedHosts;
- a request carrying
Origin must carry the same authority as Host;
Sec-Fetch-Site: cross-site is refused.
A browser reached over a LAN address therefore gets no completion until that authority is listed in
trustedHosts. The route is read-only, returns names only, and never exposes file contents.
Compatibility
| Plugin | Harness | Node | What it relies on |
|---|
| 0.1.0 | 0.1.0-rc.5 or newer in the 0.1.x line (source checkout or published dsh) | >= 22.19 | ctx.webServer.register on the host, the inputTriggers client service and its PickOutcome.text arm on the browser, and the Harness's own prompt-side @path expansion to read a picked file on send |
Verified installs: file: directory, prebuilt tarball, and a source-checkout Harness profile, each
booting with the plugin's client entry in the boot manifest and its route answering.
Coupling facts an upgrade can invalidate:
- The Harness ships client and host together and has no wire protocol version, so a Harness upgrade
may require rebuilding this plugin. The halves type the interfaces they use structurally
(
src/index.ts, src/client/index.ts) so a mismatch fails at runtime with a named error instead of
at build time against packages that may not be installed.
@deepseek-ai/cordis is a peer (>=4.0.0 <5.0.0-0): the plugin is applied by the installation's own
Cordis instance. No other official package is imported — collaboration is through Cordis services.
- The browser half throws a named error when
inputTriggers is missing rather than silently doing nothing.
- The menu group title comes from the Harness's
slash.menu dictionary, which an out-of-tree plugin
cannot extend; the group therefore renders under this source's raw name (files).
Known limitations
- Picking a directory inserts
@./dir/ and closes the menu; the next keystroke reopens it with
that directory's children. The Harness re-detects a trigger on typing, not on a programmatic
insert, so an immediately-reopening drill-down needs an upstream hook.
@ also offers the Harness's session references (child sessions) in their own group; a bare
fragment therefore matches files and session names side by side.
- Candidates come from the session's project directory for relative queries, the OS home for
~/…,
and the absolute path for /…. The ignore list keeps large trees responsive, so a name living
only inside e.g. node_modules is not offered unless you drill into it.
Publishing
Three distribution forms work, in increasing order of convenience for users:
| Form | Publish | User install | Build permission needed |
|---|
| npm registry | npm publish (or your private registry) | dsh plugin --profile web add -w @onthewayli/dsh-plugin-path-completion | no (prebuilt lib/ ships in the tarball) |
| tarball | npm pack | dsh plugin --profile web add -w ./onthewayli-dsh-plugin-path-completion-0.1.0.tgz | no |
| git host | push the repo | dsh plugin --profile web add -w github:you/repo | yes — pnpm refuses a git dependency's prepare until the profile's pnpm-workspace.yaml allowlists it (allowBuilds); pin a commit |
files lists lib/, cordis.patch.yml, and this README, and prepublishOnly/prepare rebuild the
bundles — without the explicit files list, lib/ would be dropped from the tarball (it is gitignored)
and an installed copy would fail to load. Verify with npm pack --dry-run before publishing: the listing
must show lib/index.js and lib/client.js.
Publish under your own name: the @deepseek-ai/ scope belongs to the Harness project. Check the name is
free first (npm view <name> version), since plugin packages are already appearing on the registry.
npm now blocks publishing unless the request carries 2FA: either pass the authenticator code
(npm publish --otp=<code> --access public) or, for scripted/CI publishing, use a granular access
token with "Bypass two-factor authentication" enabled (npmjs.com → Access Tokens → Generate New
Token → Granular). A plain classic npm_ token gets a 403 on publish.
Development
# Requires tsdown + typescript; point node_modules at a Harness checkout (or install them):
# ln -s ../deepseek-harness/node_modules node_modules
#
# The symlinked node_modules carries the rolldown native binding for the arch it was installed
# under (e.g. darwin-x64). Build/pack with a Node of the SAME arch and of >= 22.19 (tsdown needs
# ^22.18 || >=24); on this machine that is the x64 build of nvm's 22.23.2:
# nvm use 22.23.2 && node_modules/.bin/tsdown
node_modules/.bin/tsdown # build lib/index.js + lib/client.js
node --experimental-strip-types --test tests/plugin.test.ts # self-contained tests, no dependencies
dsh plugin add installs a copy of this package into the profile (pnpm's file: handling), so a
rebuild is not picked up by an already-installed profile: re-run the add command after building, and
restart the server (client package metadata is cached per name for the process lifetime).
The browser bundle must keep the Harness client-plugin shape: a CommonJS body wrapped in
window.__ModuleLoader__.load({ id, factory }), with platform modules (React, cordis, the shared UI
packages) left external and everything else inlined — an out-of-tree bundle cannot import another
plugin's client module as a value.