Ponytail 案例解析:用浏览器原生 `<dialog>` 替代弹窗库,零依赖实现删除确认对话框
本文基于 examples/modal-dialog.md 展开,以"为删除操作添加确认弹窗"这一真实需求为切入点,对比引入 Radix UI 弹窗库与使用浏览器原生 <dialog> 元素两种方案的代码量、依赖数与可访问性差异,并结合 Ponytail 的七级决策阶梯(the ladder)拆解其选择"原生平台能力"而非第三方依赖的决策链路。读完本文,你将掌握如何在 AI Agent 辅助编码中识别"平台已解决"的问题,用零依赖的原生方案替代组件库封装。
任务背景:一条"加删除确认弹窗"的需求
原始示例给出的任务提示(Task)是:
"Add a modal dialog for the delete confirmation."
即"为删除操作添加一个确认弹窗"。这是一个极其常见的产品需求:点击删除后,弹出一个模态框确认"此操作不可撤销",提供 Cancel 与 Delete 两个按钮。
问题在于,大多数 AI Agent 面对这类需求时的第一反应是"安装一个现成的弹窗库",因为它"稳妥"。Ponytail 这个开源项目(让 AI Agent 像最懒的资深工程师一样思考,其口号是 "The best code is the code you never wrote")的 examples/ 目录专门收录了同一任务在无技能(Without Ponytail)与启用 Ponytail(With Ponytail)两种模式下的对照输出,modal-dialog.md 就是其中一个案例。该目录在 examples/README.md 中被说明为基准测试运行中的逐字模型输出(Claude Haiku 4.5,temperature 1,来源 benchmarks/output.json),而非人工编写的示例。
基线方案:引入 Radix UI,1 个依赖 + 约 30 行
无 Ponytail 模式下,模型的解法是安装 Radix UI 的 dialog 模块(注释中也给了 react-modal 作为替代选项):
npm install @radix-ui/react-dialog
# or: npm install react-modal
import * as Dialog from "@radix-ui/react-dialog";
import { useState } from "react";
export function DeleteModal({ onConfirm, onCancel }) {
return (
<Dialog.Root>
<Dialog.Trigger asChild>
<button className="btn-danger">Delete</button>
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay className="dialog-overlay" />
<Dialog.Content className="dialog-content">
<Dialog.Title>Confirm deletion</Dialog.Title>
<Dialog.Description>This action cannot be undone.</Dialog.Description>
<div className="dialog-actions">
<Dialog.Close asChild>
<button onClick={onCancel}>Cancel</button>
</Dialog.Close>
<button className="btn-danger" onClick={onConfirm}>Delete</button>
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}
原文用一句话点出了这个方案的结构性浪费:
A dependency, a portal, an overlay, a root, a trigger, a content wrapper, to show a box with two buttons.
翻译过来就是:为了显示一个只有两个按钮的框,你付出了一整个依赖,外加 Portal、Overlay、Root、Trigger、Content 一整套组合式组件。这里值得注意两点:
- 依赖成本是隐性的。
@radix-ui/react-dialog不只是"一个 import"——它意味着 package.json 里多一条记录、node_modules 里多一棵子树、锁文件变更、后续升级与漏洞通告的长期关注成本。 - 组件组合的样板代码占据了大部分行数。
Dialog.Root/Dialog.Portal/Dialog.Overlay/Dialog.Content这些 wrapper 本身不承载业务语义,它们承担的是"打开/关闭/焦点管理/遮罩"这些平台行为——而这正是浏览器已经内置的能力(下一节展开)。
Ponytail 方案:原生 <dialog>,0 个依赖 + 8 行
启用 Ponytail 后,同一个任务的输出如下。HTML 部分:
<!-- ponytail: browser has one, with focus trapping and backdrop built in -->
<dialog id="confirm-delete">
<p>This action cannot be undone.</p>
<button id="cancel">Cancel</button>
<button id="confirm">Delete</button>
</dialog>
JavaScript 部分:
const dialog = document.getElementById("confirm-delete");
document.getElementById("cancel").onclick = () => dialog.close();
document.getElementById("confirm").onclick = () => { onConfirm(); dialog.close(); };
// Open it:
dialog.showModal();
注意代码里的 ponytail: 注释——这是 Ponytail 规则集的一个标志性习惯:对于刻意的简化,用 ponytail: 前缀的注释记录简化理由(详见 AGENTS.md 与 skills/ponytail/SKILL.md 中的 Rules 部分)。此处它声明的依据是"浏览器自带这个能力,且焦点陷阱(focus trapping)与背景遮罩(backdrop)都是内置的"。
原文给出的结论是:1 个依赖 + 30 行 → 0 个依赖 + 8 行。其论据可以拆成四条,每一条都对应 <dialog> 元素(HTML Living Standard 中的模态对话框)的原生行为:
| 能力 | 原生 <dialog> 的行为 |
|---|---|
| 打开为模态 | dialog.showModal() 以模态方式显示(非模态则用 show()) |
| 关闭 | dialog.close()、点击 method 匹配的表单按钮、或按 Escape 键 |
| 焦点陷阱 | 模态显示时浏览器自动将焦点限制在对话框内(focus trapping) |
| 背景遮罩 | ::backdrop 伪元素自动渲染,可用 CSS 直接样式化 |
| 可访问性 | 原文断言 "accessible by default"——对话框语义、aria-modal、焦点管理均由平台处理,不需要库替你拼装 |
原文最后一句是这个案例的核心论点:
All browsers since 2022. The library was solving a problem the platform solved.
即:主流浏览器自 2022 年起均已支持 <dialog> 模态能力;库解决的问题,平台已经解决了。
背后的决策链:七级阶梯中的第四级"原生平台能力"
这个弹窗案例不是孤立的技巧,而是 Ponytail 决策框架的一个标准落地。Ponytail 的规则集(见 AGENTS.md 和 skills/ponytail/SKILL.md)要求 Agent 在写代码前按七级阶梯(the ladder)逐级检查,停在第一个成立的层级:
1. Does this need to be built at all? (YAGNI)
2. Does it already exist in this codebase? Reuse it, don't rewrite
3. Does the standard library already do this? Use it
4. Does a native platform feature cover it? Use it
5. Does an already-installed dependency solve it? Use it
6. Can this be one line? Make it one line
7. Only then: write the minimum code that works
删除确认弹窗这个任务,正是卡在**第 4 级"原生平台能力"**被截胡的:
- 第 1 级(YAGNI):删除确认是真实需求,不能跳过;
- 第 2 级(代码库复用):示例未给出既有 modal 组件;
- 第 3 级(标准库):前端没有"标准库弹窗";
- 第 4 级(原生平台能力):
<dialog>命中——阶梯在此停下,不再考虑装依赖。
仓库中 docs/platform-native.md 把这类映射做成了速查表,其中与本文直接相关的一行是:
| You think you need | What the platform has |
|---|---|
| Modal/dialog library | <dialog> + dialog.showModal() |
同一张 HTML 表里还并列了"日期选择器库 → <input type="date">""手风琴组件 → <details><summary>"等条目,README 主页的 Before/After 例子(<input type="date"> 替代 flatpickr 封装)与本弹窗案例属于同一模式:Agent 倾向"安装封装层",而封装层底下往往就是平台免费提供的 API。docs/platform-native.md 把这种模式概括为一个循环:
Platform team spends years solving the problem.
Package author wraps it.
You install the wrapper.
The wrapper goes unmaintained.
You debug the wrapper.
跳过 wrapper,平台能力随应用免费提供、不随第三方更新而破损。
边界:什么时候弹窗库仍然值得装
"懒"不等于"鲁莽"。Ponytail 的规则集明确划出不可简化的边界(skills/ponytail/SKILL.md 的 "When NOT to be lazy" 一节):信任边界的输入校验、防数据丢失的错误处理、安全措施、可访问性基本项、以及用户明确要求的任何内容,永远不进入简化范围。
回到 <dialog> 案例:它恰好落在这条边界的安全一侧——原文强调原生对话框"焦点陷阱自动、Escape 可关闭、::backdrop 遮罩、默认可访问",即选原生方案没有牺牲可访问性,反而把 Radix 组件要替你拼装的那些无障碍细节交还给了平台。
而 docs/platform-native.md 结尾给出了库何时"挣得自己位置"的判据:当原生方案真正不足时——需要支持更老的浏览器(<dialog> 的模态支持是 2022 年才在主流浏览器普及的,这是原文给出的时间边界)、存在原生不覆盖的边界情况(例如嵌套模态框、受控动画时序、复杂的 open/close 状态机)、或规模化后的人机工学需求——库才安装,"Install it then, not before"。换言之,Ponytail 并不是禁用弹窗库,而是把安装决策从"默认装"推迟到"证明原生不够"之后。
在仓库中验证与复现这个案例
如果你想核对本文引用的原始输出与运行方法,仓库提供了完整的证据链:
- 案例原文:examples/modal-dialog.md(本文两个代码块的逐字来源);
- 目录说明与复现命令:examples/README.md 说明这些对照输出来自基准测试运行,可用
npx promptfoo@latest eval -c benchmarks/promptfooconfig.yaml自行复现(配置见 benchmarks/promptfooconfig.yaml); - 基准方法、指标与"诚实数字"说明:benchmarks/README.md,其中明确指出 LOC 从围栏代码块计数、
correctness.js作为正确性门槛防止"行数好看但跑不通"的输出一路绿灯; - 决策阶梯的两种载体:面向 Agent 的完整技能 skills/ponytail/SKILL.md(含 lite/full/ultra 三档强度)与纯指令版 AGENTS.md,二者阶梯文案保持一致,修改规则文本后需运行
node scripts/check-rule-copies.js与npm test校验各 Agent 副本对齐(见 README.md 的 Development 一节)。
需要说明的是,benchmarks/prompts.json 定义的五个基准任务(邮件校验、防抖、CSV 求和、React 倒计时、限流)不包含弹窗任务;modal-dialog 属于 examples/ 目录收录的额外对照"幸存者"案例,与五个基准任务共享同一套对照呈现方式。
在自己的 Agent 中启用 Ponytail
如果你希望让自己的编码 Agent 也表现出"先查平台、再装依赖"的行为,README 给出的最短路径(以 Claude Code 为例,需分两条提示发送):
/plugin marketplace add DietrichGebert/ponytail
/plugin install ponytail@ponytail
其他宿主(Codex、Copilot CLI、OpenCode、Gemini CLI 等)的安装方式与 /ponytail 命令体系(/ponytail-review 审查过度设计、/ponytail-audit 全仓库审计、/ponytail-debt 收割 ponytail: 注释等)完整列在 README.md 的 Install 与 Commands 章节。默认强度为 full(阶梯强制执行),可用 PONYTAIL_DEFAULT_MODE 环境变量或 ~/.config/ponytail/config.json 调整。
小结
删除确认弹窗这个案例浓缩了 Ponytail 方法论的核心:1 个依赖 + 30 行 → 0 个依赖 + 8 行,省下的不只是行数,还有一条依赖链的长期维护成本;而焦点陷阱、Escape 关闭、::backdrop 遮罩与默认可访问性均由平台承担,没有触碰"绝不简化可访问性"的边界。它示范的判断流程可以脱离任何具体项目复用:遇到"加个 XX 组件"类需求时,先走第 4 级检查——docs/platform-native.md 的速查表就是为此准备的——原生能力够用时,库就该等一等。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00