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 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.
@@ -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: MIT](https://img.shields.io/github/license/cholf5/dsh-plugin-file-actions?style=flat-square)](./LICENSE)
11
+ [![Platform: macOS | Windows | Linux](https://img.shields.io/badge/platform-macOS_%7C_Windows_%7C_Linux-black?style=flat-square)](#-known-limitations)
12
+ [![DeepSeek Harness plugin](https://img.shields.io/badge/DeepSeek_Harness-web_plugin-blueviolet?style=flat-square)](https://github.com/deepseek-ai/deepseek-harness)
13
+ ![Verified: dsh 0.1.7-alpha.2](https://img.shields.io/badge/verified-dsh%200.1.7--alpha.2-blue?style=flat-square)
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
+ [![File-card dropdown menu demo](docs/file-actions.gif)](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
+ [![In-session link context menu demo](docs/link-actions.gif)](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