dsh-open-in-app
开发工具 活跃维护

dsh-open-in-app

trewvip-arch/dsh-open-in-app

Web UI 插件,点击即可调用本地已安装应用打开工作区文件夹,配套对应应用图标,原生支持Finder、Terminal、VS Code、Ghostty、Zed等常用开发工具,无需手动切换路径。

0
Stars 标星
0
Forks 分支
0
Watchers 关注
0
Open Issues
JavaScript
主要语言
MIT
开源协议
319 KB
仓库大小
1 个月前
最后推送
一键安装扩展 / 插件指令
dsh plugin --profile web add github:trewvip-arch/dsh-open-in-app
git clone https://github.com/trewvip-arch/dsh-open-in-app.git
git clone git@github.com:trewvip-arch/dsh-open-in-app.git
README.md main

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".

Screenshots

The open-in-app control sits in the session header's utility row (top-right
of the chat window), next to the "Session log" button. The folder button
opens the workspace in the app you last chose for it; the chevron opens the
picker menu showing the recently used app, the system default folder handler,
and the whitelisted terminals and editors installed on the host — each with its
app icon:

The open-in-app picker menu: recently used, system default, terminals and editors

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 cwd in localStorage,
      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.openPath
      behavior; 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 a data: 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, the xdg-mime default file manager on Linux), falling back to a
      folder glyph when unresolvable.
  • 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
    service openInApp (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 icon
    • openInApp/openWith(path, appId) → open the folder with one app
    • openInApp/openDefault(path) → open the folder with the system default
    • openInApp/openDefaultEditor(path) → open the folder with the default
      editor (see EDITOR_PRIORITY in lib/apps.js)
  • Icons (lib/icons.js): resolves one data:image/* icon per app AND for
    the system default folder handler (Finder / Explorer / xdg-mime default),
    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.icns over Document.icns in Zed.app). The enumerated source path
    rides along as a non-enumerable property (lib/apps.js) so JSON transports
    never see it.
  • Client half (lib/client.js): a dsh.client web module that mounts the
    Remote endpoints via ctx.remote.$mount(...) and registers
    conversation.session.header.utilities entry open-in-app. The mount runs
    in a nested plugin fiber that declares only remote: 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 /api surface
    (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 (open exit codes, missing app) surfaces as an error row in
    the menu instead of throwing.

Development

  • lib/apps.js and lib/icons.js are 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.