real-vibe-coding0.1.1
A Pi extension that performs the agent's output — prose, reasoning and tool text alike — in time with MIDI music: a beat-synced stage panel, a look-ahead print queue, and a pluggable audio backend.
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
real-vibe-coding
一个Pi扩展,让对话里出现的一切都跟着音乐的节拍、一个 token 一个 token 或整行地浮现。 RealVibeCoding 用 MIDI 文件作为节奏基准,播放同名音频,并在编辑器上方的舞台面板里按拍揭示 Pi 会打印的内容——助手的正文、思考过程、每一次工具调用那一行。
效果
https://github.com/user-attachments/assets/d7bc2118-e3bb-4283-a7cf-eb09bee1271c
♪ He's a Pirate · 200 BPM · 0:42/1:17 · sequential
▁▃▅▇█▇▅▃▁▂▄▆█▇▅▃▁▂▄▆█▇▅▃▁▂▄▆█▇▅▃▁▂▄▆█▇▅▃▁
1 │ Weighed the two parsers, then ran the suite.Running it.
2 │ $ npm test
3 │ ℹ tests 85
4 │ ℹ pass 85
5 │ ℹ fail 0
6 │ Imports look fine.import { readFileSync } from "node:fs";
7 │
8 │ export function main(path: string): string {
正文、思考、日志、代码共用一个舞台,因此不按内容类型配色——唯一的骨架是行号, 以及鼓点。过长的行会换行,而不是被截断丢掉。
力度条是一排原地起伏的均衡器。形状由音乐决定——最近一个起音点的力度定波高, 音高定波纹的疏密(低音宽、高音细),时值定命中后涌动持续多久(长音荡得久、 短音一闪即收);波纹随速度向前走,所以整行始终在动。它从第一个鼓点起就铺满 面板,不会从一端长出来:不滚动、不累积,只有柱子的高度在变。
队列吐空、演出还没结束时,舞台末尾多出一行活动提示——还在跑的工具取最早开始 的那个,带上已经等了多少秒;没有工具在跑就是在等模型:
⏳ $ npm test · 12s
⏳ waiting for the model…
不足约 0.7 秒的空档不出这行:那是分段的呼吸,不是等待。面板高度只够标题、 力度条和一行正文时,这行也让位给正文。
舞台上是原样的输出。当模型自己没在段落结尾换行时,两段会黏在同一行上, 例如上面的第 1 行和第 6 行——那是模型自己的格式,不是面板加的。唯一的例外是 工具调用那一行:它不是模型的文本,而是扩展自己的文本,所以像会话记录里一样 自成一行(先起行,再收行)。
会话记录里不再出现尚未演完的助手正文和思考——两者都换成一行占位提示,内容 改在舞台上按节奏出现;工具调用和它的结果也一样,块先收成一行提示,舞台演到 那一段再交还给原来的渲染器:
▶ performing · 18 lines · 412 chars
▶ performing · read src/engine.ts
提示分两种:文字还在流式输出、尚未进队列时是 printing;已经排队在演是 performing。
占位只在演出进行时顶替原文,每段演出完成的那一刻就当场换回原文——逐段补全,
而不是等任务结束。舞台也是:它有内容要演才出现,任务一结束——agent 已收尾、
队列已吐空——就撤下,把行数还给会话记录:完整输出最终落在会话记录里,而舞台
那点固定高度本来也只放得下它的尾巴。
工具块先占位、演完恢复
工具调用行入队时,它的块先收起成一行提示(▶ performing · read src/engine.ts);
调用行演完恢复,工具结果接着占位(▶ performing · 3 lines · 24 chars),结果也演完就
回到工具自己的渲染器——该折叠的还是折叠,ctrl+o 照常可用。write 和 edit 的调用块
是例外:块上画着作品的预览(write 是正文开头几行,edit 是 diff),预览也是"作品",
所以它多押一步——等作品印完才露面,不随调用行提前剧透。
环境要求
- Pi 0.81 或更高版本
安装
pi install npm:real-vibe-coding
或者只在本次会话里试用:
pi -e npm:real-vibe-coding
曲库
RealVibeCoding 有自带的曲库。
如需自定义曲库,则需把成对的文件放进 <agent 目录>/real-vibe-coding/music/,例如
~/.pi/agent/real-vibe-coding/music/He's a Pirate.mid 和同目录下的
He's a Pirate.wav,RealVibeCoding 会自动找到它们。
.mid 提供节奏,.wav 提供声音。两者应当是同一份演奏——打印跟随 MIDI 的
起音点,音频同步播放,所以如果两者对不上,就会看到代码逐渐脱离节拍。
配置
运行 /real-vibe-coding 打开设置列表,
或直接编辑~/.pi/agent/real-vibe-coding.json:
{
"enabled": true,
"playlistMode": "sequential",
"loopTrack": "",
"stageHeight": 12,
"showPlaceholder": true,
"audioBackend": "wav-soundplayer",
"audioCommand": "ffplay -nodisp -autoexit \"{file}\""
}
| 设置项 | 含义 |
|---|---|
enabled |
关闭整个演出,代码恢复为正常渲染。 |
playlistMode |
sequential(顺序)、shuffle(随机)、loop-one(单曲循环)。 |
loopTrack |
单曲循环模式下重复的曲目,用曲名(.mid 文件名去掉扩展名);空表示曲库第一首。 |
stageHeight |
舞台面板占用的行数,其中两行是标题和力度条;空闲时的活动提示高度够时再占一行。 |
showPlaceholder |
是否在会话记录里用一行占位提示替换尚未演完的正文、思考和内建工具块。 |
audioBackend |
wav-soundplayer 或 command。 |
audioCommand |
command 后端的 argv 模板。 |
演出开启时,页脚里有一枚 ♪ performance mode 标记;关闭时什么都不显示。
播放后端
| 后端 | 行为 |
|---|---|
wav-soundplayer |
Windows 上的默认值。通过 PowerShell 的 System.Media.SoundPlayer 播放 .wav。无需安装、不弹窗口,进程被杀时声音立刻停止。 |
command |
执行 audioCommand:一个按空白切分的 argv 模板,其中 {file} 会替换成媒体路径。进程退出即视为一曲结束。非 Windows 系统或想用更好的播放器时使用。 |
command 示例:
"audioCommand": "ffplay -nodisp -autoexit \"{file}\""
"audioCommand": "afplay \"{file}\""
"audioCommand": "\"C:/Program Files/mpv/mpv.exe\" --no-video \"{file}\""
命令不经过 shell,且模板在替换 {file} 之前就完成切分,因此带空格的
路径始终是单个参数。播放器自身的路径带空格时,用引号包起来。
快捷键
| 按键 | 作用 |
|---|---|
ctrl+alt+p |
暂停 / 继续音乐;暂停期间打印照常。 |
ctrl+alt+n |
切到下一曲;暂停期间按下时,继续后从下一曲开始。 |
ctrl+alt+d |
立刻吐完队列里剩下的代码并停止音乐。 |
ctrl+alt+p 把演出按在原地:音乐停下、节拍不再揭示、时钟读数冻结,舞台头部
标出 ⏸ paused,力度条下那行写清按什么继续。打印不会跟着冻结——节拍没了,
队列就没有要等的东西:按下时积压一次印完,按住期间新到的文本随到随印,也就是
pi 默认的速度;模型的背压闸门也因此无事可做。再按一次继续:当前曲目从头重放
(播放器无法从中间接着放),音乐回来,重新按拍指挥之后的打印。演出未在进行时
按键什么都不做;若按下时任务已经收尾、积压又恰好印空,演出就照常收场,舞台撤下。
ctrl+alt+n 切到下一曲:当前曲目的播放器被切掉,播放列表前进一首,正在排队的
打印不中断,改由新曲目的节拍接着揭示。暂停期间按下不会立刻换曲——演出还按在原地,
这一按被记住,ctrl+alt+p 继续时直接从下一曲开始,而不是重放被按停的那首。演出
未在进行时按键什么都不做。
ctrl+alt+d 在积压很多时很有用:Esc 能中止 agent,但停不掉已经排好队的打印。
它也是让正在等待打印跟上来的那次请求立刻放行的最快办法。
实现方式
Pi 没有替换助手消息渲染器的 API,也没有延迟 token 流的手段,因此 RealVibeCoding 使用仅有的三个杠杆:
markdown 转换器把它拦得到的东西换成占位提示。 也就是助手的正文和思考 ——每个文本片段,以及每一段连续的思考块(后者正是 Pi 交给转换器的单位)。 片段还在流式输出时显示
printing;消息定稿后,只有真正进入过队列的片段才会 被替换。这份记录只存在于本次进程,绝不落盘——因此恢复旧会话时,旧内容会 照常显示,不会留下 "内容消失"的历史。它也只覆盖正在进行的那个任务:任务一 结束就清空,并让会话记录失效重绘,于是记录里渲染出真正的正文。内建工具带着包了一层的渲染器重新注册(
toolhold.ts)。Pi 从工具调用 流进消息的那一刻就画出它的块——早在工具开始执行之前——没有 "先别画" 的 接口。但块是由工具自己的渲染器画的,而一个定义可以用同名重新注册:于是每个 内建工具都包一层渲染器注册回去,调用行或结果文字还在路上时由包装层用同样的 一行提示作答,打印一走过这段文字,绘制就交还给真正的渲染器。队列坐标沿用 markdown 侧的那一套:调用行结束在哪、结果文字结束在哪、已经印到哪,crossed一发现某个被挡住的块整段越过去了,就触发一次重绘。编辑器上方的部件负责演出。 正文和思考的 delta 随流式输出入队,所以队列是 跑在音乐前面的,而不是跟在模型后面。工具调用那一行由
tool_execution_start入队,按工具名和参数重建——转换器看不到调用。工具 结果则由tool_result事件入队,该处理器只做观察——在那里返回任何值都会 替换掉工具结果本身,包括喂给模型的那一份。上舞台的不总是结果自己的文字:read的结果是例外,读进来的文件内容不是产出,不入队;write和edit则是演出活儿而不是回执——write 把它写下的全文交上来,edit 把它算出的 diff 交上来,取代"Successfully …"那类一行回执。回执里没有活儿可演时(出错、或 结果没带载荷),才按原文入队。这两种工具的调用块自带作品预览,它也押在作品 后面:工具运行期间不露面,等作品印完才露面(出错时,等错误信息印完);结果 什么都没演时——没有打印步可越过——由close当场揭示,并请一次重绘。每个 MIDI 起音点揭示多少由音符自己定价:速度定每秒钟的基准,力度是重音 ——最强拍打印的 token 数是最弱拍的十六倍;时值是要买的时长——长音一次多 印、短音一次少印,文件没给长度的按一拍算,再长的音也按一小节封顶;音高反向 倾斜——同样长度与力度,低音的价码乘 2、高音的乘 0.5;和弦厚度再加一道砝码 ——单音按原价,和弦越厚越重、趋近两倍,每个音加得都比上一个少。预算以 整 token 为单位花掉:一个 delta 就是一个 token,整块浮现,绝不会看到半个 token 慢慢长出来;工具调用行和工具结果没有 token 可言,按每 token 四个字符 折算后逐字符切。这一拍装不下开头那个 token 时也会先把它整块印出来,所以 每一拍都有动静。
预算攒到一整行的价码(24 个 token,约当面板里的一行文字)时,这一拍不再数 token,直接买下整行。光标停在行首,印出的就是一整行;停在行内,则先把 被打断的这行补完,再连下一行一起印出来——最强的拍落地时,读者看到的是一条 完整的行。行还没从模型到齐的,按队列里现有的内容印。
模型也会被同步拉住:一旦缓存里的内容多到音乐要花大约一分钟才印得完,下一次 向 LLM 的请求就会先停住,等打印把积压消化到一定程度再放行。没有这道闸,长 任务攒下的输出会比演出快得多——队列会一直涨下去。等待中的请求会自行恢复;
ctrl+alt+d可以让它立刻放行。
时钟就是 MIDI 文件自带的变速表,因此速度变化被精确跟随。起音点调度在每次 命中后都按绝对目标时刻重新计算下一次的等待时长,所以掉帧不会让后面的拍 点整体偏移。另有一个独立的 30fps 定时器,只负责视觉脉冲、时钟读数和活动行。
音乐在一个任务里首次有输出时才开始,而不是任务一开始——舞台也在同一时刻 出现。思考期间音乐继续播放;一首放完自动续下一首;只有任务已经收尾且队列 已吐空时才停止。这一次停止同时也是把已打印的文本交还会话记录、并撤下舞台的 时刻:面板只存在于演出期间,空闲时一行也不占。队列吐空而演出未终时,活动行 报出在等什么:一个还在跑的工具(取最早开始的)或模型;空档不足约 0.7 秒、 或面板腾不出那一行时,它不显示。
ctrl+alt+p 的暂停是引擎自己的状态,不是打印的:播放器被切掉、节拍定时器
取消、时钟帧冻结,曲目循环停在一个恢复点上等。打印不归它管——节拍没了,队列
就没有要等的东西:暂停瞬间把积压一次吐完,按住期间每次入队后立即揭示,随到随印
即 pi 默认的节奏;背压闸门自然也无事可做。继续时同一个循环接着跑,把当前曲目
从头打开,音乐重新指挥之后新文本的揭示。若任务已经收尾、暂停把最后一截印完,
演出照常按"收尾且队列已空"收场。
ctrl+alt+n 的切歌只动曲目循环:播放器切掉、播放列表前进,队列原地不动,由
下一首的节拍接着揭示;按住期间按下时记成标记,循环在恢复点上读到它就直接换到
下一首,而不是把当前这首从头打开。
播放列表按会话记住位置:顺序和随机模式下,同一个会话里的下一次任务接着上一首 停下的地方继续,而不是从曲库开头重来;单曲循环重复的那一首在设置面板里选择。
故障排查
pi 中途退出,终端随后不停刷出乱码。 原因通常在模型自己执行的一条命令:pi
就运行在 node.exe 上,而按映像名通杀的命令——taskkill /F /IM node.exe、
Stop-Process -Name node——会把 pi 连同它本想清掉的那个进程一起终结。/F 是
硬杀:没有任何清理来收起终端,原始模式、隐藏光标、鼠标跟踪就此留下;之后 shell
会把每次按键的转义序列原样回显,看起来就是终端在连续打印乱码。
RealVibeCoding 用 pi 的 tool_call 事件挡住了这类命令(与演出是否开启无关):命中的
调用不会执行,模型会收到一条说明,要求它改用按 PID 的杀法——先
netstat -ano | findstr :<端口> 找到 PID,再 taskkill //F //PID <PID>。拦截面
很窄:只有 shell 工具里按名字通杀 node 的写法会被拦,按 PID、或对别的映像名的
kill,都不受影响。
终端已经花了的话,在 shell 里执行 reset(或重开一个终端)即可恢复;乱码只是
被回显的转义序列,没有破坏任何东西。若还有音乐在响,那是被硬杀时没来得及退出的
播放器进程,这一首放完就会停。
已知限制
- 不做音频/MIDI 合成。 内置后端播放预渲染好的
.wav。要合成.mid就得 依赖 SoundFont 音色库,而用户未必有。 - 不做节拍检测。 MIDI 文件本身就带精确的变速表,分析音频只会引入误差。
- 不获取真实播放位置。 默认播放器不回报播放到哪儿了,进度读数由 RealVibeCoding
自己的时钟推算。播放器的启动延迟是恒定偏移,由
musicSyncOffsetMs吸收。 - 一曲的节奏随它的 MIDI 结束。 如果
.wav比.mid长(通常只多几秒的 尾部余音),打印会暂停到下一首开始。 - 继续演出会把当前曲目从头重放。 后端的契约只有"放一个文件、停下、结束":
不回报位置,也不接受从中间起播。暂停切掉的是播放器进程,恢复就只能重放整首。
打印不受影响:暂停时积压一次印完,按住期间随到随印,音乐回来后才重新按拍。
不想重放这首,就先在按住期间按
ctrl+alt+n切歌,继续时从下一首开始。 - 很长的输出会让 agent 停一下。 一旦积压到音乐要花大约一分钟才能印完,下一次
请求就会等打印跟上——否则队列会在整个任务期间一直涨下去。
ctrl+alt+d可以跳过这次等待。 - 只有内建工具的工具块会先占位。 块由工具自己的渲染器绘制,而扩展只能 重建内建工具的定义;自定义工具和 MCP 工具的渲染器没有接口可以读取,它们的 块在演出期间照常显示。入队演出不受影响:调用行和结果文字照常上舞台。
- 图片不参与演出。 结果为图片的工具调用在舞台上什么都不打印——没有文字可 揭示。Pi 仍会在会话记录里画出它。
- 读取的文件内容不参与演出。 read 的结果不上舞台——读进来的输入不是 agent 的产出,工具块随到随画(如同图片),文件内容照常留在会话记录里。顺带的好处: 读大文件不再让队列涨起来、让 agent 在背压闸门前等打印。
- 你自己发的话不参与演出。 演出的是 agent 的产出,你输入的内容不在其中。
- Pi 隐藏思考时,舞台仍会演出思考。
hideThinkingBlock只隐藏会话记录里的 思考块。若两者都不想要,请关闭整个演出。
许可
MIT