把 Claude Code 会话状态搬到桌面按键
作者通过 Claude Code Hooks 将各会话状态写入独立 JSON 文件,再由 Stream Deck 插件每秒读取并用按键颜色呈现。记录仅包含目录、会话与状态,用于及时发现审批请求和任务完成。
AI 深度解读
同时开着几个 Claude Code 会话,真正拖慢进度的可能是人:授权请求藏在窗口后面,任务结束了也没发现,还得反复切窗口确认。作者忙时会并行跑约 5 个会话,于是用 Claude Code hooks 把状态搬到桌面的 Stream Deck 上。
这套方案没有让模型跑得更快,而是让「哪个会话正在等我」更容易被看见。值得借鉴的是状态设计,以及作者为保证状态可信踩过的两个坑。至于省了多少时间,作者没有测量。
把会话状态搬出来,需要哪三块
作者用的是 Stream Deck Neo,有 8 个按键和一条细信息栏。整套实现分成三部分:
- Claude Code hooks 在提交提示、调用工具、请求权限、回合停止等时刻执行脚本。
- 脚本为每个会话写一个小 JSON 文件,记录目录、会话和状态,不保存提示正文或工具输出。
- Stream Deck 插件每秒读取一次文件,用按键颜色显示状态,按下按键就把对应窗口切到前台。
这相当于给多个会话做了一个桌面状态面板。看到红色,按键进入窗口,再在原窗口里批准操作。按键承担定位功能,授权仍在 Claude Code 里完成。
作者要缩短的是人发现等待状态的时间。
这种设计对并行处理文档、小修复和研究任务的人尤其直接:不必记住每个窗口刚才跑到了哪一步,也不用持续轮询窗口。
哪些事件才算「正在等人」
状态映射是整个方案的核心。蓝色代表运行,红色代表需要人回应,橙色代表回合结束、请来查看,灰色代表初始空闲。
下面这段代码解决了一个容易漏掉的问题:会话等待用户回答,也应该被归入「等人」。
// hook events -> key state
function stateFor(d) {
switch (d.hook_event_name) {
case "UserPromptSubmit":
case "PostToolUse":
return "running"; // blue
case "PreToolUse": // asking me a question also counts as waiting on me
return ["AskUserQuestion", "ExitPlanMode"].includes(d.tool_name) ? "ask" : "running";
case "PermissionRequest":
return "ask"; // red
case "Notification":
if (d.notification_type === "permission_prompt") return "ask";
if (d.notification_type === "idle_prompt") return "waiting";
return null;
case "Stop":
return "waiting"; // orange: done, please look
case "SessionStart":
return "idle"; // gray
default:
return null;
}
}
要看的是 PreToolUse 分支:AskUserQuestion 和 ExitPlanMode 被单独识别为 ask。只监听 PermissionRequest,就会漏掉这些需要用户参与的时刻。
另外,Stop 表示当前回合停止,不能直接推导出整个工作目标已经完成。橙灯应该理解成「有结果需要查看」,验收仍得回到会话里做。
状态灯最怕的,是看起来正常却已经过期
作者遇到最难查的问题是:会话结束了,按键却一直保持蓝色。
脚本先写临时文件,再通过重命名替换正式文件;插件每秒读取正式文件。在 Windows 上,如果替换恰好撞上读取,重命名可能报 EPERM。最初版本静默吞掉错误,结束状态没写进去,还积累了 28 个孤立临时文件。
下面是作者的修复:短暂等待后重试,最终失败时清理临时文件并抛出错误。
function replaceFile(tmp, file) {
for (let i = 0; ; i++) {
try { fs.renameSync(tmp, file); return; } catch (e) {
if (i >= 9) { try { fs.unlinkSync(tmp); } catch {} throw e; }
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 30); // ~30 ms
}
}
}
要注意 i >= 9:循环从 0 开始,最多尝试 10 次重命名,失败后的单次等待约为 30 ms。它也没有区分错误类型,直接移植时需要结合自己的运行环境判断。
另一个坑来自 /clear:会话继续运行,但 ID 换了,面板一度出现两个同名按键。作者随后在由 clear 触发的新会话启动时删除旧记录,并维护旧 ID 到新 ID 的映射,供插件使用。
作者还加入了像素小鸡 Aqui,名字来自天鹰座 Aquila:工作时抛球,等待授权时举问号,空闲时闭眼显示「z」,相邻按键都工作时还能传球。它后来衍生出超过 20 集 YouTube Shorts,小鸡还会驾驶像素风 WRX S4。这部分更偏个人趣味,作者没有给出动画改善提醒效果的数据。
我的判断
我认同把「等待人处理」单独暴露出来。多个 AI 会话并行时,运行状态通常很显眼,等待状态却容易被遮住;一个始终可见的入口能减少查找动作。
但这仍是个人项目的使用经验。作者没有给出耗时测量,也没有与系统通知、提示音或屏幕角落状态条做对照,因此不能据此声称 Stream Deck 更高效。作者能确认的变化,是自己不再到处翻找窗口。
硬件是否合适,要看工作方式。经常并行开会话、已有 Stream Deck 的人,使用成本较低;偶尔只开一个会话,屏幕提示或声音也许就足够。
更值得保留的是可靠性要求:文件更新失败不能被吞掉,ID 变化不能留下重复记录。原文没有展开窗口定位方式、异常退出后的记录清理,以及完整插件实现,照着文章还不能直接复刻整套系统。
能拿来做什么
- 经常并行使用 Claude Code 时,先把授权请求和用户提问做成可见提醒,可以从屏幕角落或提示音开始,再决定是否需要实体按键。
- 用 Node 编写 hooks 状态脚本时,只记录定位会话所需的信息,并让写入失败能够被发现;涉及 Windows 文件替换时,检查重试和临时文件清理。
- 给会话面板增加
/clear场景的验证,确认新旧 ID 切换后不会出现重复入口,也不会跳到错误窗口。 - 把「回合结束」与「任务完成」分开处理。收到结束提醒后,回到会话检查结果和待办,再决定是否继续执行。