← 返回 关于

一种用于报告程序状态的终端协议:OSC 7501

2026-10-08 · 原文链接

我为一种新的终端转义序列写了一份规范:程序状态协议 OSC 7501。它让任何程序都能告诉终端自己正在做什么:空闲、工作中、等待用户、已完成或失败,以及原因。

例如,下面展示了 Terraform 如何表示自己被用户输入阻塞,消息为“Apply 3 to add, 1 to change, 0 to destroy?”(经 base64 编码)。终端(或任何运行 Terraform 的其他工具)可以按自己认为合适的方式呈现这项信息:通知、收件箱、状态图标等。

ESC ] 7501 ; state=blocked:kind=permission:app=terraform:msg=QXBwbHkgMyB0byBhZGQsIDEgdG8gY2hhbmdlLCAwIHRvIGRlc3Ryb3k/ ESC \

本文会说明我为何认为这个协议有存在的必要、现有方法为何不够好(尤其面对编程智能体),以及协议如何工作。

这是一个完全通用、终端原生的规范与协议。 它源于我在 Superlogical 和 Ghostty 上的工作,但规范本身不含任何产品专属功能或措辞。它被设计为一份惯用而完备的规范,任何终端开发者都会感到熟悉。


问题

长时间运行的工作在终端中很常见:构建、部署、包升级、数据处理,以及如今越来越多的编程智能体。这些程序会在自行工作、等待用户和完成之间切换。与此同时,用户通常会转去做别的事,并希望在工作结束或需要自己介入时得到通知。

这个问题的某些方面,早在几十年前就已经通过各种方式得到处理。例如,有些终端会监控活动的前台进程,并在它发生变化时通知用户;还有些会等待输出“安静”一段时间(对“安静”的定义各不相同)。规范还列出了现有序列为何不够用。

最终,我认为目前没有一个连贯、与交互方式无关的通用方案,能够传达进度、阻塞、完成和任务树。也无法把既有序列拼凑起来,以可靠地实现这些能力。


聚焦“智能体收件箱”

不关心 AI、LLM 等?请跳过这一节。 这个问题本身很通用,即使不涉及 AI 也同样值得解决。AI 让它变得格外棘手,所以我想专门指出;但如果你完全不关心这些,直接跳过即可。

如今,人们越来越常因各种原因运行许多长时间工作的智能体:后台研究、问题监控、修复 bug、大型功能开发等。每个智能体会工作一段时间,然后停下来请求许可、提出问题,或报告自己已经完成。

由此产生了一类新工具,我姑且称它为 智能体收件箱:在单一视图中汇总所有运行中的智能体,显示哪些正在工作、哪些已完成、哪些正等待你处理。Herdr、cmux 和 Agent Deck 是数百个此类工具中的几个例子。

没有专门的协议时,它们通过两种方式解决智能体状态问题:启发式规则和非终端 API。

启发式规则

第一种方法是读取屏幕或窗口标题,并与已知模式匹配来猜测状态。

Herdr 是个不错的例子,因为它把这件事做得很好,也公开说明了实现方式。它的检测清单是 TOML 规则,用来把智能体归类为 idle、working 或 blocked。下面是 Claude Code 的 16 条规则中的第一条:

[[rules]]
id = "osc_title_working"
state = "working"
priority = 1100
region = "osc_title"
visible_working = true
# Braille covers <= 2.1.227; half-circles are the 2.1.228 busy spinner.
regex = ['^[\x{2800}-\x{28FF}\x{25D0}-\x{25D3}] ']

如果 Claude Code 的窗口标题以一个盲文转圈字符开头,或从 2.1.228 起以半圆转圈字符开头,它就会被视为“工作中”。该文件的历史显示,仅 Claude Code 一项,三个月内就改了十次。

这不是对 Herdr 的批评。 它的维护者在现有工具条件下已经做到了最好。但这很好地说明了统一协议能带来的益处。

非终端 API

第二种方法是让程序经由特定收件箱的带外 API 来报告自己的状态,例如 Herdr 的 socket API 或 cmux notify。这在一些方面优于启发式规则,因为真正知道自身状态的程序会亲自报告它。但每个程序都必须分别接入每个收件箱;本地 socket 若不额外桥接,也无法穿过 SSH 或容器。pty 则天然适用于所有这些场景。


程序状态协议

OSC 7501是对此问题的终端原生答案。程序通过自己始终拥有的 pty 直接报告状态,使用一种可安全发送到任何地方的格式(行为良好的终端会忽略未知的 OSC)。

序列主体是由 : 分隔的一组 key=value 键值对。唯一必需的键是 state,其值可以是:

state 含义
idle 静止,等待用户的下一条指令。
working 正在运行;可包含 progress 百分比。
done 已完成;结果已经就绪,但用户尚未查看。
blocked 在用户执行某项操作前无法继续。kind 指明类型(permission、question 或 auth),msg 说明原因。
error 失败并停止。

可选键包括 app,即 cargo 或 claude-code 这样的稳定、机器可读的程序名;还包括 msg,即一行经 base64 编码、供人阅读的消息。

同时运行多项工作的程序,可以利用层级化 id报告多条记录。一个部署工具可以在根节点处于 working,同时让 us-east 以 40% 的进度推送镜像,而 eu-west 则处于 blocked、等待获准部署到生产环境。它们可以同时都是真的,由终端决定如何展示。clear 状态会移除记录。

下面是一个完整的集成示例:用 shell 脚本包装 rsync,让它参与该协议。

status() {
  printf '\e]7501;state=%s:msg=%s\e\\' "$1" "$(printf '%s' "$2" | base64 | tr -d '\n')"
}
status working "Syncing photos"
rsync -a ~/Photos backup:/photos && status done "Photos synced" || status error "rsync failed"

只用古老朴素的 POSIX sh 就能轻松脚本化。无需 SDK、无需 socket、无需环境变量、无需 JSON。它不偏向某种特定的 GUI 呈现方式,也不偏向某一类特定工作负载(例如 AI)。这是一个格式良好、通用的基础,可以在其上构建功能,任何人、任何程序都能参与。

完整规范还涵盖了其余内容:记录生命周期、特性检测、terminfo、大小限制和安全性。它很短,全部由我亲手写成。请读一读。


实现它

我写下这份规范,源于自己多年维护终端模拟器的经验。它让应用开发者很容易发出,也让终端模拟器很容易接收和解析。

我已经实现过两次这个协议:一次在 libghostty,另一次则是在 Rex 中并行实现。我还通过插件或 fork,在 Terraform、Claude Code、Codex 和 Homebrew 中实现了概念验证。每种情况下,实现都不超过十几行。

我已与许多流行终端程序和终端模拟器的维护者沟通,他们帮助审阅并塑造了这份规范。如果你还有反馈,我很乐意听取。

如果你已经实现了这份规范,请通过电子邮件(页脚的邮件图标)告诉我,我会把你加入实现该规范的工具列表。谢谢。

我希望我们都能停止通过读取屏幕或进程树来猜测程序在做什么。程序自己早就知道。让我们给它一个说出来的方式吧!