# 文件树「打开方式」子菜单设计(2026-08-22) > 状态:已实现(分支 `feat/open-with-menu`)。本文档记录功能设计、关键取舍与实施偏差。 ## 目标 文件树(`FileTree`)右键菜单新增「在应用中打开 >」子菜单: - 内置 **资源管理器(在文件管理器中显示)、VS Code、Cursor、Zed** 四个打开方式;每个子菜单行右侧有**图钉**,点击可把该方式固定为右键菜单的**顶层直达项**(再点取消固定)。 - **远端 SSH**:设置中配置可选 SSH host(`user@host` 或 `~/.ssh/config` 别名)。非空即整个工作区视为远端:VSCode 系条目(VS Code / Cursor / 自定义且勾选"VSCode 系")改用 `://vscode-remote/ssh-remote+/` 打开;本地专用条目(资源管理器 / Zed / 非 VSCode 系自定义)从菜单隐藏。 - **自定义编辑器**:名称 + URL 模板(`{path}` 占位符,如 `cursor://file/{path}`)+ 「是否 VSCode 系」开关;配置入口在**侧边栏设置 Files 卡片齿轮**(与既有 `editorExplorer` 下拉同一弹窗)。 - 打开动作经宿主新路由 `POST /sidebar/api/open.external` 执行(argv 数组 spawn,无 shell 注入),浏览器模式与 DSH Desktop 行为一致;**不经过** `ctx.workspaces.openPath`(该入口已被 `wrapOpenPath` 劫持到侧边栏编辑器,见 `src/client/openpath-intercept.ts`)。 ## 调研结论(reuse gate) - DSH checkout(`~/.dsh/source/current`)无"文件树 open-with"、无 SSH 远程会话、无 `vscode://` 开启器;`host.openPath` 只能"默认应用打开",无法指定编辑器、无 reveal/select 语义 → 需要新增。 - dsh-external hub 无同类成品;`dsh-open-in-vscode` 是工作区行菜单、依赖外部 slot,不适用。 - primitives `Menu` 原生支持 `submenu?: readonly MenuItem[]`(子项点击走 `onSelect(child.id)`),`label: ReactNode` 可内嵌图钉;无现成图钉/品牌图标 → 引入 `react-icons`(`vsc` codicons 图钉 `VscPin`/`VscPinned` 与文件树 `VscFile`/`VscFolder(Opened)`、`si` 品牌剪影 Cursor/Zed、菜单父行 `VscLinkExternal`),VS Code 品牌剪影因 simple-icons 后期版本受微软商标政策下架而内嵌自 simple-icons@11.0.0(CC0,`IconVscode16`);菜单行图标统一 16px 与 DSH 原生菜单一致,子菜单父行的 `>` chevron 由 label 内右对齐元素提供(基元不渲染)。⚠️ react-icons 的 exports 把 `require` 排在 `import` 前,tsdown 共享 conditionNames 会选中不可树摇的 CJS 入口(bundle +6.4MB)——`tsdown.config.ts` 用 `resolve.alias` 钉死 `si`/`vsc` 的 ESM 入口,树摇后仅 +12KB。 ## 数据模型 持久化在 `pluginSettings['editor'].openWith`(宿主 schema 的开放 map,无需改 PrefsSchema): ```json { "openWith": { "sshHost": "", "customEditors": [ { "id": "custom:", "name": "Windsurf", "urlTemplate": "windsurf://file/{path}", "isVscodeFamily": false } ], "pinned": ["vscode", "custom:"] } } ``` - `parseOpenWithConfig`(`src/client/open-with.ts`)容错解析:坏行丢弃、未知 pinned 在解析目标时剪枝;**结构合法但未填完的行(空 name/模板)保留**——设置面板正在编辑的行必须能在弹窗往返中存活,由菜单侧的 `isValidCustomEditor` 过滤。 - 目标 id:内置 `explorer` / `vscode` / `cursor` / `zed`;自定义 `custom:`。 - URL 构建: - 本地:模板原文替换 `{path}`(`vscode://file//home/u/f.ts`——POSIX 绝对路径自带前导 `/`,与 VS Code 官方文档 `vscode://file/c:/…` 的约定一致;反斜杠归一化为 `/`)。 - SSH(VSCode 系):`://vscode-remote/ssh-remote+/`,路径保留前导 `/`(如 `…+dev/home/u/f.ts`,与社区约定一致)。 - 模板无 `scheme://` 或无 `{path}` → 返回 undefined(不打开)。 ## 菜单结构(文件行与目录行一致) ``` 在新 Tab 中打开 ← 既有 在侧边打开 ← 既有 ---------------------------- VS Code (SSH) ← pinned 直达项(按内置顺序 + 自定义追加) Cursor (SSH) ---------------------------- 在应用中打开 > ← 子菜单父项(始终在 pinned 之后) 资源管理器 📌 VS Code 📌(激活) Cursor 📌(激活) Zed 📌 Windsurf 📌 下载 ← 既有(仅文件行) 复制相对地址 / 复制绝对地址 ← 既有 ``` - 图钉是 `label` 内的 ``(Menu 行本身是 `