dsh-open-in-app
A DeepSeek Harness (dsh) plugin for the web UI: a button in the top-right corner of the chat window that opens the current session's workspace folder with an installed application. The menu is curated to a whitelist of terminal emulators and popular IDEs/editors, grouped under "Terminals" and "Editors & IDEs".
What it does
- Adds a Codex-style split control to the session header's utility row
(right side of the chat window header), styled as a pill capsule matching
the "Session log" header button (32px, 1px border, 18px radius, hover
fill) with a hairline divider between the two segments:
- the folder button opens the workspace with one click — in the app
you last chose for this workspace (remembered per
cwdinlocalStorage, its icon shown on the button), falling back to the default editor (the most preferred installed editor — VS Code → Cursor → Windsurf → Zed → IntelliJ IDEA → … — then the system default); - the chevron opens the menu with a pinned "Last used" entry for the
workspace, a "System default app" entry (the built-in
host.openPathbehavior; Finder on macOS) plus the whitelisted terminals and editors installed on the host, each with its app icon (read from the app bundle at 32px, delivered as adata:URL; apps whose icon cannot be resolved show their name only). The "System default app" row shows the default folder handler's real icon (Finder on macOS, File Explorer on Windows, thexdg-mimedefault file manager on Linux), falling back to a folder glyph when unresolvable.
- the folder button opens the workspace with one click — in the app
you last chose for this workspace (remembered per
- Picking an app from the menu opens the current workspace folder with it:
| Platform | App discovery | Open command | Icons |
|---|---|---|---|
| macOS | /Applications, /System/Applications (+ Utilities), ~/Applications |
open -a "<app>" <path> |
bundle .icns → 32px PNG via sips; qlmanage fallback for bundles without an icns |
| Windows | %ProgramFiles%, %ProgramFiles(x86)%, %LOCALAPPDATA%\Programs (top-level *.exe) |
cmd /c start "" <exe> <path> |
embedded exe icon via PowerShell [System.Drawing.Icon]::ExtractAssociatedIcon |
| Linux | /usr/share/applications, /usr/local/share/applications, ~/.local/share/applications (*.desktop) |
gtk-launch <id> <path> (falls back to xdg-open) |
freedesktop Icon= value: absolute path, hicolor theme (128→16), scalable svg, pixmaps |
The whitelist
Matching is case-insensitive against the enumerated app name (.app basename
on macOS, .exe basename on Windows, desktop Name on Linux). Only installed
apps that match appear in the menu.
Terminals — Terminal, iTerm2, Ghostty, Warp, Alacritty, kitty, WezTerm, Hyper, Tabby, Rio, Contour, Foot, Tilix, Terminator, Konsole, GNOME Terminal, xterm, mintty, Windows Terminal, PowerShell, Cmder, ConEmu.
IDEs & editors — Visual Studio Code, Cursor, Windsurf, Zed, Xcode, Android Studio, IntelliJ IDEA, PyCharm, WebStorm, GoLand, CLion, PhpStorm, RubyMine, Rider, DataGrip, DataSpell, RustRover, Fleet, Aqua, Visual Studio, Eclipse, NetBeans, Sublime Text, Nova, BBEdit, TextMate, CodeRunner, MacVim, Neovide, Emacs, Lite XL, HBuilderX.
To add or drop entries, edit the WHITELIST array in lib/apps.js — each
entry is { id, category, match, exact? }, where id is the canonical display
name (also what the native open command must find), match aliases are
substring-tested, and exact aliases are whole-name-tested (used for short
names like code that would otherwise match CodeRunner). Windows JetBrains
launchers (idea64, pycharm64, …) and Code.exe are covered by aliases.
Architecture
- Host half (
lib/index.js): a Cordis plugin registering one Typert Remote serviceopenInApp(discovered by the Typert Gateway's source-mode fallback — no generated TYPERT manifest needed):openInApp/listApps→{ apps, defaultIcon? }: whitelisted installed apps (with display icons) plus the default folder handler's iconopenInApp/openWith(path, appId)→ open the folder with one appopenInApp/openDefault(path)→ open the folder with the system defaultopenInApp/openDefaultEditor(path)→ open the folder with the default editor (seeEDITOR_PRIORITYinlib/apps.js)
- Icons (
lib/icons.js): resolves onedata:image/*icon per app AND for the system default folder handler (Finder / Explorer /xdg-mimedefault), best-effort per platform (see the table above), cached per source path + mtime so repeated menu opens are cheap; failures cache as absent, never error. On macOS the icns named after the app wins over file-type icns (Zed.icnsoverDocument.icnsin Zed.app). The enumeratedsourcepath rides along as a non-enumerable property (lib/apps.js) so JSON transports never see it. - Client half (
lib/client.js): adsh.clientweb module that mounts the Remote endpoints viactx.remote.$mount(...)and registersconversation.session.header.utilitiesentryopen-in-app. The mount runs in a nested plugin fiber that declares onlyremote: the api-gateway registers each namespace as a dotted cordis service (remote.openInApp), and a fiber that both mounts and injects its own namespace would deadlock the loader. Consumption therefore reads the namespace through the documented non-strict store access (ctx.reflect.get("remote.openInApp", false)). - Bundle (
cordis.patch.yml): one loader row that activates the host half; the profile tooling picks the package up as a profile bundle.
Install
From the directory containing this package:
dsh plugin --profile web add ./dsh-open-in-app
Then restart the web app (dsh web) — the loader and the client module
graph are composed at boot, there is no live reload for newly added plugins.
After the restart, the folder button appears next to the session title and a
menu shows the installed applications.
Security notes
- The Remote endpoints are not in the privileged-method list, so they sit
behind the same browser-trust fence as the rest of the
/apisurface (loopback / configured trusted hosts only). - Opening a path spawns a native process on the host. The folder path comes
from the session's own
cwd, which the agent already operates on. - Icon resolution only reads inside the enumerated app paths (the standard
application roots) plus the freedesktop icon theme dirs; buffers are capped
at 256 KB and the native conversions (
sips,qlmanage, PowerShell) run with an optional abort signal. - Command failure (
openexit codes, missing app) surfaces as an error row in the menu instead of throwing.
Development
lib/apps.jsandlib/icons.jsare pure Node (no dsh imports) — testable standalone (node -e 'import("./lib/icons.js").then(m => m.listAppsWithIcons()).then(console.log)').- The client bundle must stay self-contained: it only requires the platform
seed words (
react,react/jsx-runtime,@deepseek-ai/dsh-client-ui-primitives). - To rebuild nothing: there is no build step —
lib/is shipped as-is.
No comments yet. Be the first to write one.