Skip to content

feat(chat): show the plan usage reset countdown below the composer - #939

Open
nnnmdzz wants to merge 6 commits into
spacering-net:mainfrom
nnnmdzz:feat/plan-limit-resets
Open

nnnmdzz wants to merge 6 commits into
spacering-net:mainfrom
nnnmdzz:feat/plan-limit-resets

Conversation

@nnnmdzz

@nnnmdzz nnnmdzz commented Oct 13, 2026

Copy link
Copy Markdown

背景

用订阅登录(Claude Pro/Max、ChatGPT 套餐下的 Codex)时,用量按 5 小时窗口和每周窗口计算。codeg 里看不到用了多少、什么时候重置,额度用完只能去官网或 CLI 里查。

这些数字 agent 自己就有,不需要额外请求:

  • Claude Code 读取它自己 API 响应里的 unified rate-limit 头,SDK 以 rate_limit_event 抛出,claude-agent-acp 把它放在 usage_update 的 _meta["_claude/rateLimit"] 里转发(status、resetsAt、rateLimitType、utilization,CLI 提供时还有各窗口的 unifiedWindows)。
  • Codex 每次 token_count 都把 rate_limits(primary/secondary:used_percent、window_minutes、resets_at)写进 rollout。

codeg 目前丢弃前者,也没有读后者。

修改

后端

  • 新增 PlanLimitWindow { kind, used_percent, resets_at, window_minutes, status } 与 AcpEvent::PlanLimits,两种来源都在 acp::plan_limits 解析:Claude 的 utilization 是 0–1 的比例,转成百分比;毫秒时间戳按秒处理;旧版 Codex rollout 只有 resets_in_seconds,按该行的时间戳换算。Codex 的窗口按长度命名(300 分钟为 five_hour,10080 分钟为 seven_day),所以只有周窗口的套餐也显示为每周。
  • Claude 的报告进入 SessionState.plan_limits(按 kind 合并)和 LiveSessionSnapshot;Codex 的 rollout 解析器把最后一次 token_count 记录的窗口写入 SessionStats.plan_limits。
  • 只接受更新的数字:同一窗口,resets_at 更晚即是新窗口,直接替换;resets_at 相同时用量只增不减,取不低于现值的那份(用量相同但状态变化也接受);其余视为过时报告,忽略。较慢的连接或较早的 snapshot 不会把数字倒回去。前端合并用同一规则。
  • 按账号共享:额度属于账号而不是会话。同一 agent、启动配置相同(config_fingerprint 相同,即同一登录或同一 provider)的活跃连接共用一份:新连接从它们已知的数字开始;任一连接报告新数字时,plan_limits_subscriber_task(订阅内部事件总线)把比各连接现有更新的部分转给其它连接,经各自的事件流发出,已连接的客户端都能收到。配置不同的连接互不共享,所以切到 API key 或第三方 provider 的会话不会显示订阅的数字。桌面和 server 两种模式启动时都会 spawn 该任务。

前端

  • 连接 store 新增 planLimits,处理 plan_limits 事件;hydrate 的 fresh/stale 两条分支都按上述规则合并。
  • 输入框下方状态列(上下文用量左侧)新增 ComposerPlanLimits:沙漏图标加上最接近上限的窗口还有多久重置(已达上限优先,其次用量最高,并列取更早重置的)。80% 起或 Claude 报 allowed_warning 时为琥珀色,达到上限为红色。点开列出每个窗口的用量、进度条、剩余时间和本地重置时刻。桌面端和移动端都在这一列;移动端分支名照常截断让出空间。
  • 已过重置时间的窗口不再显示旧用量;没有任何窗口(API key 登录)或全部已重置时不渲染。Codex 会话从 session stats 读取。
  • ComposerContextUsage 读取当前 tab 的连接和 session stats 的逻辑提取到 use-composer-session,两个指示器共用,行为不变(单独一个 refactor commit)。
  • 10 种语言的文案。

刻意不做的:

  • 不请求任何接口(包括 /api/oauth/usage),也不读取凭据,只显示 agent 已经报告过的数字。所以同账号还没有任何报告之前不显示。claude-agent-acp 只在本回合已有用量数据之后才转发 rate_limit_event,数字在回复过程中出现和更新。
  • Codex 只读 rollout:codex-acp 目前不转发 account/rateLimits/updated,所以 Codex 的数字在回复写入 rollout 后更新,也不跨会话共享。

验证

  • cargo test --no-default-features --features server-bin --lib:新增 13 条,覆盖 Claude 的两种载荷形状、比例与毫秒换算、Codex 窗口命名与旧版 resets_in_seconds、过时报告不回滚、同配置连接之间的转发(不同配置、不同 agent 不受影响)、新连接的初始值、snapshot 和 rollout 解析,全部通过。本机环境下(以 root 运行等)有 10 条既有用例失败,在 main 上同样是这 10 条,与本改动无关。
  • cargo clippy --no-default-features --features server-bin --bin codeg-server --lib -- -D warnings
  • pnpm lint .、pnpm test(606 个文件 / 8947 条)、./node_modules/.bin/tsc --noEmit、prettier。前端新增 22 条:合并规则、颜色与选取、snapshot、store 的事件与 hydrate、组件渲染。
  • 端到端:本地 codeg-server 加静态导出,用一个模拟 claude-agent-acp 的 agent 在回复后发出带 _meta["_claude/rateLimit"] 的 usage_update,桌面(1440×900)和移动端(390×844)各跑一遍:回复前不显示;回复后显示 5 小时窗口的倒计时,弹层列出两个窗口;第二个新会话发送前就带着同样的数字;第二个会话报告更高用量后,所有活跃连接(9 条,其中 8 条自己没报告过这个数字)的 snapshot 都更新为该数字。另在繁体中文界面下检查了移动端布局。

Claude Code reads the unified rate-limit headers of its own API
responses and claude-agent-acp forwards them on a usage_update as
`_meta["_claude/rateLimit"]`; Codex writes the same picture into its
rollout on every token_count (`rate_limits`). Neither was kept.

Both are parsed into `PlanLimitWindow`s (`acp::plan_limits`). Claude's
live reports become an `AcpEvent::PlanLimits`, held in `SessionState`
and the snapshot; the Codex rollout parser keeps the last windows it
recorded in `SessionStats.plan_limits`. A report replaces the held
window of its kind only when it is newer: a later reset time, or the
same reset time with at least as much used.

The windows belong to the account, so a new connection starts from what
the agent's live connections on the same settings (`config_fingerprint`)
already hold, and `plan_limits_subscriber_task` hands a report on one of
them to the others. Nothing is requested from any provider.
The context-usage indicator resolved its tab's connection and session
stats inline. Both reads move to `use-composer-session`, so the next
indicator in that row reads them the same way. No behavior change.
The row below the composer gets an hourglass with the time left until
the window closest to its limit resets: amber from 80% (or Claude's
`allowed_warning`), red at the limit. Its popover lists every window
with the share used and the local reset time. A window past its reset
no longer shows its old figure, and nothing renders for a sign-in
without a plan.

The connection store keeps `planLimits` from `plan_limits` events and
snapshots by the backend's rule, so a stale snapshot never rolls the
figures back. Codex sessions read theirs from the session stats.
…odex's own bucket

- A plan-limits event no longer counts as activity on the connection that
  applies it: most arrive forwarded from another connection of the account,
  so stamping last_activity_at kept idle connections (closed browser tabs)
  out of the idle sweep for as long as any connection kept reporting.
- The subscriber offers the reporter's current figures rather than the
  report's payload, so two reports that differ only in status (each
  superseding the other) settle instead of bouncing between the connections
  forever.
- Codex: only the account's own rate-limit bucket counts (limit_id "codex",
  or none in older rollouts). Codex writes the last bucket a response
  reported into token_count, which can be a model's separately metered one.
- Codex windows carry observed_at, the rollout line's time, and a first turn
  refused at the limit (no usage, no reply) still yields session stats to
  carry them.
- Docs: the config fingerprint does not see a sign-in the agent keeps itself.
…ures

- seven_day_overage_included is a per-model weekly window (Claude Code's
  "Fable limit"), not usage including extra credit: label it "Weekly · Fable"
  and drop the sevenDayOverage strings.
- The popover notes when recorded figures are over 15 minutes old (Codex's
  own staleness threshold), since a Codex conversation opened long after its
  last turn shows that turn's figures, and that sending a message refreshes
  them.
… the day

- The status-tie test now sends a later report once the connections settle
  and checks it still reaches the peer, so a subscriber that stopped after
  one forward fails it.
- Pin the 15-minute staleness threshold, and check that a note about figures
  recorded on another day names the day as well as the time.
@xintaofei

Copy link
Copy Markdown
Collaborator

codeg work task 334 is done — #939 (19 files, +430/-57).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants