opencode 怎么住进编辑器:扩展只是启动器,Agent 还在终端
本文基于 opencode 仓库 commit 7fe9938(2026-08-04)梳理,该项目仍在高频迭代,具体行为以仓库 https://github.com/anomalyco/opencode 最新代码与文档为准。
opencode(这里说的是那个 MIT 许可、跑在终端里的开源编码 Agent 项目,不是泛指”开源代码”)的 VS Code 扩展不是一个”编辑器里的 Agent”,它是一个启动器:把终端劈到你右边、把当前文件的路径和选中行号拼成一条 @文件#L行号 塞进提示框,剩下的活全由那个跑在终端里的 opencode 进程干。 你要是抱着”装个插件就能像用编辑器原生 AI 那样在侧边栏对话”的预期去装它,会觉得功能少得离谱;但你要是明白它的定位是”让终端 Agent 少切一次窗口”,这个设计就说得通了。
这篇拆的是 opencode 这个开源编码 Agent 项目的编辑器集成层——它是怎么被拉起来的、主程序又是怎么反过来认出自己跑在哪个编辑器里的。站内已经有 Cursor 与 GitHub Copilot 的横向对比、GitHub Copilot 上手教程 和 Cursor 上手教程 讲编辑器原生 AI 产品怎么用、怎么选,本篇不重复那些,只讲一个终端 Agent 如何”寄居”进编辑器,以及这种寄居方式带来的能力边界。
一、先分清你面对的是哪一层
打开 packages/web/src/content/docs/ide.mdx,第一句话就把姿态摆得很明白:opencode 集成 VS Code、Cursor,或者任何支持终端的 IDE,在终端里跑 opencode 就算开始了。注意后半句——任何支持终端的 IDE。这不是营销话术,而是它的技术底线:集成的最小公倍数是”你有一个终端”。
在这个前提上,仓库里实际存在三条通往编辑器的路,能力完全不一样:
第一条是终端本身。你在编辑器的集成终端里敲 opencode,得到的是完整的终端界面(TUI),跟你在独立终端窗口里跑没有任何区别。
第二条是 VS Code 扩展,源码在 sdks/vscode/src/extension.ts,一共一百多行。它的职责是快捷键、劈屏、以及把当前文件引用送进去。
第三条是 ACP。packages/web/src/content/docs/acp.mdx 里写着,opencode 支持 Agent Client Protocol,配置编辑器去执行 opencode acp 命令,它就会作为一个 ACP 子进程启动,通过 stdio 上的 JSON-RPC 跟编辑器通信。文档里给了 Zed、JetBrains 系列、Avante.nvim、CodeCompanion.nvim 四种配置写法。
三条路里,只有第三条算得上”编辑器里的原生对话体验”,而它恰恰不依赖那个 VS Code 扩展。这一点是理解整个集成层的钥匙。
二、VS Code 扩展到底做了什么
sdks/vscode/package.json 里注册了三条命令,名字很直白:opencode.openTerminal、opencode.openNewTerminal、opencode.addFilepathToTerminal。快捷键绑定也在同一个文件里:前两条分别是 cmd+escape 和 cmd+shift+escape(Windows/Linux 对应 ctrl+escape 和 ctrl+shift+escape),第三条是 cmd+alt+k。sdks/vscode/README.md 把这几条能力概括成快速启动、新建会话、上下文感知、文件引用快捷键四项。
真正有意思的是 extension.ts 里 openTerminal 的实现。它做的第一件事是随机挑一个端口:
const port = Math.floor(Math.random() * (65535 - 16384 + 1)) + 16384
const terminal = vscode.window.createTerminal({
name: TERMINAL_NAME,
location: {
viewColumn: vscode.ViewColumn.Beside,
preserveFocus: false,
},
env: {
_EXTENSION_OPENCODE_PORT: port.toString(),
OPENCODE_CALLER: "vscode",
},
})
terminal.show()
terminal.sendText(`opencode --port ${port}`)
三个细节值得停一下。
一是它没有用什么私有协议启动 Agent,就是老老实实往终端里 sendText 了一行 opencode --port <端口>。你在终端里看到的那行命令就是它敲的。
二是它往终端环境里塞了两个变量:_EXTENSION_OPENCODE_PORT 记住端口号,OPENCODE_CALLER 标记调用方是 vscode。后面你会看到主程序怎么读第二个。
三是启动之后它并不认为进程立刻就绪,而是循环最多十次、每次间隔 200 毫秒去 fetch 本地的 /app 接口探活,连上了才把当前文件引用作为提示词追加进去。追加走的是 HTTP POST 到 /tui/append-prompt,这个路径在主程序侧真实存在,定义在 packages/opencode/src/server/routes/instance/httpapi/groups/tui.ts 的 TuiPaths 里。
至于”文件引用”长什么样,getActiveFile 给出了答案:取当前编辑器文档相对工作区根目录的路径,前面加个 @;如果有选区,再按 1 起始的行号拼上 #L37 或 #L37-42。没有活动编辑器、或者文件不属于任何工作区文件夹,它直接返回空,什么都不做。
所以这个扩展的全部魔法就是:劈个屏、拉起进程、把光标所在的位置翻译成一行文本。它不渲染对话、不接管 diff、不做补全。
三、主程序里那个认编辑器的口子
反方向的识别逻辑在 packages/opencode/src/ide/index.ts。整个文件五十来行,核心是一张表和两个函数:
const SUPPORTED_IDES = [
{ name: "Windsurf" as const, cmd: "windsurf" },
{ name: "Visual Studio Code - Insiders" as const, cmd: "code-insiders" },
{ name: "Visual Studio Code" as const, cmd: "code" },
{ name: "Cursor" as const, cmd: "cursor" },
{ name: "VSCodium" as const, cmd: "codium" },
]
export function ide() {
if (process.env["TERM_PROGRAM"] === "vscode") {
const v = process.env["GIT_ASKPASS"]
for (const ide of SUPPORTED_IDES) {
if (v?.includes(ide.name)) return ide.name
}
}
return "unknown"
}
识别手法是个不折不扣的工程土办法:先看 TERM_PROGRAM 是不是 vscode(VS Code 系的编辑器都会给集成终端设这个值),再去 GIT_ASKPASS 这个变量的路径字符串里找编辑器名字。为什么是 GIT_ASKPASS?因为 VS Code 系编辑器会把自己内置 git 扩展的 askpass 脚本路径注入环境,而那个路径里带着应用程序名。packages/opencode/test/ide/ide.test.ts 里的用例把这件事写得一清二楚,比如断言 GIT_ASKPASS 为 /path/to/Cursor.app/Contents/Resources/app/extensions/git/dist/askpass.sh 时 ide() 返回 Cursor。测试里还专门覆盖了两种返回 unknown 的情况:TERM_PROGRAM 是别的终端时不认,路径里没有已知编辑器名时也不认。
顺序也不是随便排的。Visual Studio Code - Insiders 排在 Visual Studio Code 前面——因为用的是子串包含匹配,Insiders 的路径里也含有 Visual Studio Code,排后面就永远匹配不上了。
第二个函数是 alreadyInstalled(),它只看 OPENCODE_CALLER 是不是 vscode 或 vscode-insiders。这正好接上了上一节扩展注入的那个变量:如果这次 opencode 是被扩展拉起来的,就说明扩展已经装了,不必再问一遍。
安装动作则是 install():从表里查出对应的命令行工具名,用它执行 --install-extension sst-dev.opencode,退出码非零就抛 InstallFailedError(带上 stderr),标准输出里出现 already installed 就抛 AlreadyInstalledError。扩展市场标识 sst-dev.opencode 与 sdks/vscode/package.json 里的 publisher 和 name 两个字段是对得上的。
还有一个容易忽略的信号:packages/schema/src/ide-event.ts 定义了一个 ide.installed 事件,但 packages/schema/test/event-manifest.test.ts 明确断言 EventManifest.Latest.has("ide.installed") 为 false——它没有被列入对外公开的事件清单。同时在当前这份代码里,整个 ide 模块除了自己的测试文件,没有别的源码文件 import 它。你可以把这理解成一块预留的、边界还没定死的地基,而不是已经稳定的公共 API。
四、四个组成部分对照表
| 组成部分 | 它负责什么 | 对应仓库位置 | 你什么时候会碰到它 |
|---|---|---|---|
| VS Code 扩展 | 注册三条命令与快捷键,劈屏建终端、注入端口与调用方标记、把当前文件与选区拼成 @路径#L行号 追加进提示框 | sdks/vscode/src/extension.ts、sdks/vscode/package.json | 按 cmd+escape 或 cmd+alt+k 时 |
| 编辑器识别与扩展安装 | 从 TERM_PROGRAM 与 GIT_ASKPASS 判断当前是哪个编辑器,并调用 code/cursor 等命令安装扩展 | packages/opencode/src/ide/index.ts | 在集成终端里首次运行 opencode 时 |
| TUI 的 HTTP 控制面 | 提供 /tui/append-prompt 等一组路径,让外部进程往终端界面里塞内容 | packages/opencode/src/server/routes/instance/httpapi/groups/tui.ts | 扩展把文件引用送进来时,或你自己写工具驱动它时 |
| ACP 通道 | 用 opencode acp 以 stdio JSON-RPC 作为编辑器的外部 Agent 运行 | packages/web/src/content/docs/acp.mdx | 在 Zed、JetBrains 系等支持 ACP 的编辑器里用它时 |
表里还漏掉一条反方向的通道:packages/web/src/content/docs/tui.mdx 里的 /editor 和 /export 两条斜杠命令,用的是你 EDITOR 环境变量指定的编辑器。文档特别提醒,图形界面编辑器要带阻塞参数,比如把 EDITOR 设成 code --wait,否则编辑器进程立刻返回,终端那边以为你已经写完了。这是”终端 Agent 反过来调用编辑器”,跟前面三条路的方向正好相反。
五、边界与代价:这个设计放弃了什么
它放弃了编辑器内的富交互。 扩展只往终端里追加文本,对话渲染、diff 审阅、行内确认全部发生在终端界面里。你没法像用编辑器原生 AI 那样在侧栏点一下”接受这块改动”。想要那种体验,得走 ACP,而 ACP 的体验质量取决于宿主编辑器的实现,不在 opencode 这一侧。
它放弃了跨编辑器的统一实现。 SUPPORTED_IDES 那张表只有五项,全是 VS Code 系。JetBrains、Zed、Neovim 一律不在识别范围内,它们要么走终端、要么走 ACP。识别逻辑本身也是脆的:靠环境变量的字符串包含来判断,编辑器哪天改了 askpass 脚本的落盘路径,识别就会退化成 unknown。
扩展与 Agent 之间的通道是本机明文 HTTP。 端口是 16384 到 65535 之间随机挑的,探活和追加提示词都走 http://localhost:<端口>。这条链路的设计前提是”只有你自己在这台机器上”。在多人共用的开发机、或者别人能连进来的容器环境里,这个前提要重新评估。packages/web/src/content/docs/server.mdx 里提到可以用 OPENCODE_SERVER_PASSWORD 给服务加基础认证,但那说的是 opencode serve 和 opencode web 这两个显式启动服务的场景。
它明确不管的事有一串。 扩展不管你的模型选择、不管权限审批、不管上下文预算——这些都是终端进程那边的事。packages/web/src/content/docs/acp.mdx 里也诚实标注了一处缺口:通过 ACP 使用时,/undo 和 /redo 这类内置斜杠命令目前不支持。
最需要你自己盯紧的是权限。 无论从哪条路进来,最终干活的都是同一个 Agent:它会在你机器上跑 shell 命令、直接改磁盘上的文件、把它读到的代码内容发给模型服务商。扩展帮你把启动门槛降到一次按键,同时也把”我到底授权了什么”这件事推得更远了——你按 cmd+escape 时不会看到任何权限确认。packages/web/src/content/docs/permissions.mdx 里的机制是 permission 配置,每条规则解析成 allow、ask、deny 三者之一,另有一个 --auto 模式会自动批准所有不是显式 deny 的请求。把 * 设成 allow、或者顺手挂上自动批准,等于让一个能改代码能跑命令的进程在你的工作区里无人值守地跑——误删误改、把含密钥的文件读进上下文再发出去,都是这么发生的。这类风险的系统性拆解可以看 Agent 权限给太大会出什么事。至于模型服务商侧的数据保留策略,各家规则不同且会调整,以官方最新说明为准。
六、上手与避坑清单
别在外部终端里等扩展自动装。 文档里的自动安装依赖的是 TERM_PROGRAM 和 GIT_ASKPASS,这两个变量只有编辑器的集成终端才会设。你在 iTerm2、Windows Terminal 里跑 opencode,识别结果就是 unknown,自然不会触发安装。要装就在编辑器的集成终端里跑第一次。
先确认命令行工具在 PATH 里。 安装动作是拿 code、cursor、windsurf、codium 这几个命令去执行的,命令不在 PATH 就必然失败。ide.mdx 给的排查动作是打开命令面板搜 “Shell Command: Install ‘code’ command in PATH”(其它编辑器搜对应项)。这也是为什么有人”什么都没做错但就是装不上”——问题不在 opencode,在编辑器自己的命令行工具没装。
用了代理就必须放行本机地址。 扩展要 fetch http://localhost:<端口>,终端界面本身也要跟本地服务通信。packages/web/src/content/docs/network.mdx 明确警告:终端界面通过本地 HTTP 服务通信,必须让代理绕开这个连接,否则会形成路由回环。做法是把 localhost,127.0.0.1 加进 NO_PROXY。企业网环境下这条几乎是必踩,因为代理变量往往是全局设的,你不会意识到它把本机流量也劫了。
@文件#L行号 只在工作区内成立。 getActiveFile 用的是相对工作区根目录的路径,文件不属于任何工作区文件夹时它直接返回空。你从外面拖一个文件进编辑器窗口,按快捷键会毫无反应——不是快捷键坏了,是这个文件压根不在它的坐标系里。
别在同一个工作区无限开新会话。 opencode.openTerminal 会先找名字叫 opencode 的已有终端并聚焦,而 opencode.openNewTerminal 每次都新建一个,各带一个新的随机端口和一个新的进程。开多了不只是费内存:多个 Agent 同时改同一批文件,冲突是必然的。真要并行,先做工作区隔离,思路见 多 Agent 并行时怎么隔离工作区。
改扩展源码时别从仓库根目录打开。 sdks/vscode/README.md 里用加粗字体强调了这一点:用 code sdks/vscode 打开这个子目录,不要从仓库根打开,然后在该目录里 bun install、按 F5 启动调试。调试窗口里改完代码,用命令面板的 Developer: Reload Window 重载即可看到效果。README 只给了”不要从仓库根打开”这句结论,没解释原因;实际按它给的顺序做就行——把 sdks/vscode 当成工作区根目录打开,F5 才能按 VS Code 扩展调试的常规约定把这个目录识别成待调试的扩展。README 还提到调试期间 tsc 和 esbuild 的 watcher 会自动跑起来(在终端面板里能看到),改动会在后台自动重新构建,所以你不需要每次手动打包,只要重载窗口。
收束:先读哪个文件
如果你只有二十分钟,读这三个文件就够形成完整判断:sdks/vscode/src/extension.ts 看扩展这一侧做了什么(一百多行,通读没压力)、packages/opencode/src/ide/index.ts 看主程序怎么认编辑器、packages/opencode/test/ide/ide.test.ts 看识别逻辑的真实输入长什么样——测试文件在这里比文档更有信息量,因为它写出了 GIT_ASKPASS 的实际取值。
装之前给自己过一遍三个问题:这台机器上的代码,允许发给模型服务商吗?我的 permission 配置里,bash 和 edit 分别是什么值?我有没有在不知情的情况下开着自动批准?三个问题答不上来,就先别按那个 cmd+escape。想系统地比较终端 Agent 这一类工具,可以接着看 开源终端 Agent 怎么选。
这个仓库是 MIT 许可证(LICENSE,Copyright 2025 opencode),packages/ 下有 32 个包,英文文档 packages/web/src/content/docs/ 有 36 份 mdx。编辑器集成只占其中很小一块——但它恰好是你每天按下最多次的那一块。
本篇属于一个把开源AI 编程 Agent 项目 opencode逐层拆开讲的系列,整体地图见 opencode 是什么:终端里的开源 AI 编程 Agent 全景地图;沿着这条线往下,还可以看 开源终端 Agent opencode 的 ACP 层拆解:重映射的代价 和 Agent 改坏了代码怎么退回去:opencode 的快照与回滚机制,和你自己 git commit 的边界。