
你有没有过这种体验Claude Code 在终端里跑一个多文件重构任务日志像瀑布一样往下滚你在 Chrome、VS Code、终端三个窗口之间来回跳每隔十几秒切回去看一眼生怕它跑偏或者早就停下了。明明 AI 替你干活结果你比手动写代码还累。这个痛点真实且普遍。Windows 11 没有原生的“AI 工作状态”展示位任务栏太小、小组件太被动最直接的解法就是自己做一个AI 编程状态悬浮球一个始终置顶、不抢焦点、随时扫一眼就知道 Claude Code 在干嘛的小窗口。这篇文章我会完整拆解它的原理、选型、代码和 Windows 11 下的所有坑你可以直接照抄也可以当成一个练手项目改造成自己的版本。1. 频繁切窗口的代价悬浮球要解决的真实问题先说清楚一件事这份焦虑不是你的错觉而是多任务切换的注意力成本被严重低估了。1.1 为什么不放心让它跑Claude Code 这种编程代理和普通代码补全完全不同它不是“你敲一个字它补三个字”而是拿到一个目标之后自主规划、自主执行。它会自己读文件、改代码、跑命令、看报错、再改一个任务可能持续几分钟甚至更久。这个过程中你面临一个信息盲区它是在思考还是已经卡住了它在调哪个工具读写哪个文件它这次跑了多久是不是陷入循环了它是正常结束还是报错退出了你以为切回终端看一眼就能解决但实际体验是每次切换都要先找回上下文眼睛在终端那片密密麻麻的日志里找关键信息找到之后还要判断“这是刚发生的还是几分钟前的”。一个 5 秒的查看动作实际消耗的是几十秒的注意力恢复时间。人的专注力是有限的一天下来光是这个“看一眼”的动作就能消耗掉大量精力。1.2 悬浮球的核心信息架构所以这个悬浮球不是把终端窗口缩小版搬过来它只需要解决一个核心问题让最关键的几个状态信息常驻视觉边缘。我设计的悬浮球只展示四类信息信息项作用当前状态空闲、工作中、已结束、出错最近事件正在执行什么动作比如“写入 file.tsx”会话标识当前 Claude Code 的会话目录或 session ID发生时间最后一次事件的时间戳用来判断“是不是卡了”状态用颜色区分眼睛余光就能判断绿色是空闲橙色是工作中红色是报错或异常退出。不需要阅读能力看颜色就够了。这套设计有个额外好处因为它只显示状态不显示详细日志窗口可以做得很小很克制不会遮挡代码区域也不需要频繁交互。它更像是仪表盘而不是日志终端。2. 方案选型为什么是 PowerShell WPF 自绘悬浮窗确定需求之后下一个问题是用什么技术做这个悬浮球我在 Windows 11 上评估过几条路线各自都有明显短板。2.1 现成方案的短板先从省事的说起。市场上确实有现成悬浮球软件但问题在于它们和数据源之间的适配。Claude Code 的状态需要从命令行工具实时拉取很少有通用悬浮球软件能理解“一个 JSON 事件流”并渲染成状态指示器。Rainmeter 这类桌面美化工具理论上可以做到但你得用 Lua 写皮肤、解析 JSON、维护数据接口学习成本不比写一个小脚本低而且对多显示器 DPI 的适配经常需要手动调。AutoHotkey 做全局快捷键很好用但它自带的 GUI 渲染能力偏弱做不出漂亮的半透明圆角窗口。Electron 这类的 Web 技术方案倒是渲染能力够强可为了一个悬浮球引入 Node.js 和几百 MB 的依赖多少有点过度工程了。开机自启之后内存占用也难看。2.2 PowerShell WPF 的四个优势最终我选了 PowerShell WPF理由非常实际第一Windows 11 自带。PowerShell 5.1 和 .NET Framework 4.8 开箱即用不需要装任何额外运行时。第二WPF 的渲染能力足够。圆角、阴影、半透明、动画都原生支持做一个现代化观感的悬浮球绰绰有余。第三脚本式开发迭代快。改一行代码重新跑一次就生效不用编译调试体验接近写前端。第四和 Claude Code 的对接链路最短。Claude Code 本身的配置和 hook 脚本也用命令行生态PowerShell 在这条链路上几乎零摩擦。2.3 整体数据链路三条通道并行这个悬浮球的架构本质是一条单向数据流从 Claude Code 流向悬浮球全程本机通信数据源头Claude Code 的 hook 机制在特定事件发生时触发一个 PowerShell 脚本。传输通道脚本把事件信息写成一行 JSON追加到本地事件文件events.jsonl或者通过 HTTP POST 发给本机监听服务。展示终端悬浮球脚本用DispatcherTimer定时读取最新事件解析后更新 UI 状态。之所以选“事件文件 定时轮询”作为主力通道是因为它够简单够稳定。文件读写是操作系统最成熟的能力不存在端口占用、进程崩溃导致数据断流的问题。HTTP 通道后面我作为进阶方案补充可以做到实时推送但会引入常驻进程。3. 数据源头用 Claude Code 的 Hook 把状态“推”出来悬浮球要工作第一步是让 Claude Code 把状态“吐出来”。这里的关键机制是 Claude Code 的 hook。3.1 hook 是什么为什么选它hook 是 Claude Code 提供的一种事件回调机制。简单理解你在配置文件里声明“当某个事件发生时帮我执行这个命令”Claude Code 就会在对应时机调用你的脚本并把事件信息作为参数传过去。哪些事件值得关注这是整个设计方案的核心。我主要监听三类事件触发时机在悬浮球上显示什么UserPromptSubmit用户提交新的指令进入“工作中”状态显示指令摘要PreToolUse/PostToolUse工具调用前后显示正在执行的动作Stop一次响应结束回到“空闲”显示结束原因这些事件消息里通常会带上会话路径、事件类型等上下文信息足够悬浮球判断当前处于什么阶段。3.2 配置 hook 的具体步骤Claude Code 的 hook 配置在用户级或项目级的 JSON 配置文件里。Windows 11 上常见路径是C:\Users\你的用户名\.claude\settings.json。打开这个文件不存在就新建在hooks字段下注册事件处理器{ hooks: { UserPromptSubmit: [ { matcher: , hooks: [ { type: command, command: powershell -NoProfile -ExecutionPolicy Bypass -File \D:\\dev\\claude-float\\event.ps1\ } ] } ], PreToolUse: [ { matcher: , hooks: [ { type: command, command: powershell -NoProfile -ExecutionPolicy Bypass -File \D:\\dev\\claude-float\\event.ps1\ } ] } ], Stop: [ { matcher: , hooks: [ { type: command, command: powershell -NoProfile -ExecutionPolicy Bypass -File \D:\\dev\\claude-float\\event.ps1\ } ] } ] } }这里有个细节值得说为什么命令前面要加powershell -NoProfile -ExecutionPolicy Bypass因为 Claude Code 执行 hook 命令时使用的是系统默认 shell直接写.ps1脚本路径可能因为执行策略被拦下来。-ExecutionPolicy Bypass是临时绕过策略只对当前进程生效相对安全。-NoProfile是为了避免加载用户配置文件拖慢启动速度。hook 执行得越快对 Claude Code 主流程的影响越小。3.3 事件持久化脚本被 hook 触发后event.ps1要做的只有一件事把事件信息写成一行 JSON追加到事件文件末尾。param( [string]$EventType ) $payload [PSCustomObject]{ time Get-Date -Format yyyy-MM-dd HH:mm:ss type $EventType project $env:CLAUDE_PROJECT_DIR session $env:CLAUDE_TRANSSCRIPT_PATH } | ConvertTo-Json -Compress Add-Content -Path D:\dev\claude-float\events.jsonl -Value $payload -Encoding UTF8实际使用时你可能会发现 hook 是否传入事件类型参数、环境变量名是什么会随 Claude Code 版本有差异。我在不同版本上踩过坑所以建议你直接在脚本里把环境变量和入参都打印出来先跑一次看输出。先做数据侦查再写解析逻辑这个顺序能省很多排查时间。如果 hook 参数传不进脚本还有备选方案脚本里直接读取 Claude Code 的 transcript 文件也就是会话记录 JSON用Get-Item拿最后修改时间再解析出最后一条消息的类型。这个方法绕开了 hook 参数兼容问题但会多几次文件 IO对性能略有影响不过对悬浮球这个场景完全够用。3.4 不依赖 hook 的兜底方案有些用户使用的是第三方封装版 Claude Codehook 机制可能不完整。这时候可以退一步直接把 Claude Code 的终端输出中转到事件文件。思路是这样的用 PowerShell 启动 Claude Code 并重定向输出流逐行扫描发现包含tool_use、Stop、error这类关键词的行就解析成事件写入事件文件。这种做法不需要 hook只要终端输出流可达就行。$proc Start-Process -FilePath claude -ArgumentList --output-format stream-json -RedirectStandardOutput D:\dev\claude-float\raw_output.log -RedirectStandardError D:\dev\claude-float\raw_error.log -PassThru -NoNewWindow Get-Content D:\dev\claude-float\raw_output.log -Wait -Tail 1 | ForEach-Object { if ($_ -match stop_reason|tool_use|error) { Add-Content D:\dev\claude-float\events.jsonl -Value $_ -Encoding UTF8 } }需要注意-Wait -Tail 1在 PowerShell 5.1 里有时不够灵敏我会改用FileSystemWatcher监听文件变化这样延迟更低。兜底方案不是最优解但它的存在保证了悬浮球在任何模式下都能拿到数据。4. 悬浮球本体一个“不抢焦点”的置顶小窗数据通道打通了接下来是整个项目里最容易翻车、也最考验 Windows 桌面开发基本功的部分悬浮球窗口本身。4.1 窗口核心属性用 PowerShell 创建 WPF 窗口并不复杂核心代码是这样的Add-Type -AssemblyName PresentationFramework Add-Type -AssemblyName PresentationCore Add-Type -AssemblyName WindowsBase $window New-Object Windows.Window $window.WindowStyle [Windows.WindowStyle]::None $window.AllowsTransparency $true $window.Background [Windows.Media.Brushes]::Transparent $window.Topmost $true $window.ShowInTaskbar $false $window.ResizeMode [Windows.ResizeMode]::NoResize $window.Width 320 $window.Height 120这几行每一句都有讲究。WindowStyleNone去掉标题栏和边框AllowsTransparencyTrue允许背景透明配合无边框样式才能做圆角。TopmostTrue确保窗口始终在普通窗口上方。ShowInTaskbarFalse让悬浮球不占用任务栏位置保证 Alt-Tab 切换窗口时不会把它当成一个独立窗口。窗口默认位置放在右下角也就是任务栏“安全区”上方$workArea [System.Windows.SystemParameters]::WorkArea $window.Left $workArea.Right - $window.Width - 24 $window.Top $workArea.Bottom - $window.Height - 24这里用WorkArea而不是屏幕分辨率是因为WorkArea已经扣掉了任务栏不会出现窗口被任务栏挡住一半的问题。4.2 点击不抢焦点的关键技术悬浮球最容易犯的错误是你正在编辑器里打字不小心点了一下悬浮球焦点立刻被它抢走输入断掉切回编辑器还要重新点一下。这个问题不解决悬浮球反而成负担。解决方案是给窗口加上WS_EX_NOACTIVATE扩展样式。这个扩展样式的语义就是窗口可以被点击、可以响应鼠标消息但永远不会被激活、不会抢占输入焦点。Add-Type using System; using System.Runtime.InteropServices; public class Win32Helper { [DllImport(user32.dll)] public static extern int GetWindowLong(IntPtr hWnd, int nIndex); [DllImport(user32.dll)] public static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong); } $GWL_EXSTYLE -20 $WS_EX_NOACTIVATE 0x08000000 $hWnd [System.Windows.Interop.WindowInteropHelper]::new($window).EnsureHandle() $currentStyle [Win32Helper]::GetWindowLong($hWnd, $GWL_EXSTYLE) [Win32Helper]::SetWindowLong($hWnd, $GWL_EXSTYLE, ($currentStyle -bor $WS_EX_NOACTIVATE)) | Out-Null要注意的是EnsureHandle()必须在窗口显示之前或刚显示后调用确保 HWND 已经创建。我习惯在$window.Show()之后立刻执行这段代码。这个细节是悬浮球体验的分水岭。加上这一句你对它的使用方式会从“小心点别点错”变成“随手点、随手划完全无压力”。4.3 UI 布局与状态呈现窗口本体用一个半透明深色卡片内部分三块状态圆点、事件描述、时间戳。布局用 WPF 的StackPanel容器就能实现不需要复杂 Grid。状态呈现用三个核心元素一个Ellipse圆点颜色跟随状态切换一个TextBlock显示最近事件描述一个TextBlock显示时间戳和会话目录名称。为了提升可读性我给窗口加了一个背景遮罩层用的是 60% 不透明度的深色圆角矩形。半透明的好处是遮挡感弱即使悬浮在代码上方也不会太干扰视线。窗口的圆角效果我直接让根Border的CornerRadius和背景同步Border Background#CC1E1E1E CornerRadius12 Padding12 StackPanel StackPanel OrientationHorizontal VerticalAlignmentCenter Ellipse x:NameStatusDot Width10 Height10 Fill#4CAF50 / TextBlock x:NameStatusText Margin6,0,0,0 ForegroundWhite FontSize13 Text空闲 / /StackPanel TextBlock x:NameEventText Margin0,8,0,0 Foreground#E0E0E0 FontSize12 TextWrappingWrap Text等待任务... / TextBlock x:NameMetaText Margin0,6,0,0 Foreground#909090 FontSize11 Text-- / /StackPanel /Border我把 XAML 放在 PowerShell 的外层字符串里运行时用[xml]加载再解析出控件引用。用纯代码写也能实现但可读性差很多后面要改布局会很痛苦。4.4 消息循环与事件消费窗口显示出来后需要定期消费事件文件。这里我选DispatcherTimer而不是多线程轮询因为 WPF 的 UI 更新必须在主线程完成DispatcherTimer天然跑在 UI 线程上省去了跨线程调度的麻烦。$timer New-Object Windows.Threading.DispatcherTimer $timer.Interval [TimeSpan]::FromSeconds(2) $timer.Add_Tick({ $events Get-Content D:\dev\claude-float\events.jsonl -Tail 20 -Encoding UTF8 # 解析最后一条事件更新 UI 控件状态 }) $timer.Start()两秒的轮询间隔是我实测后的折中值。太短500ms会导致高负载下文件读取频繁反而拖慢系统太长10s又会让状态更新看起来迟钝。Claude Code 的单次工具调用通常在几秒以上两秒足够及时又不会给系统制造压力。解析逻辑上要注意Get-Content -Tail 20是倒序输出最后 20 行所以第一条数据是最近的事件。找到第一条之后用ConvertFrom-Json解析字段根据type字段映射到状态颜色和文案。5. 从零到一Windows 11 上的完整搭建过程理论说完了下面直接给出可以照抄的完整步骤。5.1 准备工作目录、执行策略、依赖建议把项目放在一个固定目录比如D:\dev\claude-float\原因很简单hook 命令里写了绝对路径路径一旦变化就得同步修改 settings.json省得给自己挖坑。New-Item -ItemType Directory -Path D:\dev\claude-float -Force检查执行策略Get-ExecutionPolicy如果是Restricted需要给当前用户放开权限Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUserRemoteSigned允许本地脚本运行远程下载的脚本必须有签名是比较均衡的策略。5.2 合并 Claude Code 的 hook 配置打开C:\Users\你的用户名\.claude\settings.json把hooks片段合并进去。如果你之前已经配置过其他 hook比如自动格式化注意保留已有字段不要整个覆盖。合并之后验证一下配置是否生效在任意项目目录运行claude随便发一条指令然后检查事件文件Get-Content D:\dev\claude-float\events.jsonl -Tail 5如果看到 JSON 行在增长说明 hook 链路已经通了。5.3 编写悬浮球脚本创建一个float.ps1把第 4 节的窗口初始化代码、XAML 布局、事件消费逻辑都放进去。完整结构大致如下# 1. 设置 DPI 感知见第 6 节 # 2. 加载 WPF 程序集 # 3. 创建窗口对象设置属性 # 4. 加载 XAML获取控件引用 # 5. 添加 WS_EX_NOACTIVATE # 6. 启动 DispatcherTimer 消费事件文件 # 7. 显示窗口并进入消息循环 $window.Show() | Out-Null $window.Activate() | Out-Null [System.Windows.Threading.Dispatcher]::Run()运行方式powershell -NoProfile -ExecutionPolicy Bypass -File D:\dev\claude-float\float.ps15.4 开机自启动与首次验收悬浮球这种工具类脚本建议放到启动文件夹登录后自动拉起。对当前用户启动文件夹路径是$startupPath [Environment]::GetFolderPath(Startup) Copy-Item D:\dev\claude-float\float.ps1 $startupPath\claude-float.ps1注意启动文件夹里的脚本如果直接是.ps1文件系统不会自动执行因为双击.ps1默认是用记事本打开。更可靠的做法是创建一个快捷方式指向powershell.exe参数里带上-File$WshShell New-Object -ComObject WScript.Shell $shortcut $WshShell.CreateShortcut($startupPath\claude-float.lnk) $shortcut.TargetPath powershell.exe $shortcut.Arguments -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File D:\dev\claude-float\float.ps1 $shortcut.Save()-WindowStyle Hidden很重要否则开机时会闪一下黑色控制台窗口很掉价。首次验收建议按这个顺序检查悬浮球出现且位置正确点击悬浮球任意区域正在编辑器的光标焦点不丢失打开 Claude Code 发一个任务悬浮球状态在几秒内变橙色任务结束后状态变回绿色时间戳刷新。6. 实测中遇到的五个 Windows 11 专属坑方案听着很顺但实际跑起来Windows 11 的桌面生态有很多细节足以让悬浮球“看起来能用却用着难受”。这些坑我全部踩过汇总如下。6.1 高 DPI 缩放导致窗口模糊现象4K 屏开 150% 缩放悬浮球文字发虚圆角边缘锯齿严重。原因PowerShell 的默认 DPI 感知级别是 System 或 Unaware在高 DPI 显示器上会被系统位图拉伸。解法脚本开头调用SetProcessDpiAwarenessContext把进程标记为 Per-Monitor V2Add-Type using System; using System.Runtime.InteropServices; public class DpiHelper { [DllImport(user32.dll)] public static extern bool SetProcessDpiAwarenessContext(IntPtr dpiAwarenessContext); } $DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 [IntPtr]::new(-4) [DpiHelper]::SetProcessDpiAwarenessContext($DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) | Out-Null这段代码必须放在Add-Type -AssemblyName PresentationFramework加载 WPF 之前执行让 WPF 初始化时就拿到正确的 DPI 上下文。放在窗口创建之后调用就晚了。6.2 任务视图和 Snap Layout 对置顶窗口的“夺权”现象Windows 11 按 WinTab 进入任务视图或者拖动窗口触发 Snap Layout 时悬浮球偶尔会被遮盖失去置顶效果。原因这两种系统 UI 是全屏覆盖层它们接管了最顶层 z-orderTopmost窗口在这种模式下排序反而被压住。对策这是系统级行为应用层很难完美对抗。我能给的实用方案是在悬浮球上双击触发一次强制置顶$window.Topmost $false $window.Topmost $true通过关闭重开Topmost强制 WPF 重新排序窗口层级。实测在任务视图中悬浮球被遮挡的概率从“经常”降到了“偶尔”可接受。6.3 hook 命令超时拖住主流程现象配置了Notification或Stophook 后Claude Code 响应变慢每条消息结束后都要等一两秒。原因Claude Code 的 hook 是同步等待的——它会在执行完主逻辑后等待你的命令退出超时才会继续。PowerShell 启动和Add-Content虽然快但在极端情况下也可能被系统 IO 拖慢。解法hook 命令里用Start-Process异步启动子进程让 hook 立即返回Start-Process -FilePath powershell -ArgumentList -NoProfile -ExecutionPolicy Bypass -File D:\dev\claude-float\event.ps1 -WindowStyle Hidden这样 Claude Code 主进程不等待脚本执行结束等待感完全消失。代价是事件写入文件有一两百毫秒的延迟对悬浮球来说完全无感知。6.4 PowerShell 5.1 读取 UTF-8 JSON 乱码现象事件文件里的中文路径或提示信息在悬浮球上显示成乱码。原因PowerShell 5.1 从文件读取时默认按系统 ANSI 编码中文系统是 GBK解码。而event.ps1用-Encoding UTF8写入事件文件两者不一致导致乱码。解法读取时显式指定 UTF-8Get-Content D:\dev\claude-float\events.jsonl -Tail 20 -Encoding UTF8另外ConvertTo-Json对中文字符默认是转成\uXXXX的这个不用管ConvertFrom-Json会自动还原。6.5 脚本进程残留杀错 PowerShell调试阶段最常遇到的一个坑同时跑着几个 PowerShell 脚本其中一个卡住了你想关掉它顺手执行了Stop-Process -Name powershell然后你正在用的其他 PowerShell 窗口也被一起杀掉了。对策给悬浮球脚本进程加一个特征通过进程 ID 精准控制。在脚本开头把自己写入 PID 文件Set-Content -Path D:\dev\claude-float\float.pid -Value $PID需要重启悬浮球时先读 PID 再定向清理$pidValue Get-Content D:\dev\claude-float\float.pid Stop-Process -Id $pidValue -Force调试阶段我建议直接在单独的 PowerShell 窗口里运行float.ps1用 CtrlC 结束进程比任何进程管理都直观。7. 接着还能怎么玩核心功能跑通之后这个悬浮球就是一个可以无限扩展的底座。我分享几个我实际验证过好用的玩法。7.1 多会话切换Claude Code 支持同时开多个会话每个会话在各自的项目目录里干活。悬浮球的事件文件被多个会话同时写入时可以按project字段分组悬浮球上做一个小下拉切换器显示每个会话的状态点一下就能看到另一个会话的最近动态。实现上只需要读取事件文件时按project分组取最后一条然后在上拉菜单里列出分组名称。我实测在三个会话并行时依然流畅窗口也不会因为信息变多而臃肿。7.2 点击悬浮球一键打开最新 transcriptenv.CLAUDE_TRANSSCRIPT_PATH指向当前会话的 JSON 记录文件。给悬浮球加一个 Click 事件直接调用Invoke-Item $env:CLAUDE_TRANSSCRIPT_PATH这样你看到悬浮球显示“工作中”时点一下就能直接跳到完整对话记录查看具体上下文。这一步对复盘和调试帮助非常大因为它把“状态感知”和“问题定位”串起来了。7.3 团队共享状态面板如果你的团队有多台机器同时跑 Claude Code 任务可以在每台机器上把事件文件转发到一个共用的 HTTP 接口用一个简单网页展示所有人的状态。这个方向适合扩展到“测试机集群跑 AI 自动化任务”的场景已经超出悬浮球本身但数据模型完全一致。7.4 适配其他编程代理最后说个通用性很强的点这套方案不绑定 Claude Code。任何 CLI 编程工具只要满足“能在事件发生时执行外部命令”或“输出流能被重定向”就能对接这个悬浮球。我自己实测过把 hook 配置换成其他编码代理的命令事件文件照样写入悬浮球基本零改动就能显示新工具的状态。提示hook 的事件类型字段各工具略有差异对接新工具时先打印一次原始事件对照字段名调整解析逻辑即可结构上完全不用动。从最早“不放心让它跑”的焦虑状态到现在悬浮球常驻桌面右下角、我安心切到浏览器查资料写方案这个项目带给我的不只是省下那几秒切换时间更是工作节奏上的一种松绑。你如果也在 Windows 11 上高频使用 Claude Code不妨花一个下午把它搭起来过程中对 Windows 窗口机制和事件管道的理解会是很意外的收获。