pi-clean-mode0.5.1
Collapse a whole agent run into a single duration header, leaving only the final answer
Untested on omp
This is a pi package. No one has reported whether it works on omp yet. If you try it, add your result below.
Reports 0
No reports yet.
Readme
pi-clean-mode
pi-clean-mode 把一轮 agent 运行的完整工作过程折叠成一行耗时头,对话里只留最终答案。按快捷键(默认 f2)可以随时展开回看。
改变了什么
一次提问通常会让 agent 执行很多次工具调用、穿插好几段解说,最后才给出答案。Pi 默认把这些全部展示出来。本扩展把除最终答案之外的内容都视为工作过程并折叠:
[用户消息]
▌ 用时 4m 26s ▶
原因查到了:今天补货用了两天前的旧销量,低估了需求,所以看起来只够约 10 天。
展开后工作行会回来,耗时头的箭头从 ▶ 变成 ▼。
三级折叠
| 层级 | 行 | 行为 |
|---|---|---|
| 运行级 | ▌ 用时 4m 26s ▶ |
收起整轮工作过程,只留最终答案 |
| 动作组级 | │ 探索 · 3 步 ▶ |
把一个 turn 的多条工具调用收成一行组头 |
| 成员行级 | ├─ 运行命令 npm test ▶ |
展开组后一条命令一行,点哪行才铺哪行的原文 |
| 工具行级 | $ find . -name '*.ts' |
Pi 自带的单行输出展开(ctrl+o,或点结果区) |
组边界跟着解说走:带正文的 assistant 消息开新组,连续的纯工具 turn 合并进当前组。所以一串连续工作会读成一行 —— 即使模型每个 turn 只发一条工具调用(这是常态)。
组内只有一条时不折叠,直接显示那条工具行本身,所以单条命令仍然读起来就是它自己。
▌ 用时 11s ▼
我会先核对补货数据。
│ 探索 · 3 步 ▶ ← 动作组收起
▌ 用时 11s ▼
我会先核对补货数据。
│ 探索 · 3 步 ▼ ← 动作组展开:一条命令一行
├─ 读取 src/index.ts ▶
├─ 运行命令 pnpm test ▶
└─ 搜索 "handleMouse" ▶
想看清某一条的原始输出,点它那一行就就地铺开(再点收回单行):
│ 探索 · 3 步 ▼
├─ 读取 src/index.ts ▶
├─ 运行命令 pnpm test ▼
│ 运行命令 pnpm test ← Pi 原本的渲染
│ Tests 42 passed
│ ... (18 more lines, ctrl+o to expand)
└─ 搜索 "handleMouse" ▶
组状态与运行状态互相独立:展开整轮时,各组保持你上次留下的收放状态(每条命令是否铺开了原文也一样保留)。
交互
| 触发方式 | 效果 |
|---|---|
f2 |
收起或展开本轮工作过程 |
shift+f2 |
展开或收起全部动作组 |
| 鼠标点击耗时头 | 同 f2,仅全屏 TUI 模式 |
| 鼠标点击组头 | 单独展开或收起该动作组,仅全屏 TUI 模式 |
| 鼠标点击成员命令行 | 就地展开或收起这一条命令的原文(整行可点),仅全屏 TUI 模式 |
/clean |
同快捷键 |
/clean config |
打开交互式配置面板 |
/config:clean-mode |
打开交互式配置面板 |
/config:clean-mode <key>=on|off |
不开面板,直接改一个布尔配置并保存 |
配置面板
Pi 自带的 /settings 只管内核选项,没有给扩展注册配置项的入口,所以清爽模式自带一个面板:/clean config(或直接 /config:clean-mode)会用列表接管编辑器。
↑/↓移动,enter/space切换,esc关闭activityRows会再开一层1–20的列表:enter选定,esc保持原值- 选中项下方显示说明,右侧显示当前值
- 每改一项立刻写进配置文件;写失败会弹错误提示,不会只改内存不落盘
鼠标需要 pi --tui-mode fullscreen;常规模式下终端自己接管鼠标输入与滚动,Pi 收不到点击。可点击区域是折叠头那行(运行中就是 ▌ 处理中 · Ns 状态行)加上它上面的空行,点正文不会误触。
如果本轮运行中你自己切换过折叠状态,本轮结束后不会自动收起 —— 直到下一次运行开始都尊重你的选择。手动收起只收起工作过程:顶部那条 ▌ 处理中 · Ns 会继续显示,直到运行结束换成 ▌ 用时 …。
折叠后长什么样
三级用不同的视觉,一眼能分出层级,也不会和正文混在一起。用的是左侧竖条 + 字重 + 色档,两级竖条都从第 0 列起画,两级文案因此同列;但两根竖条本身不是一条严格对齐的连续轨道 —— 半格实心块 ▌ 画在格子左半边、居中的 │ 画在格子中间,横向差半格,所以组头上面那行留白,不用竖条硬接上下两端:
▌ 用时 21s · 5 步 ▼
│ 探索 · 3 步 ▼
├─ 运行命令 ls -la ▶
└─ 按类别:目录、视频、Excel、其他。
| 层级 | 视觉 | 含义 |
|---|---|---|
| 运行级折叠头 | 粗竖条 ▌ + 加粗主文字色 + 紧跟在文案右边的箭头 |
这里收了一整轮 |
| 动作组头 | 细竖条 │ + 普通字重 + 弱化色 + 紧跟其后的箭头(它上面那行留白)+ 工具底色 |
这里收了一次或一组工具调用 |
| 组内成员命令 | 树形前缀 ├─ / └─ + 弱化色摘要 + 工具底色 |
这条命令不是 Agent 写的字 |
| 运行期间的通知条目 | 前缀 │ + 同一个工具底色(warning / error 级用出错档) |
它也是工作记录里的一条,不是 Agent 写的字 |
| 正文 | 不画竖条、不铺底色,保持 Pi 原本的样子 | 没被收起来的内容 |
组头上面那行留白,不画竖条:那行除了竖条什么都没有,右边没有文字接着,看上去就是一根悬在组头上面的短竖线(曾经这么画过,读起来像画错的一截),而上下两根竖条本身也差半格,硬接只会更明显。空行与组头同属一个点击块,点它照样展开/收起整组。组头文案与展开后成员命令的摘要文字都用弱化色,它们是「有这条」的提示而不是正文,不该比正文还抢眼。
运行期间扩展写入的块(pi-extensions-i18n 的通知、工作流结果面板、pi-distill 审计卡片)也跟着轨道走:收起态之外,它们的每一行前面补上同样的细竖条,内容整体缩进两列,轨道不会从中间被一块满宽块切断。收起态与运行之外的块不接 —— 那时组头本身不显示,加一条孤立竖条反而多出个没头没尾的结构字符。
接轨道时分两路,都只为了同一件事:让色带的左边缘与工具行对齐在第 0 列。通知条目(pi-extensions-i18n 画的那种满宽块)整行重铺成工具底色,连块自己那层 customMessageBg 一起换掉;其余块(审计行、工作流结果面板)保留自己的配色,只把前缀插进块自己那层底色里面。前缀如果拼在整行前面,色带就会从第 2 列才开始铺,而工具行的色带是从第 0 列铺的 —— 两者左边缘差两列,相邻行看上去就是一条锯齿(组头挨着几条遥测时最明显)。
接轨道时还去掉块开头自带的空行(Pi 的 CustomEntryComponent / CustomMessageComponent 都会在自己的内容前面插一个 Spacer(1)):运行期间画的是「一行一条」的密集列表,这一行就成了列表中间的空洞 —— 上下都是紧挨着的记录,只有扩展块前面空一行,看起来像列表被随机断开。去掉之后块与相邻记录紧挨着排,一条提示就占一行。运行之外的块不加轨道,也就保留 Pi 原本的间距。
三级都不带来源前缀:折叠头每轮都画、位置固定,前缀只是噪音,还会把文案推到与细节行不同的列上。粗竖条本身就是「这是清爽模式画的」的标记。
单条动作的标签直接用这条动作自己的摘要(「运行命令 ls -la」),多条才汇总成「主词 · N 步」;组内没有哪一类动作过半时不再写「探索 · N 步」这种让人猜的话,而是把实际构成写出来(运行命令 2 · 读取文件 1,条数多的在前),拿不到计数时才退回通用词。每一行折叠头和每一条可见工具行,首行都以 ▶ / ▼ 收尾,紧跟在文案右边 —— 状态和「可以点」一眼就能看到,没有要记的快捷键。
底色只用在工具行与接轨道的通知条目上:组头与成员命令(折叠态下组头就是这些调用的替身)铺 Pi 原生工具行的三档底色 —— 成员行看自己的状态(还在跑 toolPendingBg、出错 toolErrorBg、其余 toolSuccessBg),组头只有「当前组 + 本轮还没结束」才算进行中,其余都算已完成(否则最后一组会一直带着「还在跑」的底色)。这不是层级,是「这条不是 Agent 写的字」:Pi 原生工具行本来就有底色,摘要行替掉原生行之后得把这层语义接回来。运行期间的通知条目用的是同一套底色(warning / error 级用出错档),不是它自己那层块底色 —— 两套色档在列表里一行隔一行地出现就是一条条纹,读起来像表格没画完。底色块铺到整宽,所以这几类行会自己截断再补空格。折叠头、正文、活动块都不铺底色,主题缺这几个色键时整行退化成纯文本。
工作过程与最终答案怎么区分
Pi 在扩展入口导出了对话组件,本扩展替换 AssistantMessageComponent 的 render 与 updateContent,以及 ToolExecutionComponent.render:
| 组件 | 折叠时的行为 |
|---|---|
| 带 tool call 的 assistant 消息 | 整条隐藏(解说属于工作过程) |
| 不带 tool call 的 assistant 消息 | 保留,并挂上耗时头 |
| 工具行 | 整行隐藏,或收成一行动作组组头 |
| 扩展条目(custom entry) | 运行期间与会话恢复窗口内出现的条目一并隐藏;通知按级别:info 级(metrics 遥测、回执)一起收起,warning / error 留着 |
「不带 tool call 的消息就是最终答案」的依据是:agent 循环只有在一次回复不含 tool call 时才结束,所以一次运行里这样的消息只有最后那一条。
扩展条目(pi.appendEntry 写的 row,例如 distill 的审计行)走的是 Pi 内部的 CustomEntryComponent,它不在 Pi 的公开导出里,也没有折叠信号。所以这里补丁了 pi-tui 的 Container.prototype.render,靠「同时持有 entry / renderer / hasContent」认 出条目组件,收起时返回 0 行。只收工作条目:一次运行期间,以及会话恢复窗口(session_start 之后、首次 agent_start 之前)首次渲染的条目;运行结束后才出现的条目(提示、汇总)保持可见;pi-extensions-i18n 的通知按级别区分:info 级(metrics 的逐轮遥测、配置保存成功)属于过程噪声,跟着工作过程一起收起;warning / error 是扩展出错时唯一能说话的地方,无论何时都留着,否则「配置读取失败」这类警告会被静默吞掉。可用 hideExtensionEntries: false 关掉这个行为。
同一个补丁还负责把运行期间的扩展条目与消息面板接上轨道:内容按 width - 2 出图,去掉开头自带的空行,再逐行加上 │ 前缀,整行宽度不变、右边缘照旧铺满。通知条目走 applyEntryBand(整行重铺成工具底色,左边缘落在第 0 列),其余块走 applyEntryRail(保留自己的配色,前缀插进那层底色里,左边缘同样落在第 0 列)。归属按条目/消息对象记,Pi 重建组件后不会翻面。
被隐藏的行渲染为 0 行,所以耗时头正好落在最终答案上方。耗时头本身是包了 MouseRegion 的真实子组件,而不是 render 里拼的字符串 —— 因为 Pi 的 Container 按子组件高度计算鼠标命中偏移。
运行时的行为
agent 执行期间保持展开 —— 否则折叠状态下用户在答案出现前会什么都看不到。运行结束(agent_settled)后自动收起。如果用户在本次运行中手动切换过状态,本次运行不再自动收起。
运行中手动收起也一样:收起的是工作过程与活动块,顶部那条 处理中 · Ns 状态行照常显示 —— 它是运行结束前唯一能说明「还在跑」的信息(收起整轮后屏幕上就只剩它和正在流式的答案)。用时 … 头要等 agent_settled 写入耗时后才出现,之前同一个槽位里放的就是状态行。
恢复会话后的历史
/resume、/reload、/fork 之后,历史消息不会重放 agent_start / agent_settled,状态会停在初始的展开态,整段历史看起来像根本没开清爽模式。所以 session_start 会调一次 restoreHistory:开启总开关时直接按「已结束」处理(收起),下一轮真正开始运行时 agent_start 再重新展开。
代价:历史轮次没有耗时头。耗时与步数在内存里,不写进会话,所以恢复后 getRunDuration 拿不到值,折叠头就不显示。仅靠 f2 或命令展开全部。
配置
配置文件路径:<pi agent 目录>/extensions/pi-clean-mode/config.json,示例见 config.example.json。
{
"enabled": true,
"autoExpandWhileRunning": true,
"showRunHeader": true,
"hideThinking": true,
"hideExtensionEntries": true
}
| 配置项 | 含义 |
|---|---|
enabled |
总开关。关闭后所有补丁直接放行原始渲染。 |
autoExpandWhileRunning |
执行中自动展开,运行结束后自动收起。 |
showRunHeader |
在整轮最上方显示 用时 … 折叠头。 |
enableActionGroups |
把一个 turn 的多条工具调用收成一行组头。 |
showActivityArea |
运行中显示实时活动块,跟着当前动作组头走。 |
activityRows |
活动区高度,1-20,默认 4。 |
animateActivity |
是否播放动画;关闭后只保留静止标记。 |
hideThinking |
把 Pi 的 thinking 块整个抽掉(默认开)。 |
hideExtensionEntries |
折叠时连扩展写入的条目一起收起(默认开);info 级通知也跟着收,警告与错误仍可见。 |
hideThinking 是把 thinking 内容块从消息里抽掉,而不是开 Pi 自己的「隐藏 thinking」开关:后者会把 thinking 渲染成一行占位文本,即使把占位文案清空也会留下一个空行和它后面的 Spacer。抽掉内容块则连那两行一起消失(有测试盯着行数)。
调试
设置 PI_CLEAN_MODE_DEBUG=1 后,事件与渲染决策会追加写入 <pi agent 目录>/pi-clean-mode-debug.log。默认关闭,且开关只在进程启动时读一次,关闭时对渲染路径没有开销。
实时活动区
一块屏幕分两处:最上面只回答「整轮一共跑了多久」,最新状态永远贴在最新那条动作的下面。
用户:帮我改一下 xxx
▌ 处理中 · 42s ← 顶部:整轮时间 + 状态(粗竖条,不是底色块)
│ 探索 · 5 步 ▶ ← 当前组头:步数(组收起时成员行整行隐藏)
├─ ◐ 思考 正在追踪 token 失效路径… ← 活动块:思考,与命令行平级
└─ ⠋ 运行命令 npm test · 读取 4 · 搜索 3 ← 活动块:正在跑的动作 + 本轮分类计数尾注
↳ 12 passing ← 活动块:这条命令此刻的输出最后一行
组展开时同一个列表读起来是命令清单,思考行仍在末尾收口:
│ 探索 · 5 步 ▼
├─ 读取 src/index.ts ▶
├─ 运行命令 ls -la ▶
├─ 运行命令 npm test ▶
└─ ◐ 思考 正在追踪 token 失效路径… ← 与命令行平级,接在列表最后
- 顶部(运行级状态行):只报「在处理 + 跑了多久」,不带计数、不带思考与工具细节,也不带转动图标 —— 耗时每秒都在变,「还在跑」已经说清楚了。运行中是
▌ 处理中 · 42s,结束后换成▌ 用时 42s · 3 步 ▶—— 同列同款,状态切换只换文案。 - 组头:主词按组内实际动作选 —— 有一类动作过半时用它命名(
运行命令 · 12 步);混在一起说不清(一类不到一半)时写出构成(运行命令 2 · 读取文件 1),拿不到计数才退回通用词探索 · N 步。分类计数不在这里,它降级成活动块里最后一个子项行的尾注。 - 一个动作名只说一遍:组内只有一条时,组头就是那条动作的摘要(
运行命令 npm test ▶),活动块不再重列动作名,只补上思考与输出尾巴;多条成员的组头是汇总文案,没写出具体动作,所以收起时活动块才逐条列出正在跑什么 —— 展开后命令已经一行一条列在那里,动作名同样不再重复。 - 活动块:接在当前组最后一条可见行的下面。组展开时接在末位成员下面;组收起时成员行整行隐藏,接在组头下面。无论哪种情况它都贴在列表最底部,不会悬在中段。
- 活动块用竖折挂在组头下面:每个子项前面是
├─(后面还有别的子项)或└─(最后一个),输出尾巴缩到分支符后的正文列,续行前面还有后续子项时补一条│贯通。分支符与组头的竖条同列,连成一条轨道。只靠空格缩进时看不出这些行到底属于上面哪一条;去掉动作名(单条组)之后会重新算收口,思考行接替成为最后一项。 - 思考行与命令行平级:展开的组里成员行也用同一套树形前缀,思考行接在列表最后,所以它读起来是命令的兄弟项(末位命令让出
└─),不会挂在某条命令的多行正文底下。
所以任何时刻,眼睛往下扫到最后一行就能看到最新状态,不用回头往上看。运行级收起(工具行整行隐藏)时活动块整块不画,只留顶部的整轮时间 —— 收起的语义就是把过程收干净。
分类计数不再进组头:它接在活动块里最后一个子项行(正在跑的动作或思考头部)的后面,例如 └─ ⠋ 运行命令 npm test · 读取 3 · 命令 2。接在已渲染的行上而不是另占一行,所以它不占行数预算,单条组把动作行去掉之后也不会跟着消失。不接在输出尾巴上:尾巴是命令自己打出来的那行,把本轮计数接上去读起来像这条命令的汇总。只有一次动作时不出(组头已经把那次动作的名字写在上面了)。
活动块里全是普通行(不重复顶部的时间,行首带 ├─ / └─ 竖折前缀):思考头部、正在执行的工具(并行时逐条列出)与其最新输出行。内容全部来自真实事件,不生成推测出来的进度。
两条实现约束:
- 内容不变就完全不重绘。 每 tick 先把行渲染成字符串与上一 tick 比较,一致时根本不请求重绘。
- 行数只增不减。 运行期间活动块用空行补齐到本轮见过的最大行数,避免高度反复抖动把下面的内容推来推去;动画每 150ms 推进一步(约 6.7fps),定时器只在运行时存在并
unref(),animateActivity: false时降到 1s 并显示静止标记。
活动区在展示当前动作时会顺手隐藏 Pi 自己的 Working... 提示,避免同一件事说两遍。这个接管是整轮一次的:本轮只要接管过,就一直藏到本轮结束(clearActivityArea 交给运行结束时),不会因为「工具刚跑完、下一条还没开始」那一两帧活动行暂时为空就把提示弹回来 —— 底部那行提示带着耗时,一闪一闪比重复显示更难受。只有当轮首状态行也被关掉(showRunHeader: false)且这一帧没有活动行时,提示才会让回去,那是屏幕上唯一的反馈。
实现上分两处渲染,但共用同一份行数据(runtime.activityArea.rows 先补齐到本轮最大行数,再渲染成完整形态 lines 与去掉动作名的 detailLines):当前组的工具行渲染时把活动块接在自己末尾(component-patches.ts 的 appendActivityTail,只有最后一条可见行会接),轮首折叠头子组件只输出运行级时间(同文件的 createRunHeaderComponent 读 getRunStatusLines),组头主词由 getGroupActivityLabel(按组内过半分类,dominantActivityClass)拼上去,没有过半分类时改用 activityCompositionLabel 写构成(运行命令 2 · 读取文件 1),分类尾注由 activity-area.ts 在渲染后接到最后一个子项行尾(formatActivityCountersNote + appendActivityCountersNote)—— 分类按组累加(registerActionToolCall 的 activity 字段),所以历史组也能说出自己在干什么。工具调用在流式阶段就登记:stream-registration.ts 从流式 assistant 消息的内容块里扫出工具调用(形状 { type: "toolCall", id, name, arguments }),赶在 Pi 渲染那一行工具行之前写进动作组;只靠 tool_call / tool_execution_start 会晚 300ms 上下,那一行会先以「未登记」的原样画一两帧再突然被收走。开组也是同一处做的,且必须早于登记 —— 解说出现在工具调用后面时,那几个已经登记的调用会被改挂到新组。单条组与展开的多条组都用去掉动作名的形态(resolveActivityTail,后者是因为命令已经逐行列在列表里),收起的多条组才逐条列出正在跑什么。所有活动行在接上去之前先按本次渲染宽度截断(clampLinesToWidth):主屏模式下 pi-tui 遇到超过终端宽度的行会直接抛错停机,全屏模式则被硬切。三处都只认「当前组」与「当前轮的轮首承载者」,所以历史轮次不会重复显示同一块内容。折叠头与活动块都不铺底色,补位空行仍渲染成空行;工具底色只铺在组头与成员命令上。
兼容性
本扩展替换 Pi 组件的原型方法,因此与 Pi 的组件导出面绑定(AssistantMessageComponent.hasToolCalls、ToolExecutionComponent.render,以及「空渲染等于 0 行」的行为)。它在 reload 与 shutdown 时还原原型,并且不会覆盖安装之后被其它扩展替换掉的原型。
扩展条目折叠(hideExtensionEntries)额外补丁了 pi-tui 的 Container.prototype.render:Pi 内部的 CustomEntryComponent 没有公开导出,只能靠「同时持有 entry / renderer / hasContent」这个结构特征识别。Pi 若改了它的字段名、或让条目组件自己覆写 render,这一项会静默失效(条目重新变得可见)而不是报错;tests/extension-entry-patch.test.ts 盯着特征判定。
选 f2 是因为 Pi 内置键位没有占用它。如果你改过 Pi 键位,请避免与它冲突。
安装
pi install npm:pi-clean-mode
开发
npm test
npm run typecheck