十个 tab 跑着十个 AI,我手一抖关了一个
Ten tabs, ten AIs, and the one I closed by accident
我桌面上常年开着十个 Windows Terminal tab。
每个 tab 里跑着一个 Claude Code session,每个 session 在写不同项目的代码:左边那个在改 A 股策略的回测,中间那个在调 Caddy 反代,最右边那个在 review 一个我自己都快忘了的 PR。
它们安静地跑着,偶尔在底部闪一下 prompt,等我回去说下一句话。
这是我一天里大部分时间的样子。
直到那天下午,我伸手够咖啡,肘子蹭到了键盘上的 Ctrl+W。
✵
我盯着那个一秒前还在跑着东西、现在只剩下一片空白的 tab,第一反应不是骂脏话。
是慌。
因为那个 tab 里跑的不是一个 shell,是一个我已经磨了一整个上午的 Claude——它刚帮我把一个反复出 bug 的 PnL 计算逻辑捋清楚了,正准备落代码。它脑子里装着我们刚刚那二十轮对话的所有上下文。
那条会话历史,在那一秒之后,不会回来。
哦对了,分屏也没了。我之前在那个 tab 里 Alt+Shift+D 切了一个左右分屏,左边跑 Claude,右边开着 git log --oneline 做参照。分屏的工作目录、profile 选的是哪个 shell、那一组配套关系——一并蒸发。
我那天没继续干活。我打开了 GitHub,开始翻 Windows Terminal 的源码。
✵
我们这些每天在 terminal 里跑 AI 的人,心里多多少少有个错觉:tab 嘛,关了再开就好。
这个错觉来自一个老习惯——以前 tab 里跑的只是 shell,shell 是一个无状态的东西,你关掉它和你重开它,没差别。
但现在不一样了。
现在 tab 里跑的是 AI session。AI session 是有状态的——上下文是状态、待办是状态、它记下的项目规矩是状态、它跟你磨出来的那种”它已经知道你不喜欢加注释”的默契,也是状态。
你关一个 tab,不是关一个 shell。
你是在关一个正在累积的东西。
这跟我上次写 claude-repath 时碰到的问题,本质上是同一个:AI 编程时代最值钱的东西,往往挂在最脆弱的载体上。一次是路径字符串,这次是 tab 进程。
✵
我那天晚上的目标很简单:让这件事不能再发生。
不是”少发生”,是不能再发生。手一抖、咖啡蹭到、windows update 重启、WT 崩了——任何一种意外,都不能让我再丢一次会话。
我列了一下要防的事:
- 单个 tab 误关(最常见,每周至少一次)
- 整个 Windows Terminal 进程退出(WT 闪退、系统重启、不小心点了红 X)
- 后台 tab 跑完了,我不知道——必须一个个切过去才能确认 Claude 是不是 idle 了
前两个是结构问题,第三个是感知问题。
如果只防第一个,重启一次还是全军覆没;如果只防前两个,我每天还是要切来切去看哪个 Claude 在等我;如果只防第三个,那只是把”漏了”的时刻往后推,没解决底层。
得三层一起做。
✵
第一层:tab 布局不能丢。
这一层其实 Windows Terminal 自己就能解。我之前没去翻设置,所以一直不知道。
settings.json 顶层加一行:
"firstWindowPreference": "persistedWindowLayout"
keybindings 数组里追加:
{ "id": "Terminal.RestoreLastClosed", "keys": "ctrl+shift+z" }
前者管整体——关 WT 时自动把整个窗口的 tab 结构、分屏布局、每个 pane 的工作目录序列化到磁盘,下次启动自动还原。后者管单个——关错一个 tab 按 Ctrl+Shift+Z 撤销,跟 VS Code 撤销关闭文件一个道理。栈深 100,挪过的 pane 也能恢复。
我顺手翻了一下 microsoft/terminal 仓库的 TabManagement.cpp,发现关闭 tab 时它会调用 tab->BuildStartupActions(BuildStartupKind::None),递归遍历整棵 pane 树,把每个 split 序列化成 SplitPane action,含方向、比例、profile GUID、实时 CWD。重启时把这些 action 回放一遍。
这是 WT 早就埋好的能力,没几个人知道。
但它有个边界:能恢复结构,没法恢复进程。tab 重启回来,分屏布局还在,工作目录还对,但 Claude session 是从零开始的——里头跑的进程不会被序列化。
这意味着,第一层只能解决一半问题:我的工作”摆位”不丢了,但工作”内容”还是没了。
得有第二层。
✵
第二层:Claude session 的对话历史本身要落盘。
这一层我本来准备自己写——想着在 Claude Code 启动时拉一个 daemon,定时把对话状态序列化到本地 JSON。
写了一半我去翻 Claude Code 的 ~/.claude/projects/ 目录,发现:它早就在干这件事了。
每个项目目录下面有个 sessions/ 子目录,里头一堆 .jsonl 文件,每个文件是一次会话的完整流水。每一轮对话——user 说了啥、assistant 回了啥、调了哪些 tool、tool 返回了啥——按 turn 一行一行落盘,实时写入。
我关 tab 那瞬间,Claude session 的进程是死了。但那个 .jsonl 文件,从头到尾完整躺在硬盘里,一个字节都没丢。
我那天慌得没意义。
我只要在新 tab 里 cd 进同一个项目,然后 claude --resume <session_id>,整段对话就接着续上了。它甚至连”我刚才说到哪了”都不需要我提醒——.jsonl 完整放着,Claude 一读就知道。
Anthropic 已经替我做完了。 我之前没意识到只是因为没出过事——出事的人才会去翻这个目录。
所以第二层我什么都不用做。只需要在文档里写明:“会话内容这一层 Claude Code 自己保住,不用配置。”
✵
到第二层为止,意外丢失这件事已经被防住了。
但还剩第三个问题:我每天还是在不停切 tab,因为我不知道哪个 Claude 跑完了。
这不是”丢失”问题,是”感知”问题。十个 tab 在后台跑,谁好了?谁卡了?谁正等着我点 yes?我只能一个个切过去看。
切来切去这事看起来不重,但放在一整天里——每次切 tab 5 秒,每天切个一百次,半小时没了。
我想要的很简单:Claude idle 了就主动叫我,不要让我去问它。
✵
这一层是我撞坑撞得最狠的一层。也是这个工具包的灵魂。
我第一版(v0.1.0)的方案听起来天经地义——
Claude Code 有个 hook 机制,能在 Stop 和 Notification 事件里执行任意命令。我让它在 idle 的时候 printf '\a',那是 ASCII 响铃字符。Windows Terminal 默认收到响铃会触发 bellStyle: ["window", "taskbar"]——任务栏图标闪一下、窗口边框闪一下——这不就完美吗?
我配好了,重启 Claude,等它 idle。
任务栏没动。
我以为是 hook 没触发,去翻 debug log,发现 hook 是触发了的——printf '\a' 确实跑了。
但响铃没出来。
我盯着这事看了快两个小时。
✵
后来我去翻了 Claude Code 的 hook 文档,找到一行字,看完想骂街:
For most events, stdout is written to the debug log but not shown in the transcript.
翻译人话:hook 命令的 stdout,根本不会到 terminal。
它被 Claude Code 自己捕获走,写到 debug log 里去了。
那一刻我才想明白:printf '\a' 输出的那个字节,确实被打印了——但它打进了 Claude Code 的内部 log 文件,没有任何一条路径让它到达 Windows Terminal 的输入流。WT 自然不会响铃。
✵
不死心。
我说那我绕开 stdout——直接写 /dev/tty,那是 controlling terminal 的设备文件,写它就是直接绕过 Claude 的捕获写到终端设备本体上。Linux 老办法。
我把命令改成 printf '\a' > /dev/tty 2>/dev/null || true,重启,等 idle。
任务栏没动。
我去 Git Bash 里手动跑这条命令:
$ printf '\a' > /dev/tty
/usr/bin/bash: line 1: /dev/tty: No such device or address
No such device.
Claude Code v2.x 的 hook 子进程没有分配 controlling tty。/dev/tty 在那个上下文里根本不存在——它不是被 Claude 捕获了,它压根没出生。
而我那个 || true 是好心办坏事——它把错误吞了,hook 看起来一切正常,没有任何错误日志。静默失败。
那天晚上我对着屏幕坐了很久。我突然意识到一件事:
任何”让一个字节到达 terminal 来触发 WT 行为”的方案,都走不通。
不是路径选错了。是这整个方向是死的。
✵
我换了思路。
既然字节到不了 terminal,那就绕过 terminal。
Windows 11 本身就有现代化的桌面通知系统——Toast 通知,靠 Windows.UI.Notifications API。这玩意儿不需要 terminal 当中间人,直接走系统 API,弹到右下角。
PowerShell 社区有个模块叫 BurntToast,封装了那套 WinRT API。一行命令就能弹 Toast:
New-BurntToastNotification -Text 'Claude 等你输入', '项目: quant-trading'
我把 hook 命令改成这个,重启 Claude,等 idle。
右下角”叮”一声,弹出一条 Toast。
成了。
这才意识到 Claude Code 的 hook 配置里有个 shell 字段——
{
"type": "command",
"shell": "powershell",
"command": "New-BurntToastNotification -Text ..."
}
加上 "shell": "powershell",hook 命令直接在 PowerShell 中执行,完全跳过 bash。再也不用纠结那个 bash 嵌套 PowerShell 的引号转义噩梦了。
整个 v0.2.0 的核心就一句话:别让字节走 terminal,让系统 API 直接弹通知。
✵
弄通之后我又顺手做了一件事——把通知分类。
Claude Code 的 Notification hook 不止 idle_prompt 一种。我翻了它的源码,发现有六种事件类型:
| matcher | 什么时候触发 |
|---|---|
idle_prompt | Claude 回答完了,等你输入 |
permission_prompt | Claude 想执行个 Bash 命令,等你按 yes |
elicitation_dialog | Claude 中途用 AskUserQuestion 问你东西 |
auth_success | 认证成功 |
elicitation_complete | 你回答完 elicitation |
elicitation_response | elicitation 响应 |
后三个是 Claude 自己内部状态变化,不需要打扰我;前三个都需要我介入,但需要我做的事情不同——是该输入还是该批准还是该决策。
所以我让三个 Toast 用不同标题:
Claude Code→ 等你输入(我可以慢慢回)Claude Code · 等待授权→ 等你批准(一般要立刻去看,可能在等执行)Claude Code · 询问中→ 等你决策(Claude 卡住等我答)
扫一眼通知标题就知道是什么级别的事。需要立刻处理的和可以稍等的,视觉分开。
✵
这三层加起来,整个工作流变成了这样:
- 手一抖关错 tab →
Ctrl+Shift+Z一秒复活,分屏布局回来 - 整个 WT 进程崩了或者系统重启 → 重开 WT 自动恢复全部 tab + pane 结构
- 哪怕极端情况整个会话被强杀 →
~/.claude/projects/sessions/*.jsonl完整保留,新开claude --resume接着续 - 后台某个 Claude 闲着了 → 右下角弹 Toast 告诉我是哪个项目、什么级别的事
整套配置加起来不到 40 行 JSON 加一个 PowerShell 模块。但每一行背后都是一次撞墙——从 \a 响铃到 /dev/tty 到最后绕开 terminal 走 WinRT,三个晚上才走到对岸。
✵
回看这件事,我有个挺扎心的体会。
我们这些把 AI 当工具用的人,常常以为:只要工具本身没坏,工作流就是稳的。
但其实工作流的脆弱性,从来不在工具本身。
它在工具的承载方式上。
claude-repath 那次,我以为我的”AI 默契”在项目代码里,结果发现它绑在一个绝对路径字符串上,挪个文件夹就断了。
这次,我以为我的”工作上下文”在 Claude 会话里,结果发现它绑在一个 tab 进程上,手一抖就没了。
每次都不是 Claude 出问题,是承载 Claude 的那一层基础设施——文件系统的路径、操作系统的 tab、终端的 stdout——在不经意的瞬间,把上面所有积累全部清零。
我们一直在用脆弱的基础设施,去承载越来越值钱的 AI 上下文。
这件事,是早晚要被认真对待的。
✵
claude-code-wt-tab-resilience 在 GitHub 上(xPeiPeix/claude-code-wt-tab-resilience),MIT,免费。configs/ 目录里两个 snippet 文件,复制粘贴改两处配置就跑起来。BurntToast 一次 Install-Module,从此不用管。
我没期待它火。
但下一次你伸手够咖啡、肘子蹭到 Ctrl+W 的时候——我希望那个 tab 还能回来。
或者更好的——你已经习惯了它一定会回来,所以你从来不会再为它心疼一次。
✵
也想问问你——
你每天和 AI 一起堆出来的那些上下文:对话历史、待办、那些它已经懂你的东西……
你有没有想过,它们其实并不在 AI 那一边?
它们在你这一边。在一个 tab 进程里、在一个 PID 里、在一个 stdout 流里。
挂在那些你从来没在意过的东西上。
哪天你伸手够咖啡,它还在吗?