# dsh-open-in-app

[English](README.en.md) | 中文

面向 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)（dsh）Web UI 的一个插件：在**聊天窗口右上角**提供一个按钮，用**已安装的应用**打开当前会话的**工作区文件夹**。菜单按**终端模拟器**和**流行 IDE/编辑器**白名单整理，分组为“终端应用”和“编辑器与 IDE”。

## 截图

**open-in-app** 控件位于会话头部工具行（聊天窗口右上角），“Session log”按钮旁边。**文件夹按钮**一键打开工作区——使用你上次为该工作区选择的应用；**下拉箭头**打开选择菜单，显示最近使用的应用、系统默认文件夹处理器，以及主机上白名单内的终端和编辑器——每一项都带有应用图标：

![open-in-app 选择菜单：最近使用、系统默认、终端应用与编辑器与 IDE](art/open-in-app.png)

## 功能

- 在会话头部工具行（聊天窗口右上角）新增一个 Codex 风格的**拆分控件**，样式与**“Session log”头部按钮一致的胶囊**（32px 高、1px 边框、18px 圆角、悬停填充），两段之间用细线分隔：
  - **文件夹按钮**一键打开工作区——使用你上次为该工作区选择的应用（按 `cwd` 记录在 `localStorage` 中，按钮上显示其图标），否则回退到**默认编辑器**（优先级最高的已安装编辑器——VS Code → Cursor → Windsurf → Zed → IntelliJ IDEA → …——然后是系统默认）；
  - **下拉箭头**打开菜单，包含置顶的**“最近使用”**条目、一个“系统默认应用”条目（内置 `host.openPath` 行为；macOS 上是 Finder），以及主机上已安装的白名单终端和编辑器，**每一项都带有应用图标**（从应用包中以 32px 读取，以 `data:` URL 传递；无法解析图标的仅显示名称）。“系统默认应用”行显示**默认文件夹处理器的真实图标**（macOS 是 Finder，Windows 是资源管理器，Linux 是 `xdg-mime` 默认文件管理器），无法解析时回退为文件夹图标。
- 从菜单选择应用即可用该应用打开当前工作区文件夹：

| 平台 | 应用发现 | 打开命令 | 图标 |
| --- | --- | --- | --- |
| macOS | `/Applications`、`/System/Applications`（+ `Utilities`）、`~/Applications` | `open -a "<app>" <path>` | 通过 `sips` 将 bundle `.icns` → 32px PNG；无 icns 的 bundle 用 `qlmanage` 回退 |
| Windows | `%ProgramFiles%`、`%ProgramFiles(x86)%`、`%LOCALAPPDATA%\Programs`（顶层 `*.exe`） | `cmd /c start "" <exe> <path>` | 通过 PowerShell `[System.Drawing.Icon]::ExtractAssociatedIcon` 提取内嵌 exe 图标 |
| Linux | `/usr/share/applications`、`/usr/local/share/applications`、`~/.local/share/applications`（`*.desktop`） | `gtk-launch <id> <path>`（回退到 `xdg-open`） | freedesktop `Icon=` 值：绝对路径、hicolor 主题（128→16）、可缩放 svg、pixmaps |

## 白名单

匹配对应用名（macOS 的 `.app` 基名、Windows 的 `.exe` 基名、Linux 的 desktop `Name`）不区分大小写。只有已安装且匹配的应用会出现在菜单中。

**终端应用** — Terminal、iTerm2、Ghostty、Warp、Alacritty、kitty、WezTerm、Hyper、Tabby、Rio、Contour、Foot、Tilix、Terminator、Konsole、GNOME Terminal、xterm、mintty、Windows Terminal、PowerShell、Cmder、ConEmu。

**编辑器与 IDE** — 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。

要增删条目，编辑 `lib/apps.js` 中的 `WHITELIST` 数组——每个条目为 `{ id, category, match, exact? }`，其中 `id` 是规范显示名（也是原生打开命令必须能找到的名称），`match` 别名做子串匹配，`exact` 别名做全名匹配（用于诸如 `code` 这类会误匹配 `CodeRunner` 的短名称）。Windows JetBrains 启动器（`idea64`、`pycharm64` 等）与 `Code.exe` 已通过别名覆盖。

## 架构

- **宿主端**（`lib/index.js`）：一个 Cordis 插件，注册一个 Typert Remote 服务 `openInApp`（通过 Typert Gateway 的 source-mode 回退发现——无需生成 TYPERT manifest）：
  - `openInApp/listApps` → `{ apps, defaultIcon? }`：白名单内已安装应用（含展示图标）以及默认文件夹处理器的图标
  - `openInApp/openWith(path, appId)` → 用指定应用打开文件夹
  - `openInApp/openDefault(path)` → 用系统默认方式打开文件夹
  - `openInApp/openDefaultEditor(path)` → 用默认编辑器打开文件夹（见 `lib/apps.js` 中的 `EDITOR_PRIORITY`）
- **图标**（`lib/icons.js`）：为每个应用以及系统默认文件夹处理器（Finder / Explorer / `xdg-mime` 默认）解析一个 `data:image/*` 图标，按平台尽力而为（见上表），按源路径 + mtime 缓存，使重复打开菜单开销很低；失败时缓存为“无”，绝不报错。在 macOS 上，与应用同名的 icns 优先于文件类型 icns（Zed.app 中的 `Zed.icns` 优先于 `Document.icns`）。枚举的 `source` 路径作为不可枚举属性（`lib/apps.js`）携带，因此 JSON 传输永远不会看到它。
- **客户端**（`lib/client.js`）：一个 `dsh.client` web 模块，通过 `ctx.remote.$mount(...)` 挂载 Remote 端点，并注册 `conversation.session.header.utilities` 条目 `open-in-app`。挂载运行在**声明了 `remote` 的嵌套插件 fiber** 中：api-gateway 将每个命名空间注册为点分隔的 cordis 服务（`remote.openInApp`），一个既挂载又注入自身命名空间的 fiber 会导致加载器死锁。因此消费方通过文档化的非严格 store 访问（`ctx.reflect.get("remote.openInApp", false)`）读取该命名空间。
- **Bundle**（`cordis.patch.yml`）：一行加载器记录激活宿主端；profile 工具将其作为 profile bundle 引入。

## 安装

在包含本包的目录下执行：

```sh
dsh plugin --profile web add ./dsh-open-in-app
```

然后**重启** web 应用（`dsh web`）——加载器与客户端模块图在启动时组合，新增插件没有热重载。重启后，文件夹按钮会出现在会话标题旁，菜单会显示已安装的应用。

## 安全说明

- Remote 端点**不在**特权方法列表中，因此它们与其余 `/api` 面处于同一浏览器信任边界之后（仅限 loopback / 已配置的可信主机）。
- 打开路径会在宿主机上派生原生进程。文件夹路径来自会话自身的 `cwd`，即该 agent 正在操作的目录。
- 图标解析仅读取枚举的应用路径（标准应用根目录）以及 freedesktop 图标主题目录；缓冲区上限为 256 KB，原生转换（`sips`、`qlmanage`、PowerShell）带有可选的 abort 信号。
- 命令失败（`open` 退出码、应用缺失）会以菜单中的错误行呈现，而不是抛异常。

## 开发

- `lib/apps.js` 与 `lib/icons.js` 是纯 Node（无 dsh 导入）——可独立测试（`node -e 'import("./lib/icons.js").then(m => m.listAppsWithIcons()).then(console.log)'`）。
- 客户端 bundle 必须保持自包含：它只依赖平台种子词（`react`、`react/jsx-runtime`、`@deepseek-ai/dsh-client-ui-primitives`）。
- 无需重建：没有构建步骤——`lib/` 原样发布。
