跳到正文

Codenotch

一个 macOS 常驻小程序,把一小条黑色 notch 钉在屏幕边缘,让它变成你手上那些编码 agent 的实时账本。悬停出来的卡片列出每一家、每一个限额窗口和它的重置时间,圆环内层在会话干活时有一条细弧在转,一旦某个会话停下来等你,它就变成琥珀色。

Screenshot of Codenotch
编辑截图, 1 Oct 2026Codenotch ↗

这是什么

一个没有窗口、也没有 Dock 图标的 macOS 应用:它全部的表面就是焊在某一条屏幕边缘上的一小块黑色 notch,只回答一个问题——每个编码助手的用量上限已经花掉多少。Claude Code、Cursor、Codex、Antigravity 以及另外十来个 provider 各占一个圆环;悬停会列出它的限额窗口、各自的重置时间,以及按名字列出的每一个实时会话;agent 在干活时圆环内层有一条细弧在转,停下来等你时会变成琥珀色的脉冲。五小时限额还能以文字形式画进菜单栏,上一次的好读数可以跨启动保留,同样的百分比也会经由局域网推给手机端。整套设计靠一句坦白撑着:没有任何厂商会公布一个干净的会话限额接口,所以每个适配器都申明自己的可信度,借用所属工具已经持有的东西,并把每一种失败都变成一个可见状态,而不是编一个百分比。

谁做的仓库所有者,账号 vinzdg:791 次提交里有 387 次是他写的,全部集中在 2026-09-05 到 2026-09-30 这 25 天。另外 404 次分散在贡献者列表列出的其他四十九个账号上,最多的是 RawJat 的 58 次和 mulhamna 的 57 次。103 条提交带共同作者尾注,其中 88 条写的是一个 Claude 模型,另有 Codex 的 4 条和 Cursor 的 2 条。

它是怎么搭起来的

组成 · 6

一个没有窗口的常驻应用,全部界面就是一块焊在屏幕边缘的面板。这块面板是一个非激活的 NSPanel,层级在状态栏,除了自己那块形状之外全部点击穿透;形状只按右边写了一份 SideNotchShape,其余三条边由它变换而来,而不是手写四份。里面的一切都在一维的栈空间里工作——along 是 provider 栈的长度方向,across 是从边框向内量——只有 NotchPlacement 一处把它映射回真实屏幕坐标,这也是让「上边或下边」变成一个独立布局、而不是一次旋转的原因。NotchLayout 里的每个尺寸都以设计帧像素为单位、引自 docs/design/frame-124-hover-tooltip.png,于是布局可以直接对着那张帧核对。再往下是一套 provider 协议,附带一个必须申明的可信度,和一个负责轮询、跨启动保留上一次好读数、持久化限流截止时间、并把每一种失败都变成一个界面画得出来的状态的 store。

Sources/Providers/
89 个文件、881 KB:每个 provider 一个适配器,凭证读取器就放在它旁边,包括 46 KB 的 Claude OAuth provider、28 KB 的 Codex 本地 provider、13 KB 的 Cursor 凭证,外加共用的 SQLiteStore、凭证缓存,以及一个 95 KB 的 GlyphOutline,装着这些矢量标记——一部分从设计帧描出来,一部分从厂商自己的 SVG 压平而来。
Sources/Notch/
15 个文件、371 KB:表面本身。NotchWindowController 100 KB、NotchViewModel 73 KB、NotchRootView 51 KB、SideNotchShape 37 KB、NotchLayout 30 KB、NotchGeometry 25 KB——四条边、折叠、过角,以及与硬件刘海的合并都在这里决定。
Sources/Model/、Sources/Sessions/ 与 Sources/Costs/
store 以及所有喂给它的东西。UsageStore 47 KB,装着轮询、刷新调度、陈旧判定和退避;UsageArchive 把上一次好读数和持久化的限流截止时间带过重启,UsageResetWatcher 盯着窗口翻转。旁边是 28 个会话监视器,每个工具一个,因为它们各自宣告自己的方式都不同:Claude Code 每个进程写一个会话文件,Cursor 根本不公布注册表,Codex 只会往 rollout 后面追加。
Sources/Settings/、Sources/Features/ 与 Sources/App/
可见的那一半。SettingsView 一个文件 123 KB,Preferences 58 KB;ReleaseNotes 又是 55 KB 的应用自带版本史,用 Swift 写而不是从 appcast 拉取,这样第一次启动、没有网络时它也在。TooltipCard 是 53 KB 的悬停卡片,AppDelegate 66 KB 装着应用自身的接线,Sources/Localizable.xcstrings 有 1.5 MB。
Tests/
113 个文件、1.6 MB,按关注点而不是按文件对应 Sources/,而且钉住的是录下来的响应而不是在线端点:AntigravityTests 80 KB、CodexUsageTests 70 KB、UsageResponseTests 66 KB、NotchLayoutTests 66 KB、MergesWithTheCutoutTests 65 KB;布局测试靠采样整条轮廓来核对画出来的形状,而不是戳一个点。
windows/ 与 docs/
移植与纸面记录。windows/codenotch 是 63 个 Rust + Tauri 2 文件,其中 main.rs 有 98 KB,另有两个 HTML 界面分别 114 KB 和 142 KB。docs/specs/2026-08-28-usage-notch-design.md 是项目照着建的设计文档,docs/plans/ 里三份计划(其中一份讲本地模型 provider,35 KB),docs/providers/ 存按 provider 分的笔记,而 TASKS.md 是 110 KB 的实现史,把更正和当时的推理都留在里面。

取舍,以及它替代了什么

  • 读 OAuth 用量端点,而不是读本地记录 替代 计划中的第一个适配器——把 ~/.claude/projects/**/*.jsonl 解析成一个滚动的五小时窗口

    语料是在的——15,838 条助手消息,每条都带 token 计数,足够重建 70 个窗口——但它既不包含上限也不包含窗口元数据,从它算出的百分比需要一个凭空发明的分母。端点直接返回 Anthropic 自己的数字。代价就写在旁边:这不是一个公开 API,随时可能变,所以它是变了之后第一个失败的测试(UsageResponseTests),而本地解析器仍然被写成端点消失时正确的后备方案。

  • 借用所属工具已经持有的凭证 替代 自己走一遍 OAuth 流程,或者提供一个登录按钮

    作者自己的说法是:这个应用根本没有账号可连,所以一个「连接」按钮只会是表演;真正值得展示的是这些读数属于谁。这个选择一直延伸到打包——App Sandbox 保持关闭,因为它会挡住读取 Cursor 的 state.vscdb 和 Codex 的 rollout 日志,而 Developer ID 并不需要它。借用后来还成了一个正确性问题而不只是便利问题:早期版本在 WebView 里登录 cursor.com,悄悄创建了第二个 Cursor 账号,于是如实报告了一份属于别人的 0% 用量。

  • 给主标题窗口起名,而不是取第一个 替代 windows.first,以及更早的「最紧张的那个窗口」

    Claude 在会话窗口一滚过就会把 five_hour 从限额数组里拿掉,于是一条位置式的规则会让每周数字在最有人可能正盯着它的那一刻滑进会话的位置。而「最紧张者优先」有同一个毛病的温和版本:会话 22% 对每周 24% 时显示 24,之后会话 27% 对每周 24% 时显示 27。现在主标题是一个被声明的 id,那个窗口真的不在时格子显示一道横杠,而不是把别的窗口提上来。

  • 去问 Antigravity 自己的语言服务器 替代 直接调用 Google 的配额端点

    cloudcode-pa 上的 retrieveUserQuotaSummary 对个人账号返回 403,因为这个 API 会判断是哪个客户端在问,而应用没法诚实地宣称自己就是 Antigravity。Antigravity 自己的窗口有同样的问题、也用同样的办法解决,所以应用照做:机器上那个服务器本来就同时持有凭证和客户端身份。说好的代价是它只在 Antigravity 运行时有效——而后来有贡献者证明这个限制比看上去更窄:那个服务器可以用 --standalone 启起来,不挂编辑器也能读到同一份钥匙串凭证。

  • 没有上限可除的时候,就显示一个计数 替代 一个分母由应用自己编出来的百分比

    Antigravity 只回答档位、一个数字都不给,而 Google 对一个裸的 Gemini API key 根本不公布用量资源,所以那一个圆环是把这些工具自己写下来的东西加起来的——Gemini CLI 的会话文件、OpenCode 的数据库、Hermes 的用量表——按调用 id 去重(因为 CLI 会把同一次调用写两遍)。Perplexity 只报告还剩多少、因此根本没有分母,正是它让限额窗口的占比和重置时间变成了可选字段;它的适配器留在树里,但已不再注册。

依据docs/specs/2026-08-28-usage-notch-design.md、TASKS.md(109,699 字符)、README.md(26,771 字符)、CONTRIBUTING.md、docs/plans/2026-08-28-usage-notch-plan.md、Sources/Providers/AntigravityBridge.swift、Sources/Providers/CodexLocalProvider.swift、Sources/Providers/CursorCredentials.swift、Sources/Model/UsageStore.swift、.github/workflows/package.yml,以及完整的 516 个文件树及其体积。

制作过程

6 个阶段
  1. 01

    二十五天、十九个版本,和一个改过名的应用

    仓库建于 2026-09-05,到 2026-09-30 已有 791 次提交、2,639 个星、420 个 fork 和 19 个 release——从 2026-09-06 的 v1.4.0 到 2026-09-30 的 v1.20.0,另外还有一个滚动的 preview 标签,每次推送到 main 都会被重建并重新上传。应用自带一份能回溯到 1.0.0 的版本说明目录,所以版本线比它在 GitHub 上的第一个 release 更早;而设计文档开头仍然写着「Working name」,并没有把它当成已经定了的事。从 UsageNotch 改名留下的痕迹,作者自己都记了下来:旧应用还装在 /Applications/UsageNotch.app,没有任何东西会去删它;钥匙串会再问一次,因为它的访问列表绑定在一条包含 bundle identifier 的指定要求上;Sparkle 的更新源仍指向 vinzdg.github.io/usage-notch/,而 dmg 已经从 hivinz.com 分发。

  2. 02

    Claude Code 与 Cursor:钥匙串、缓存,和编辑器自己的数据库

    设计文档开篇就把整个项目赖以成立的坦白写在前面:没有任何厂商会公布一个干净的「your session limit is N% used」接口。于是每个适配器都去借用所属工具已经持有的东西,并申明自己的可信度——official、derived 还是 manual——凡是这里算出来的数字,悬停卡片都会加上一个波浪号。Claude Code 有三个来源,按顺序来。Claude Desktop 是一个 Chromium 应用,它自己面板画出来的用量响应就写在 ~/Library/Application Support/Claude 下的 HTTP 缓存里;只有缓存 URL 正好是这个账号的 /api/organizations/<id>/usage 的条目才会被打开,匹配依据是 Claude Code 为该 profile 记下的组织;而由于响应体是 zstd,仓库里 vendored 了一份只做解码的 Zstandard。之后才去跑 claude "/usage"。最后才拿登录钥匙串里的 OAuth token 去请求 GET https://api.anthropic.com/api/oauth/usage——那条命令自己用的同一个端点,返回的是 Anthropic 自己的会话与每周数字。Cursor 有文档的 Admin 与 Analytics 接口是团队级的、需要管理员密钥,所以圆环改为请求 GET https://cursor.com/api/usage-summary,凭证是编辑器自己的会话,从 state.vscdb 里读出 cursorAuth/accessToken 与 cursorAuth/stripeMembershipAuthId,再以 WorkosCursorSessionToken=<account>::<token> 这个 cookie 发出去——同一个 token 换成 bearer 头会返回 401。

  3. 03

    Codex 与 Antigravity:一个从磁盘读来的 token,一个跑在回环上的语言服务器

    Codex 是实时读的:GET https://chatgpt.com/backend-api/wham/usage,用 Codex CLI 存在 ~/.codex/auth.json 里的 access token 和 account id 认证,这个应用只读它们,从不刷新也从不写入。这条失败时退回 Codex 自己写进每个线程 rollout 日志里的限额快照——最新的那一份通过 state_5.sqlite 的 threads 表找到,而不是去翻几千个文件的会话目录树。Antigravity 是最有意思的一家,因为 Google 会拒绝:cloudcode-pa 上的配额 RPC 对个人账号返回 403「You do not have a valid license of this product」,因为这个 API 会判断究竟是哪个客户端在问。Antigravity 自己的窗口有同样的问题、也用同样的办法绕开,于是这个应用照做:从进程表里找到语言服务器的 pid,从它的命令行上读出 --csrf_token,用 lsof 找出它监听的端口,然后往回环上 POST {"forceRefresh":true} 到 /exa.language_server_pb.LanguageServerService/RetrieveUserQuotaSummary,带上那个要从二进制里才试出来的头 x-codeium-csrf-token。

  4. 04

    它多久问一次,以及被叫停时怎么办

    UsageStore 每 15 秒走一格,在有会话忙的时候每 30 秒刷新一次,闲下来降到五分钟——因为没人用的时候用量根本不会动。有三个事件会在计划之外发问:会话停下来、菜单栏项被打开、指针落到某个圆环上;最后这一种做了间隔控制,四秒内划过四个圆环只算一次请求。Anthropic 的端点在限流时返回 429 并附上 Retry-After: 0,照字面执行就是「等零秒」,于是刚被限流就立刻再撞上去;现在这个提示只被允许抬高一个下限——从 60 秒起,每连续一次 429 就翻倍,上限 15 分钟——而且这个截止时间会被持久化,所以在惩罚期内重启应用是等待,而不是再花掉一次尝试。作者自己在那条记录里写得很直白:那个 make run 的开发循环,正是维持这场惩罚的元凶。429 被渲染成「数据陈旧」而不是错误,而被记住的读数也从不会被当作实时读数展示——它回来时是暗的、带日期的。

  5. 05

    作者写下来的 bug,包括一个他自己搞错的

    代码注释和任务史里留着一批各花掉一天的失败。用 immutable=1 打开 Cursor 或 Codex 的 SQLite 会让它忽略 write-ahead log,于是正在干活的 agent 看起来是闲的、已经轮换过的 token 看起来还有效;而重启之后 -shm 旁挂文件消失,只读打开会直接失败——这就是它在一台有 2,752 个线程的机器上报告「本机没有 Codex 线程」的原因。Claude 的会话文件把 procStart 写成 UTC 的 ctime 字符串,按本地时间解析会让 pid 复用守卫差出七个小时,活动指示器于是静默地从不出现。圆环的主标题原本取数组里的第一个窗口,而 Claude 在会话窗口一滚过就会把 five_hour 从数组里拿掉,于是在最有人盯着的那个时刻,每周数字悄悄滑进了会话的位置;现在主标题是具名的而不是位置式的,指定窗口真的不在时显示一道横杠。而在 dmg 安装包那个 issue 上,此前把原因归到下载缓存的那位贡献者公开更正了自己——「I was wrong about the cause」——随后又贴出他自己重打包三次、三次都出现的第三种状态。

  6. 06

    一个 macOS 应用、一个 Rust 移植,和五十个账号

    贡献者列表里有五十个账号,作者占 387 次提交,紧随其后的两个是 58 次和 57 次。Windows 那边不是把代码搬过去:windows/ 是一个独立的 Rust + Tauri 2 应用,按同一套线上格式重写每一个 provider,有自己的 i18n 文件、自己的安装器和自己的打包工作流——而那三个把 notch 沿 Windows 屏幕边框拖动的 PR,全都落在这里。它周围的队列混合着移植和争论。有一位贡献者用了三个 PR 争论菜单栏项该不该预留宽度,最后不是靠口味而是靠数字定的——单个 provider 每个五小时窗口会重排五次,大约每小时一次——并在最后一个 PR 里推翻了自己先前的立场。另一个人把这一版仍在显示英文的中文字串补齐,繁体中文按台湾用词单独处理。还有一个人发现经由 Tailscale 地址访问的 Custom Endpoint 会卡在 App Transport Security 上,于是补了一条只覆盖 100.64.0.0/10 的例外。月底时开着 54 个 issue。而常驻应用同样没有窗口可以打印东西,失败都进统一日志,作者也记下:最早那几次诊断,是照着截图猜出来的。

相关档案

全部档案 →