dsh-plugin-file-actions 0.1.7-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.en-US.md +444 -0
- package/README.md +226 -0
- package/cordis.patch.yml +3 -0
- package/lib/client.js +1222 -0
- package/lib/index.js +753 -0
- package/package.json +40 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 cholf5
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.en-US.md
ADDED
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# dsh-plugin-file-actions
|
|
4
|
+
|
|
5
|
+
**Copy paths, open files in your editor, run them in a terminal — right from
|
|
6
|
+
every presented-file card in the DSH web GUI. macOS / Windows / Linux.**
|
|
7
|
+
|
|
8
|
+
English · [简体中文](README.md)
|
|
9
|
+
|
|
10
|
+
[](./LICENSE)
|
|
11
|
+
[](#-known-limitations)
|
|
12
|
+
[](https://github.com/deepseek-ai/deepseek-harness)
|
|
13
|
+

|
|
14
|
+
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
Verified against `@deepseek-ai/dsh@0.1.7-alpha.2`.
|
|
18
|
+
|
|
19
|
+
## 🎬 Demo
|
|
20
|
+
|
|
21
|
+
**File-card dropdown (the card's only control)** — since dsh 0.1.7 the card's actions come from the official `deliverables.file.actions` seat; the plugin takes that seat over and re-offers the official rows (default app, OS association list, reveal) alongside its own: run in a terminal, open the containing folder in a terminal, copy paths
|
|
22
|
+
|
|
23
|
+
[](docs/file-actions.gif)
|
|
24
|
+
|
|
25
|
+
**In-session link context menu** — right-click a link in a message for a type-aware menu: file / `mailto:` / web / git / svn
|
|
26
|
+
|
|
27
|
+
[](docs/link-actions.gif)
|
|
28
|
+
|
|
29
|
+
## ✨ Features
|
|
30
|
+
|
|
31
|
+
A dual-face DeepSeek Harness plugin that extends the dropdown menu of every
|
|
32
|
+
**presented-file card** (the file list a session delivers at the end of a turn)
|
|
33
|
+
in the DSH web GUI:
|
|
34
|
+
|
|
35
|
+
- 📋 **Copy relative path** / **Copy absolute path** — one click each
|
|
36
|
+
(browser-side, every platform), closing the menu at the bottom. On both the
|
|
37
|
+
card menu and the link context menu.
|
|
38
|
+
- 🚀 **Open the file in a detected editor or IDE** — VS Code, Cursor, Sublime
|
|
39
|
+
Text, the JetBrains family, and more, each shown with its real application
|
|
40
|
+
icon. Detection and launching reuse the official `open-in-app` resolver:
|
|
41
|
+
macOS checks `.app` bundles, Windows checks the registry (`App Paths`,
|
|
42
|
+
Uninstall records, `%ProgramFiles%` scans), Linux checks PATH names and
|
|
43
|
+
desktop entries. **Link context menu only** (the card is covered by the
|
|
44
|
+
official OS association list).
|
|
45
|
+
- 📂 **Open the containing folder in the file manager** — Finder (macOS) /
|
|
46
|
+
File Explorer (Windows) / Files (Linux), with the real application icon,
|
|
47
|
+
through the official `POST /open-in-app/open` route — exactly what the
|
|
48
|
+
session-header dropdown does. **Link context menu only** (on the card, the
|
|
49
|
+
absorbed reveal row below covers it).
|
|
50
|
+
- 📂 **Open with the OS default application / any associated application /
|
|
51
|
+
reveal in the file manager** — the rows absorbed from the official card
|
|
52
|
+
control this plugin's cell replaced: the default app named in the row, the
|
|
53
|
+
per-file association list the serving desktop reports (with the desktop's real
|
|
54
|
+
icons and the default marked), and "show file location".
|
|
55
|
+
- ▶️ **Run this file in a terminal** / **Open its containing folder in a
|
|
56
|
+
terminal** — a submenu per detected terminal: Terminal.app / Ghostty on
|
|
57
|
+
macOS, Windows Terminal / Git Bash on Windows, GNOME Terminal / Konsole /
|
|
58
|
+
Ghostty on Linux. Plugin-only — the official card actions have no terminal
|
|
59
|
+
capability.
|
|
60
|
+
- 🖱️ **The same menu on the file links inside session messages** — right-click
|
|
61
|
+
a file link rendered in a message (file mentions / markdown file links, the
|
|
62
|
+
path rides in the `title`) and the same menu opens at the cursor. The
|
|
63
|
+
workspace directory comes from the viewed session's `cwd`, which relative
|
|
64
|
+
paths resolve against. The official left-click preview behavior is
|
|
65
|
+
untouched. On touch devices the same menu opens on long-press (see
|
|
66
|
+
"Mobile (touch)" below).
|
|
67
|
+
- 🔗 **Per-link-type right-click menus** — `mailto:` offers **copy email
|
|
68
|
+
address / compose email**; http(s) links offer **copy link / open in the
|
|
69
|
+
built-in browser / open in browser** (the built-in entry appears only when
|
|
70
|
+
the deployment ships the browser tab, opening through the official
|
|
71
|
+
`sidebarRight` service); git repository URLs (`.git` suffix, `git@host:path`,
|
|
72
|
+
`git://`, `ssh://` — as anchors or inline code) offer **copy link / clone
|
|
73
|
+
to…**; svn URLs (the `svn://` family, inline code) offer **copy link / check
|
|
74
|
+
out to…**. Clone/checkout first opens the official directory picker for the
|
|
75
|
+
parent directory, then the host runs `git clone` / `svn checkout` with argv
|
|
76
|
+
straight to the executable — no shell, and the URL is strictly validated
|
|
77
|
+
first (leading dashes, whitespace, and overlong strings are refused, closing
|
|
78
|
+
the option-injection door). The target directory is the URL's last segment.
|
|
79
|
+
|
|
80
|
+
> [!NOTE]
|
|
81
|
+
> Since dsh 0.1.7, "open in an editor" and "show in the file manager" are owned
|
|
82
|
+
> by the official `open-in-app` control — the official side lists every
|
|
83
|
+
> application the operating system associates with that exact file (macOS
|
|
84
|
+
> queries NSWorkspace, Windows the registry, Linux desktop entries), with the
|
|
85
|
+
> default app marked and real icons, which is more accurate than the plugin's
|
|
86
|
+
> fixed catalog. The plugin's card cell therefore **takes over that seat** (same
|
|
87
|
+
> id, lower priority) and **absorbs** those rows into its single dropdown rather
|
|
88
|
+
> than duplicating them beside a second button: the default-app open, the
|
|
89
|
+
> per-file OS association list, and reveal are all re-offered there, dispatched
|
|
90
|
+
> through the seat's own authorized route. The plugin's own contributions — run
|
|
91
|
+
> in a terminal, open the containing folder in a terminal, copy paths — follow in
|
|
92
|
+
> the same menu, and its curated editor/file-manager catalog survives on the
|
|
93
|
+
> **message-link context menu**, where no official alternative exists.
|
|
94
|
+
|
|
95
|
+
## 🧩 How the application list is decided
|
|
96
|
+
|
|
97
|
+
The plugin aligns with the official `open-in-app` mechanism — the **official
|
|
98
|
+
resolver probed against the local machine**, no configuration:
|
|
99
|
+
|
|
100
|
+
- The host half loads the resolver library straight out of the official
|
|
101
|
+
`@deepseek-ai/dsh-host-open-in-app` package (exact-pinned), resolves every
|
|
102
|
+
application through the exact locator chains the official routes use, and
|
|
103
|
+
launches the resolved executable with the file path appended (`open -a
|
|
104
|
+
<bundle> <file>` on macOS, a direct spawn of the resolved exe on
|
|
105
|
+
Windows/Linux). When the official catalog gains an app or revises a locator
|
|
106
|
+
spelling, the plugin follows with a release upgrade.
|
|
107
|
+
- Editors and terminals take a **double intersection**: the browser half
|
|
108
|
+
intersects the official probe result (`GET /open-in-app/apps`) with the
|
|
109
|
+
**local resolution result** the plugin's own info route reports
|
|
110
|
+
(`available`): an app appears only when the official probe verified it, the
|
|
111
|
+
plugin's resolver resolved it, and it is whitelisted — so even a version
|
|
112
|
+
skew between the plugin's resolver copy and the host dsh's can never
|
|
113
|
+
reproduce "the menu shows it, the click 400s". Newly installed apps appear
|
|
114
|
+
after the next `dsh web` restart; uninstalled apps disappear immediately.
|
|
115
|
+
- The file-manager entry **follows the official probe alone** (macOS `finder`
|
|
116
|
+
/ Windows `explorer` / Linux `filemanager` — first in the official
|
|
117
|
+
catalog's menu order): its launch is the official `POST /open-in-app/open`
|
|
118
|
+
with the file's directory — the exact call the session-header split button
|
|
119
|
+
makes — so the official route itself is the complete "menu shows it, the
|
|
120
|
+
click works" guarantee; no plugin-side intersection applies.
|
|
121
|
+
- Icons come from the official icon route (`GET /open-in-app/icon/<id>`) — the
|
|
122
|
+
same real application icons as the session header (extracted from the
|
|
123
|
+
executable on Windows); a generic glyph stands in when missing.
|
|
124
|
+
|
|
125
|
+
Terminal entries carry a hover submenu: **Run this file in a terminal** (the
|
|
126
|
+
command comes from the extension map below; unmapped extensions grey the item
|
|
127
|
+
out) and **Open its containing folder in a terminal** (forwarded to the
|
|
128
|
+
official `POST /open-in-app/open` route with the parent directory).
|
|
129
|
+
|
|
130
|
+
### 📱 Mobile (touch)
|
|
131
|
+
|
|
132
|
+
On phones and tablets (`pointer: coarse`) two pieces adapt:
|
|
133
|
+
|
|
134
|
+
- **Long-press opens the link menu** — touch has no right-click: iOS never
|
|
135
|
+
fires `contextmenu` for a hold (its native behavior is the link-preview
|
|
136
|
+
callout), so over the message flow the plugin opens the same menu on a
|
|
137
|
+
**~0.5 s long-press**, and the release is swallowed so no synthesized click
|
|
138
|
+
follows the link. The iOS preview callout and the selection loupe are
|
|
139
|
+
suppressed for message links and inline code (`-webkit-touch-callout` /
|
|
140
|
+
`user-select`, scoped to the conversation flow, injected under a
|
|
141
|
+
`data-plugin` style tag and torn down with HMR). Android's native
|
|
142
|
+
`contextmenu` long-press path keeps working and is de-duplicated against the
|
|
143
|
+
new press timer.
|
|
144
|
+
- **Terminal submenus flatten** — the official Menu primitive opens submenus
|
|
145
|
+
beside the parent row with no viewport clamp, so on a 390px phone the side
|
|
146
|
+
card lands off-screen (measured at x 362..540), and touch has no hover
|
|
147
|
+
anyway. On touch the terminal actions flatten into the top level: **Run this
|
|
148
|
+
file in {terminal}** / **Open its containing folder in {terminal}**, one row
|
|
149
|
+
per action naming the terminal. Desktop keeps the hover submenu.
|
|
150
|
+
|
|
151
|
+
## 📦 Install
|
|
152
|
+
|
|
153
|
+
### Prerequisites
|
|
154
|
+
|
|
155
|
+
- **dsh** reachable — `dsh --version`, or prefix every command below with
|
|
156
|
+
`npx @deepseek-ai/dsh`
|
|
157
|
+
- **pnpm** on PATH (the dsh plugin manager calls it): `npm install -g pnpm`
|
|
158
|
+
|
|
159
|
+
### 1. Add the plugin
|
|
160
|
+
|
|
161
|
+
```sh
|
|
162
|
+
# local directory (link: — source edits apply directly)
|
|
163
|
+
# ⚠️ run npm install inside the checkout first for link: installs:
|
|
164
|
+
# plugin dependencies resolve from the checkout's own node_modules
|
|
165
|
+
npm install # in the checkout (skip for npm/git installs — pnpm brings the deps)
|
|
166
|
+
npx @deepseek-ai/dsh plugin --profile web add link:/absolute/path/to/dsh-plugin-file-actions -w
|
|
167
|
+
|
|
168
|
+
# from GitHub
|
|
169
|
+
npx @deepseek-ai/dsh plugin --profile web add git+https://github.com/cholf5/dsh-plugin-file-actions.git -w
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### 2. Restart and refresh
|
|
173
|
+
|
|
174
|
+
Restart `dsh web`, then refresh the browser page (hard refresh after updates).
|
|
175
|
+
|
|
176
|
+
### 3. Verify (optional, but recommended)
|
|
177
|
+
|
|
178
|
+
Verify the route with a cookie — an unauthenticated 401 happens for every
|
|
179
|
+
`/api` path and proves nothing about registration:
|
|
180
|
+
|
|
181
|
+
```sh
|
|
182
|
+
curl -s -c /tmp/dsh-cookies.txt "http://127.0.0.1:3080/?token=<token-from-launch-url>" -o /dev/null # mint session cookie (303)
|
|
183
|
+
curl -s -b /tmp/dsh-cookies.txt http://127.0.0.1:3080/api/file-actions/info # JSON body = registered; 404 "not found" = not
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
<details>
|
|
187
|
+
<summary>No pnpm, and don't want it? Manual fallback</summary>
|
|
188
|
+
|
|
189
|
+
```sh
|
|
190
|
+
git clone https://github.com/cholf5/dsh-plugin-file-actions.git ~/.dsh/profiles/web/node_modules/dsh-plugin-file-actions
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Then edit `~/.dsh/profiles/web/cordis.patch.yml` so the top-level list contains
|
|
194
|
+
(this is the file's final state — do not blindly append after a `[]` line):
|
|
195
|
+
|
|
196
|
+
```yaml
|
|
197
|
+
- insert:
|
|
198
|
+
- id: file-actions
|
|
199
|
+
name: dsh-plugin-file-actions
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
The running dsh hot-loads this row (patch file watch); refresh the browser
|
|
203
|
+
afterwards.
|
|
204
|
+
|
|
205
|
+
</details>
|
|
206
|
+
|
|
207
|
+
<details>
|
|
208
|
+
<summary>Update / remove</summary>
|
|
209
|
+
|
|
210
|
+
```sh
|
|
211
|
+
npx @deepseek-ai/dsh plugin --profile web update dsh-plugin-file-actions -w # or remove
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Restart `dsh web` afterwards.
|
|
215
|
+
|
|
216
|
+
</details>
|
|
217
|
+
|
|
218
|
+
### 🩺 Troubleshooting
|
|
219
|
+
|
|
220
|
+
| Symptom | Cause & fix |
|
|
221
|
+
|---|---|
|
|
222
|
+
| `dsh: command not found` | npx-only install — prefix `npx @deepseek-ai/dsh` |
|
|
223
|
+
| `pnpm was not found` (exit 127) | `npm install -g pnpm`, or use the manual fallback |
|
|
224
|
+
| `ERR_PNPM_ADDING_TO_ROOT` | the `-w` flag was dropped |
|
|
225
|
+
| Installed but the UI is unchanged | restart `dsh web` (bundle layers don't hot-reload), then refresh the page |
|
|
226
|
+
| The extended menu never appears on cards | the card menu takes over the official `deliverables.file.actions` seat (id `open-in-app`, `priority: -10`); if an upgrade renamed the seat or the shipped cell id, or the card DOM moved the title, the plugin renders nothing (degrade-invisible). Check the DevTools console first, then the known limitations for updating the seat registration |
|
|
227
|
+
| Two dropdowns on a card | the plugin's `priority: -10` no longer shadows the official cell — a dsh upgrade changed how the ledger picks a cell for an id. Re-verify the `entriesOfSlot` first-live-entry-per-id rule against the installed `dsh-client-ui-slots` |
|
|
228
|
+
| The card menu shows "Could not load applications" | the association read against the owner-provided `actionUrl` (GET) failed or returned an unexpected shape; the default-open and reveal rows keep working. Check the Network tab for that request, then the known limitations |
|
|
229
|
+
| `dsh web` boot log shows a `file-actions:` error, or `Cannot find package '@deepseek-ai/dsh-host-open-in-app'` | the official dependency is missing or unresolvable — for `link:` installs run `npm install` inside the checkout; for npm/git installs reinstall with `dsh plugin --profile web update dsh-plugin-file-actions -w` |
|
|
230
|
+
| An editor/terminal is missing from the menu | the app was not verified by BOTH the official probe and the plugin's own resolution (does it appear in the official probe result?) — both intersections must pass. The file manager follows the official probe alone: if the official probe lists it, the link menu will too |
|
|
231
|
+
|
|
232
|
+
## ⚙️ Configuration
|
|
233
|
+
|
|
234
|
+
The host row accepts:
|
|
235
|
+
|
|
236
|
+
```yaml
|
|
237
|
+
- insert:
|
|
238
|
+
- id: file-actions
|
|
239
|
+
name: dsh-plugin-file-actions
|
|
240
|
+
config:
|
|
241
|
+
runCommands: # extension (no dot) → command run ahead of the quoted file path
|
|
242
|
+
py: python3 # defaults are platform-aware: Windows defaults to python / cmd /c / powershell -File, etc.
|
|
243
|
+
sh: bash
|
|
244
|
+
js: node
|
|
245
|
+
ts: tsx
|
|
246
|
+
allowExecutableBit: true # also offer "run" for unmapped extensions that carry an execute bit
|
|
247
|
+
launchTimeoutMs: 10000 # deadline for bounded host commands, and the detached-launch watch window
|
|
248
|
+
cloneTimeoutMs: 120000 # deadline for one git clone / svn checkout (network-bound, ceiling far above the launch watch)
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
> [!WARNING]
|
|
252
|
+
> Override in the profile's own `cordis.patch.yml` — a patch row replaces the
|
|
253
|
+
> target row's whole `config` (no deep merge), so restate every key you need.
|
|
254
|
+
|
|
255
|
+
## 🔍 How it works
|
|
256
|
+
|
|
257
|
+
| Layer | File | Runs in |
|
|
258
|
+
| --- | --- | --- |
|
|
259
|
+
| Host | `lib/index.js` | Node — the Cordis Loader |
|
|
260
|
+
| Client | `lib/client.js` | Browser — the dsh client module system |
|
|
261
|
+
|
|
262
|
+
### Host — `lib/index.js`
|
|
263
|
+
|
|
264
|
+
The `file-actions` Cordis row registers four exact routes on the shared
|
|
265
|
+
authenticated `/api` channel:
|
|
266
|
+
|
|
267
|
+
| Route | Behavior |
|
|
268
|
+
| --- | --- |
|
|
269
|
+
| `GET /api/file-actions/info` | registration probe — a JSON body proves the plugin is loaded |
|
|
270
|
+
| `POST /api/file-actions/launch` | resolve the app through the official resolver → `launchResolved` with the file path appended (one re-resolution on a missing executable, official semantics) |
|
|
271
|
+
| `POST /api/file-actions/run` | build the terminal command per the table below and launch detached |
|
|
272
|
+
| `POST /api/file-actions/clone` | validate the repository URL (VCS shape + option-injection screening) → argv `git clone` / `svn checkout` into the derived subdirectory |
|
|
273
|
+
|
|
274
|
+
Terminal adapters (all spawned detached through the official launcher with a
|
|
275
|
+
credential-scrubbed environment; the terminal outlives dsh):
|
|
276
|
+
|
|
277
|
+
| Terminal | Platform | Mechanism |
|
|
278
|
+
| --- | --- | --- |
|
|
279
|
+
| Terminal.app | macOS | AppleScript `do script "cd <dir> && <command>"` |
|
|
280
|
+
| Ghostty | macOS / Linux | macOS `open -na Ghostty --args -e`; Linux `ghostty --working-directory=<dir> -e bash -c` |
|
|
281
|
+
| Windows Terminal | Windows | `wt -d <dir> cmd /k` with the command line passed through the `%FILE_ACTIONS_RUN_CMD%` environment variable — the token holds no whitespace, so wt's command-line reconstruction cannot mangle it, and cmd expands it at execution time |
|
|
282
|
+
| Git Bash | Windows | `<Git>/usr/bin/mintty.exe -e <Git>/usr/bin/bash.exe -l -c "cd <dir> && <command>; exec '<Git>/usr/bin/bash.exe' -l -i"` (the shells always run by absolute path — a bare `exec bash` resolves through the Windows PATH to WSL's `system32\bash.exe`; `CHERE_INVOKING=1` keeps the login shell from cd-ing home) |
|
|
283
|
+
| GNOME Terminal / Konsole | Linux | `--working-directory` / `--workdir` + `bash -c "<command>; exec bash -i"` |
|
|
284
|
+
|
|
285
|
+
POSIX terminals keep an interactive shell after the command ends (matching
|
|
286
|
+
Terminal.app's behavior); every route asks the composition's `connection`
|
|
287
|
+
service for a rejection first — the same trust fence as the official
|
|
288
|
+
open-in-app host.
|
|
289
|
+
|
|
290
|
+
### Client — `lib/client.js`
|
|
291
|
+
|
|
292
|
+
The card menu mounts through the official slot system, **replacing the shipped
|
|
293
|
+
control**: `ctx.slots.inject('deliverables.file.actions', …)` registers the
|
|
294
|
+
plugin's menu cell under the official cell's **own id `open-in-app`** at
|
|
295
|
+
**`priority: -10`**. The slot ledger sorts a list slot by `(priority, order)`
|
|
296
|
+
and keeps the first live entry per id, and `register()` only rejects a same-id
|
|
297
|
+
collision at the *same* priority — naming the shadowing remedy itself
|
|
298
|
+
("register at a different priority to shadow it (lowest renders)"). The
|
|
299
|
+
official `FileRouteAction` therefore stops rendering and the card carries
|
|
300
|
+
exactly **one** dropdown. Because this cell took that seat over, it also
|
|
301
|
+
**absorbs what the official control was good at** rather than leaving it to a
|
|
302
|
+
second button: the OS default application (`onAction('open')`), the per-file OS
|
|
303
|
+
association list with the desktop's own icons (`onAction('open', appId)`), and
|
|
304
|
+
reveal (`onAction('reveal')`) — all dispatched through the seat's `onAction`,
|
|
305
|
+
i.e. through the owner's own authorized `actionUrl`, never a plugin route. The
|
|
306
|
+
list comes from **GET `actionUrl`** (the same call the official cell made) and
|
|
307
|
+
its icons are embedded PNG/SVG data URLs, validated against the official
|
|
308
|
+
`NativeFileApplication` contract before they reach an `<img src>`; a failed read
|
|
309
|
+
keeps the default-open row alive and shows the official apps-unavailable row,
|
|
310
|
+
and a failure code returned by `onAction` lands in this plugin's own error row
|
|
311
|
+
instead of the official toast. The plugin's own terminal rows follow (its
|
|
312
|
+
unique value), and the browser-side copy entries close the menu. `available:
|
|
313
|
+
false` (no desktop on the serving Host — the state in which the official
|
|
314
|
+
control rendered nothing) drops the absorbed rows; `pending` and an in-flight
|
|
315
|
+
association read gray them out in place. The file path no longer rides a React
|
|
316
|
+
fiber: the cell renders inside the card, and after mount it reads the preview
|
|
317
|
+
button's `title` (the workspace-resolved path) through its own host element via
|
|
318
|
+
`closest('[data-presented-file]')`; `cwd` comes from the seat's standard props
|
|
319
|
+
(`sessionId` + `useSessions`, the owning session). The menu keeps `side:'top'`
|
|
320
|
+
(grows upward to dodge the viewport clamp); when the plugin info lands after the
|
|
321
|
+
cell mounted, the shared-state subscription fills the terminal rows in place. If
|
|
322
|
+
the official DOM drifts (no readable path), the cell renders nothing instead of
|
|
323
|
+
a dead button. The review tab's separate `deliverables.review.file.actions` seat
|
|
324
|
+
is deliberately left to the official control.
|
|
325
|
+
|
|
326
|
+
The right-click menu is pure event delegation: a document-level `contextmenu`
|
|
327
|
+
listener matches the file-link buttons the official markdown renders (file
|
|
328
|
+
mentions and markdown file links share one hashed class and carry the path in
|
|
329
|
+
their `title`; the input area's reference chips share the class but mark
|
|
330
|
+
themselves with `data-ref-chip` and are excluded). On a match it prevents the
|
|
331
|
+
native menu and opens the plugin menu at the cursor through the Menu
|
|
332
|
+
primitive's `getAnchorRect` (portal mode). On touch devices a long-press
|
|
333
|
+
detector (`touchstart`/`touchmove`/`touchend`) feeds the same open path — see
|
|
334
|
+
"Mobile (touch)" above. The viewed session's workspace
|
|
335
|
+
directory is published by an empty cell occupying the official
|
|
336
|
+
`conversation.session.header.utilities` slot — the same seat and the same
|
|
337
|
+
standard props (`sessionId` + `useSessions`) the official open-in-app button
|
|
338
|
+
consumes. The URL menu's optional capabilities (built-in browser tab,
|
|
339
|
+
directory picker) are read the way official client plugins do it: declared in
|
|
340
|
+
`exports.inject` as `remote` / `remote.directoryPicker` first — reading an
|
|
341
|
+
undeclared service trips cordis's `cannot get property ... without inject`
|
|
342
|
+
guard and crashes the whole menu render.
|
|
343
|
+
|
|
344
|
+
> [!IMPORTANT]
|
|
345
|
+
> If the card DOM drifts (the preview button's `title` is unreadable), the
|
|
346
|
+
> plugin cell renders nothing rather than a dead trigger — the same
|
|
347
|
+
> degrade-invisible principle the old fiber probe followed.
|
|
348
|
+
|
|
349
|
+
## 🚧 Known limitations
|
|
350
|
+
|
|
351
|
+
- **The application catalog is fixed**, aligned with the official open-in-app
|
|
352
|
+
philosophy: deployers cannot add their own editors from cordis.yml; extending
|
|
353
|
+
the table means extending the host's `EDITOR_IDS`/`TERMINALS` (or the
|
|
354
|
+
client's `FILE_MANAGER_IDS`) and the client dictionaries. What appears is
|
|
355
|
+
decided entirely by the official probe (the official catalog declares no
|
|
356
|
+
win32 locators for Zed, so Zed never shows on Windows, for example).
|
|
357
|
+
- **Run commands are recognized by extension.** Unmapped extensions get the
|
|
358
|
+
"run" offer based on executability: POSIX consults the execute bit (which the
|
|
359
|
+
client cannot see), Windows derives it from the extension
|
|
360
|
+
(`.exe`/`.bat`/`.cmd`/`.com` — chmod has no effect there; `.bat`/`.cmd` map
|
|
361
|
+
to `cmd /c` by default). Extension-less files get no "run" offer on Windows.
|
|
362
|
+
Configure `runCommands` when needed.
|
|
363
|
+
- **Windows Terminal run commands go through cmd.** The command string is
|
|
364
|
+
executed by `cmd /k`, so cmd metacharacters in configured values are
|
|
365
|
+
expanded; run `.sh` scripts in the Git Bash terminal instead (its command
|
|
366
|
+
executes in an MSYS bash context). Git Bash "run" relies on the mintty that
|
|
367
|
+
ships with a full Git for Windows install.
|
|
368
|
+
- **The official dependency is exact-pinned.** The host locates
|
|
369
|
+
`@deepseek-ai/dsh-host-open-in-app`'s `lib/types/resolver.js` through its
|
|
370
|
+
package manifest (shipped in the published tarball, with multiple layouts
|
|
371
|
+
tried per version); the dependency is pinned to `0.1.6-alpha.2` and does not
|
|
372
|
+
drift on `dsh plugin update`. The host dsh carries its own resolver copy —
|
|
373
|
+
any skew between the two is bridged by the client's double intersection
|
|
374
|
+
(official probe ∩ plugin resolution). If a future version changes the
|
|
375
|
+
layout, the plugin fails loudly at activation with a `file-actions:` error
|
|
376
|
+
instead of silently degrading.
|
|
377
|
+
- **The card menu depends on the official seat and the card DOM shape.** The
|
|
378
|
+
menu takes over the official `deliverables.file.actions` seat under the shipped
|
|
379
|
+
cell's own id `open-in-app` at `priority: -10`, and reads the path from the
|
|
380
|
+
card preview button's `title`; a dsh upgrade that renames the seat, changes
|
|
381
|
+
the shipped cell id, or moves the title elsewhere makes the card menu degrade
|
|
382
|
+
to nothing — update the seat registration and `cardPathOf`. The absorbed
|
|
383
|
+
association list is read from the owner-provided `actionUrl`, so if a dsh
|
|
384
|
+
upgrade changes that route's payload shape the rows degrade to the
|
|
385
|
+
apps-unavailable row (the plugin validates every entry instead of trusting
|
|
386
|
+
it). The card-side editor/file-manager sections are covered by the official OS
|
|
387
|
+
association list — if the official query misses an editor installed without a
|
|
388
|
+
file association, the plugin's curated catalog on the link context menu
|
|
389
|
+
remains the fallback.
|
|
390
|
+
- **The right-click menu depends on the official file-link DOM shape.** The
|
|
391
|
+
match condition is "the `fileMention` hashed class + the path in `title`" on
|
|
392
|
+
a button; if a dsh upgrade changes the markdown rendering (class renamed,
|
|
393
|
+
path moved elsewhere), the context menu silently stops appearing (plain
|
|
394
|
+
right-clicks keep working); updating `FILE_LINK_SELECTOR` restores it. File links
|
|
395
|
+
rendered outside the viewed session's context resolve their paths against
|
|
396
|
+
the currently viewed session's `cwd`.
|
|
397
|
+
- **URL menus only recognize what the official renderer links.** The official
|
|
398
|
+
sanitizer keeps only http/https/mailto hrefs, so `svn://` and `git@` URLs
|
|
399
|
+
are recognized in their inline-code form (the whole code text being the
|
|
400
|
+
repository URL); URLs bare in plain text have no reliable boundary and are
|
|
401
|
+
not menu targets. svn-over-http(s) is indistinguishable from a web page and
|
|
402
|
+
gets the http menu.
|
|
403
|
+
- **Cloning writes to the host filesystem — the same trust tier as "run this
|
|
404
|
+
file in a terminal".** The URL comes from chat text; the host spawns argv
|
|
405
|
+
straight to the executable and first refuses anything that could parse as
|
|
406
|
+
an option (leading dash), holds whitespace, or is overlong. Private repos
|
|
407
|
+
fail fast instead of hanging the bounded command (`GIT_TERMINAL_PROMPT=0`);
|
|
408
|
+
cached credential helpers and agent keys keep working.
|
|
409
|
+
|
|
410
|
+
## 🛠️ Development
|
|
411
|
+
|
|
412
|
+
```sh
|
|
413
|
+
npm install
|
|
414
|
+
node --test
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
`node --test` discovers and runs all three test files under `test/`. The tests
|
|
418
|
+
inject seams (resolver / launcher / runCommand / stat / platform) to cover the
|
|
419
|
+
win32, linux, and darwin adapters deterministically on any development machine —
|
|
420
|
+
including the execute-bit fallback that reads a POSIX mode (the stat seam fakes
|
|
421
|
+
the mode, no chmod involved) — plus one integration test that loads the real
|
|
422
|
+
official resolver library. `client-sweep.test.mjs` additionally drives the real
|
|
423
|
+
`apply()` and the card menu cell against a minimal fake DOM plus a tiny hook
|
|
424
|
+
runtime to regression-sweep the client half — covering the structural bugs a
|
|
425
|
+
stubbed `require` cannot catch, such as the seat registration having to shadow
|
|
426
|
+
the official `open-in-app` cell (same id, lower priority) instead of stacking a
|
|
427
|
+
second control beside it, the card menu having to carry the absorbed official
|
|
428
|
+
rows (default application / association list / reveal) ahead of the terminal and
|
|
429
|
+
copy sections, every absorbed row having to dispatch through the seat's
|
|
430
|
+
`onAction` and map its failure code to the plugin's error row, the association
|
|
431
|
+
list having to come from the owner-provided `actionUrl` and to degrade to the
|
|
432
|
+
apps-unavailable row on a failed or malformed read, the path having to come from
|
|
433
|
+
the card preview button's `title`, and the cell having to render nothing when
|
|
434
|
+
the official DOM drifts.
|
|
435
|
+
|
|
436
|
+
> [!TIP]
|
|
437
|
+
> With a `link:` install, edits to `lib/client.js` hot-swap into the running
|
|
438
|
+
> `dsh web` without a restart; host-half changes need a restart — and the
|
|
439
|
+
> checkout must have `npm install` run first (dependencies resolve from the
|
|
440
|
+
> checkout's `node_modules`).
|
|
441
|
+
|
|
442
|
+
## 📄 License
|
|
443
|
+
|
|
444
|
+
[MIT](./LICENSE) © cholf5
|