diff --git a/CHANGELOG.md b/CHANGELOG.md index b334e12..64230ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ TokenStep 各版本的更新内容,新版本在前。每个版本的完整说明在 [docs/RELEASE_NOTES_<版本号>.md](docs/)。 +## 0.2.19 更新:额度与排名更精简 + +额度条粗细统一,Cursor 模型额度也有时间进度竖线;排名只在前 10 名时显示前三名,改为显示离你最近的后一名;大数字的单位更紧凑,页脚更简洁。详见 [0.2.19 发布说明](docs/RELEASE_NOTES_0.2.19.md)。 + ## 0.2.18 更新:额度预警写明窗口 额度提前用完的提示会写出是哪个窗口,例如「7 天额度会提前用完」,不再和快要重置的 5 小时窗口混淆;面板里的排名可以直接打开 Token Rank 榜单和个人主页。详见 [0.2.18 发布说明](docs/RELEASE_NOTES_0.2.18.md)。 diff --git a/README.md b/README.md index c0c4b92..bc4df85 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ TokenStep 是一个 macOS 菜单栏 App,用来本地统计你在 Codex、Claud 下载最新版 DMG,打开后把 `TokenStep.app` 拖进「应用程序」即可使用: -[下载 TokenStep 最新版](https://github.com/Backtthefuture/TokenStep/releases/latest/download/TokenStep-0.2.18.dmg) +[下载 TokenStep 最新版](https://github.com/Backtthefuture/TokenStep/releases/latest/download/TokenStep-0.2.19.dmg) 也可以从 Release 页面查看所有版本: @@ -32,9 +32,9 @@ TokenStep 已使用 Developer ID 签名并通过 Apple 公证。首次打开时 Windows版本由十七做了移植,欢迎大家前往使用:https://github.com/canyexuanfan/TokenStep-Windows/releases -## 最新版本:0.2.18 +## 最新版本:0.2.19 -额度提前用完的提示会写出是哪个窗口,例如「7 天额度会提前用完」;面板里的排名可以直接打开 Token Rank 网页。0.2.16 起菜单栏面板改为一列紧凑面板,额度刷新也更及时。详见 [0.2.18 发布说明](docs/RELEASE_NOTES_0.2.18.md)。 +面板的额度和排名更精简:额度条粗细统一,排名只在前 10 名时显示前三名,并显示离你最近的后一名。0.2.16 起菜单栏面板改为一列紧凑面板,额度刷新更及时。详见 [0.2.19 发布说明](docs/RELEASE_NOTES_0.2.19.md)。 历次更新(主题包、引力动效、自动更新等)见 [CHANGELOG.md](CHANGELOG.md)。 @@ -95,7 +95,7 @@ TokenStep 默认只做本地统计。 ## 安装方式 -1. 下载 [TokenStep 最新版 DMG](https://github.com/Backtthefuture/TokenStep/releases/latest/download/TokenStep-0.2.18.dmg)。 +1. 下载 [TokenStep 最新版 DMG](https://github.com/Backtthefuture/TokenStep/releases/latest/download/TokenStep-0.2.19.dmg)。 2. 打开 DMG。 3. 把 `TokenStep.app` 拖到「应用程序」。 4. 启动 TokenStep。 @@ -166,7 +166,7 @@ TokenStepSwift/dist/TokenStep.app 公开发布强制执行 Developer ID 签名、Apple 公证、票据装订、系统分发检查和隔离安装验证。不再生成可被误上传的“仅签名、未公证”发布包: ```bash -TOKENSTEP_VERSION=0.2.18 \ +TOKENSTEP_VERSION=0.2.19 \ CODE_SIGN_IDENTITY="Developer ID Application: Your Name (TEAMID)" \ TOKENSTEP_NOTARY_PROFILE="tokenstep-notary" \ ./script/package_release.sh --notarize diff --git a/TokenStepSwift/Sources/TokenStepSwift/Models/QuotaModels.swift b/TokenStepSwift/Sources/TokenStepSwift/Models/QuotaModels.swift index b4071ee..c72e78e 100644 --- a/TokenStepSwift/Sources/TokenStepSwift/Models/QuotaModels.swift +++ b/TokenStepSwift/Sources/TokenStepSwift/Models/QuotaModels.swift @@ -53,7 +53,7 @@ enum QuotaWindowKind: String, Codable, Equatable { case .monthlyCredits: return L("本月额度") case .tokenWindow: return L("Token 窗") case .spend: return L("花费") - case .cursorModels: return L("Cursor 模型") + case .cursorModels: return L("自带") case .otherModels: return L("其他模型") } } @@ -67,7 +67,7 @@ enum QuotaWindowKind: String, Codable, Equatable { case .monthlyCredits: return L("本月") case .tokenWindow: return L("Token") case .spend: return L("花费") - case .cursorModels: return L("自有") + case .cursorModels: return L("自带") case .otherModels: return L("其他") } } diff --git a/TokenStepSwift/Sources/TokenStepSwift/Support/Localization.swift b/TokenStepSwift/Sources/TokenStepSwift/Support/Localization.swift index 5ecd824..6060dbf 100644 --- a/TokenStepSwift/Sources/TokenStepSwift/Support/Localization.swift +++ b/TokenStepSwift/Sources/TokenStepSwift/Support/Localization.swift @@ -360,8 +360,6 @@ enum TokenStepLocalization { "保存浮层 PNG": "Save popover PNG", "截取浮层": "Capture popover", "细线是每日目标": "Thin line marks the daily goal", - "手动刷新": "Manual refresh", - "刷新 %@": "Refresh %@", "打开仪表盘": "Open Dashboard", "退出": "Quit", "青绿": "Green", @@ -498,7 +496,6 @@ enum TokenStepLocalization { "再跑": "Run", "分享": "Share", "另有 %d 项正常:%@": "%d more, all normal: %@", - "差 %@": "%@ ahead", "已用": "Used", "已用 · %@": "Used · %@", "引力动效": "Gravity motion", @@ -513,7 +510,6 @@ enum TokenStepLocalization { "今日还没有你的上传": "No uploads from you today yet", "本地比榜单多 %@,同步后预计升到 #%d": "%@ more locally than on the board; about #%d after sync", "本地比榜单多 %@,等待同步": "%@ more locally than on the board, waiting for sync", - "榜上 %@": "On board %@", "榜单 %@": "Board %@", "照此速度会提前用完": "Runs out early at this pace", "%@额度会提前用完": "%@ runs out early", @@ -528,6 +524,8 @@ enum TokenStepLocalization { "进入前 %d 名": "to enter the top %d", "退出 TokenStep": "Quit TokenStep", "领先第二名 %@": "%@ ahead of #2", + "身后 #%d %@": "Behind you: #%d %@", + "领先 %@": "%@ ahead", "近似 · %d 会话": "Approx. · %d sessions", "近似值(按会话开始日汇总)": "Approximate (totals by session start day)", "版本": "Ver", @@ -573,9 +571,8 @@ enum TokenStepLocalization { "需要 Coding Plan 的 ZAI_API_KEY": "Needs a Coding Plan ZAI_API_KEY", "需要 ~/.kimi 的 OAuth,不要用开放平台 key": "Needs ~/.kimi OAuth, not a Moonshot platform key", "需要 grok login 产生的 ~/.grok/auth.json": "Needs ~/.grok/auth.json from grok login", - "Cursor 模型": "Cursor models", "其他模型": "Other models", - "自有": "Included", + "自带": "Included", "圈数进度,颜色不按来源分段": "Lap progress only. Color is not split by source.", "Cursor 无本地 token,额度见右栏": "Cursor has no local tokens. See quotas on the right.", "Cursor 无本地 token,不在此列": "Cursor has no local tokens, so it is not listed here.", @@ -1085,8 +1082,6 @@ enum TokenStepLocalization { "保存浮层 PNG": "儲存浮層 PNG", "截取浮层": "擷取浮層", "细线是每日目标": "細線是每日目標", - "手动刷新": "手動重新整理", - "刷新 %@": "重新整理 %@", "打开仪表盘": "開啟儀表板", "退出": "退出", "青绿": "青綠", @@ -1196,7 +1191,6 @@ enum TokenStepLocalization { "再跑": "再跑", "分享": "分享", "另有 %d 项正常:%@": "另有 %d 項正常:%@", - "差 %@": "差 %@", "已用": "已用", "已用 · %@": "已用 · %@", "引力动效": "引力動效", @@ -1211,7 +1205,6 @@ enum TokenStepLocalization { "今日还没有你的上传": "今日還沒有你的上傳", "本地比榜单多 %@,同步后预计升到 #%d": "本機比榜單多 %@,同步後預計升到 #%d", "本地比榜单多 %@,等待同步": "本機比榜單多 %@,等待同步", - "榜上 %@": "榜上 %@", "榜单 %@": "榜單 %@", "照此速度会提前用完": "照此速度會提前用完", "%@额度会提前用完": "%@額度會提前用完", @@ -1226,6 +1219,8 @@ enum TokenStepLocalization { "进入前 %d 名": "進入前 %d 名", "退出 TokenStep": "結束 TokenStep", "领先第二名 %@": "領先第二名 %@", + "身后 #%d %@": "身後 #%d %@", + "领先 %@": "領先 %@", "近似 · %d 会话": "近似 · %d 會話", "近似值(按会话开始日汇总)": "近似值(按會話開始日彙總)", "版本": "版本", @@ -1296,9 +1291,8 @@ enum TokenStepLocalization { "清除密钥只在你往钥匙串里存过内容时才出现。Grok 正常走 ~/.grok/auth.json,一般看不到这个按钮。": "清除密鑰只在你往鑰匙圈裡存過內容時才出現。Grok 正常走 ~/.grok/auth.json,一般看不到這個按鈕。", "Grok Build 弹出的短码是给终端用的。贴回正在跑 grok login 的窗口,等它显示成功,再点重新检测。": "Grok Build 彈出的短碼是給終端用的。貼回正在跑 grok login 的視窗,等它顯示成功,再點重新檢測。", "一般不用填,短码不要贴到这里": "一般不用填,短碼不要貼到這裡", - "Cursor 模型": "Cursor 模型", "其他模型": "其他模型", - "自有": "自有", + "自带": "自帶", "圈数进度,颜色不按来源分段": "圈數進度,顏色不按來源分段", "Cursor 无本地 token,额度见右栏": "Cursor 無本地 token,額度見右欄", "Cursor 无本地 token,不在此列": "Cursor 無本地 token,不在此列", diff --git a/TokenStepSwift/Sources/TokenStepSwift/Views/Popover/CompactPopoverModel.swift b/TokenStepSwift/Sources/TokenStepSwift/Views/Popover/CompactPopoverModel.swift index d66ed8c..28a50d9 100644 --- a/TokenStepSwift/Sources/TokenStepSwift/Views/Popover/CompactPopoverModel.swift +++ b/TokenStepSwift/Sources/TokenStepSwift/Views/Popover/CompactPopoverModel.swift @@ -143,9 +143,10 @@ enum CompactPopoverModel { return resetsAt.addingTimeInterval(-5 * 60 * 60) case .sevenDay, .weekly: return resetsAt.addingTimeInterval(-7 * 24 * 60 * 60) - case .monthlyCredits: + case .monthlyCredits, .cursorModels, .otherModels: + // Cursor's model pools reset with its monthly billing cycle. return calendar.date(byAdding: .month, value: -1, to: resetsAt) - case .session, .tokenWindow, .spend, .cursorModels, .otherModels: + case .session, .tokenWindow, .spend: return nil } } @@ -198,14 +199,23 @@ enum CompactPopoverModel { /// Token Rank's public board counts days in this zone. static let rankBoardTimeZoneIdentifier = "Asia/Shanghai" + /// The podium is only worth showing to viewers near it; further down the + /// board the gap to the top is too large to act on. + static let podiumRankLimit = 10 + struct Podium: Equatable, Identifiable { var id: Int { rank } var rank: Int var name: String var tokens: Int var isMe: Bool - /// How far ahead of the viewer this entry is; nil when unknown or not ahead. - var leadOverMe: Int? + } + + /// The closest entry behind the viewer, and by how much the viewer leads it. + struct Behind: Equatable { + var rank: Int + var name: String + var lead: Int } enum Chase: Equatable { @@ -222,8 +232,10 @@ enum CompactPopoverModel { var rankedUsers: Int var listedLimit: Int var myBoardTokens: Int? + /// Empty unless the viewer ranks within `podiumRankLimit`. var podium: [Podium] var chase: Chase? + var behind: Behind? /// Minutes to close the chase gap at the last hour's pace. var etaMinutes: Int? var tokensPerHour: Int @@ -242,29 +254,32 @@ enum CompactPopoverModel { ) -> RankSection { let entries = board.entries.sorted { $0.rank < $1.rank } let mine = myUserID.flatMap { id in entries.first { $0.userID == id } } - let podium = entries.prefix(3).map { entry in - Podium( - rank: entry.rank, - name: entry.name, - tokens: entry.totalTokens, - isMe: entry.userID == mine?.userID, - leadOverMe: mine.flatMap { me in - entry.rank < me.rank ? max(entry.totalTokens - me.totalTokens, 0) : nil - } - ) - } + let podium = (mine.map { $0.rank <= podiumRankLimit } ?? false) + ? entries.prefix(3).map { entry in + Podium(rank: entry.rank, name: entry.name, tokens: entry.totalTokens, isMe: entry.userID == mine?.userID) + } + : [] let sameDay = localTimeZoneIdentifier == rankBoardTimeZoneIdentifier var chase: Chase? + var behind: Behind? var gap: Int? if let mine { - if let next = entries.last(where: { $0.rank < mine.rank }) { - let needed = max(next.totalTokens - mine.totalTokens, 0) + 1 + // Local tokens run ahead of the board between uploads; on the same + // day they are the truer count to measure gaps from. + let myTokens = sameDay ? max(mine.totalTokens, localTodayTokens) : mine.totalTokens + let others = entries.filter { $0.userID != mine.userID } + if let next = others.last(where: { $0.totalTokens >= myTokens }) { + let needed = next.totalTokens - myTokens + 1 gap = needed - let progress = next.totalTokens > 0 ? Double(mine.totalTokens) / Double(next.totalTokens) : 0 + let progress = next.totalTokens > 0 ? Double(myTokens) / Double(next.totalTokens) : 0 chase = .next(rank: next.rank, name: next.name, gap: needed, progress: min(max(progress, 0), 1)) - } else if let second = entries.first(where: { $0.rank > mine.rank }) { - chase = .leading(lead: max(mine.totalTokens - second.totalTokens, 0)) + } else if let second = others.first { + chase = .leading(lead: max(myTokens - second.totalTokens, 0)) + } + // First place already shows its lead over second. + if case .next = chase, let below = others.first(where: { $0.totalTokens < myTokens }) { + behind = Behind(rank: below.rank, name: below.name, lead: myTokens - below.totalTokens) } } else if myUserID != nil, let last = entries.last, entries.count >= board.topLimit { // Not listed: the board only returns the top `topLimit`. Local tokens are @@ -296,6 +311,7 @@ enum CompactPopoverModel { myBoardTokens: mine?.totalTokens, podium: podium, chase: chase, + behind: behind, etaMinutes: eta, tokensPerHour: tokensPerHour, unsyncedTokens: unsynced, diff --git a/TokenStepSwift/Sources/TokenStepSwift/Views/Popover/CompactPopoverSections.swift b/TokenStepSwift/Sources/TokenStepSwift/Views/Popover/CompactPopoverSections.swift index 02bed58..9bf2d1a 100644 --- a/TokenStepSwift/Sources/TokenStepSwift/Views/Popover/CompactPopoverSections.swift +++ b/TokenStepSwift/Sources/TokenStepSwift/Views/Popover/CompactPopoverSections.swift @@ -32,6 +32,21 @@ struct QuotaProviderIcon: View { .stroke(Color.black.opacity(0.08), lineWidth: 0.5) } .accessibilityHidden(true) + } else if provider == .grok { + // Black tile with the white mark, like Grok's app icon. + RoundedRectangle(cornerRadius: size * 0.25, style: .continuous) + .fill(Color.black) + .frame(width: size, height: size) + .overlay { + SVGPathShape(pathData: ProviderMark.grokPath, viewBox: ProviderMark.grokViewBox) + .fill(Color.white, style: FillStyle(eoFill: true)) + .padding(size * 0.2) + } + .overlay { + RoundedRectangle(cornerRadius: size * 0.25, style: .continuous) + .stroke(Color.white.opacity(0.12), lineWidth: 0.5) + } + .accessibilityHidden(true) } else { RoundedRectangle(cornerRadius: size * 0.25, style: .continuous) .fill(AgentSourceRegistry.color(for: provider.rawValue)) @@ -97,6 +112,22 @@ enum CompactPopoverStyle { level == .normal ? Color.tokenInk : color(for: level) } + /// A large compact token count. CJK units carry a wide left side bearing + /// that reads as a space after big digits, so the unit is set smaller and + /// pulled in to sit against them. + static func tokens(_ value: Int, size: CGFloat, weight: Font.Weight, design: Font.Design) -> some View { + let text = TokenStepFormat.tokens(value, compact: true) + let digits = String(text.prefix { $0.isASCII }) + let unit = String(text.dropFirst(digits.count)) + return HStack(alignment: .firstTextBaseline, spacing: unit.isEmpty ? 0 : -size * 0.08) { + Text(digits).font(.system(size: size, weight: weight, design: design)) + if !unit.isEmpty { + Text(unit).font(.system(size: size * 0.8, weight: weight)) + } + } + .accessibilityElement(children: .combine) + } + static func percent(_ value: Double) -> String { "\(Int(value.rounded()))%" } @@ -187,14 +218,17 @@ private struct CompactBar: View { Capsule() .fill(color) .frame(width: max(proxy.size.width * min(max(fraction, 0), 1), fraction > 0 ? height : 0)) + } + // The marker overhangs the bar; drawn over it so a bar with a + // marker keeps the same thickness as one without. + .overlay(alignment: .topLeading) { if let marker { RoundedRectangle(cornerRadius: 1) .fill(Color.tokenInk.opacity(0.55)) .frame(width: 2, height: height + 6) - .offset(x: proxy.size.width * min(max(marker, 0), 1) - 1) + .position(x: proxy.size.width * min(max(marker, 0), 1), y: proxy.size.height / 2) } } - .frame(height: proxy.size.height) } .frame(height: height) } @@ -349,9 +383,7 @@ struct CompactTokenSection: View { Text(L("今日 Token")) .font(.system(size: 11)) .foregroundStyle(.secondary) - Text(TokenStepFormat.tokens(usage.totalTokens, compact: true)) - .font(.system(size: 24, weight: .bold, design: .rounded)) - .monospacedDigit() + CompactPopoverStyle.tokens(usage.totalTokens, size: 24, weight: .bold, design: .rounded) .foregroundStyle(Color.tokenInk) } Spacer(minLength: 4) @@ -370,7 +402,7 @@ struct CompactTokenSection: View { .font(.system(size: 12)) .foregroundStyle(Color.tokenInk) .lineLimit(1) - .truncationMode(.middle) + .truncationMode(.tail) .frame(width: 112, alignment: .leading) CompactBar(fraction: model.share, color: Color.tokenGreen.opacity(0.85), height: 5) Text(TokenStepFormat.tokens(model.tokens, compact: true)) @@ -419,11 +451,6 @@ struct CompactRankSection: View { .font(.system(size: 12)) .foregroundStyle(.secondary) Spacer(minLength: 4) - if let board = section.myBoardTokens { - Text(LFormat("榜上 %@", TokenStepFormat.tokens(board, compact: true))) - .font(.system(size: 12)) - .foregroundStyle(.secondary) - } } .contentShape(Rectangle()) .onTapGesture { openMyPage?() } @@ -448,16 +475,26 @@ struct CompactRankSection: View { .font(.system(size: 12, weight: .semibold)) .monospacedDigit() .foregroundStyle(Color.tokenInk) - Text(entry.leadOverMe.map { LFormat("差 %@", TokenStepFormat.tokens($0, compact: true)) } ?? "") - .font(.system(size: 11)) - .foregroundStyle(.secondary) - .frame(width: 70, alignment: .trailing) } .accessibilityElement(children: .combine) } if let chase = section.chase { chaseCard(chase) } + if let behind = section.behind { + HStack(spacing: 4) { + Text(LFormat("身后 #%d %@", behind.rank, behind.name)) + .lineLimit(1) + .truncationMode(.tail) + Spacer(minLength: 4) + Text(LFormat("领先 %@", TokenStepFormat.tokens(behind.lead, compact: true))) + .monospacedDigit() + } + .font(.system(size: 11)) + .foregroundStyle(.secondary) + .padding(.horizontal, 12) + .accessibilityElement(children: .combine) + } } } @@ -551,9 +588,7 @@ struct CompactRankSection: View { HStack(alignment: .firstTextBaseline, spacing: 4) { Text(L("再跑")) .font(.system(size: 12, weight: .medium)) - Text(TokenStepFormat.tokens(gap, compact: true)) - .font(.system(size: 13, weight: .bold, design: .rounded)) - .monospacedDigit() + CompactPopoverStyle.tokens(gap, size: 13, weight: .bold, design: .rounded) Text(target) .font(.system(size: 12, weight: .medium)) .lineLimit(1) diff --git a/TokenStepSwift/Sources/TokenStepSwift/Views/Popover/ProviderMark.swift b/TokenStepSwift/Sources/TokenStepSwift/Views/Popover/ProviderMark.swift index 510bd69..bc32243 100644 --- a/TokenStepSwift/Sources/TokenStepSwift/Views/Popover/ProviderMark.swift +++ b/TokenStepSwift/Sources/TokenStepSwift/Views/Popover/ProviderMark.swift @@ -18,6 +18,18 @@ enum ProviderMark { "4-3 1.4-6.2 1.6-8 1.3-6.4 1.2-7.9.7-2.6v-.2H49L43 72l-9 12.3-7.2 7.6-1.7.7-3-1.5.3-2.8L24 86l10-" + "12.8 6-7.9 4-4.6-.1-.5h-.3L17.2 77.4l-4.7.6-2-2 .2-3 1-1 8-5.5Z" static let claudeColor = Color(red: 217 / 255, green: 119 / 255, blue: 87 / 255) + + /// The Grok symbol, in a 24×24 view box, with its arcs converted to cubic + /// curves for `SVGPathShape`. Fill with the even-odd rule. + static let grokPath = + "M9.27 15.29L17.248 9.393C17.639 9.103 18.198 9.216 18.385 9.665C19.365 12.034 18.927 14.88 16.975 16.834C" + + "15.024 18.788 12.308 19.216 9.826 18.24L7.115 19.497C11.004 22.158 15.726 21.5 18.677 18.544C21.018 16.2 " + + "21.743 13.005 21.065 10.124L21.071 10.131C20.088 5.899 21.313 4.207 23.821 0.748C23.881 0.666 23.941 0.584 " + + "24 0.5L20.699 3.805L20.699 3.795L9.267 15.292M7.623 16.723C4.831 14.053 5.313 9.922 7.694 7.539C9.455 5.776 " + + "12.341 5.056 14.86 6.114L17.565 4.864C17.003 4.45 16.388 4.114 15.736 3.864C14.648 3.419 13.484 3.192 12.308 " + + "3.195C11.133 3.199 9.97 3.433 8.885 3.885C7.8 4.337 6.814 4.998 5.984 5.83C3.451 8.366 2.654 12.266 4.022 " + + "15.594C5.044 18.081 3.369 19.84 1.682 21.616C1.083 22.246 0.483 22.875 0 23.541L7.62 16.726" + static let grokViewBox = CGSize(width: 24, height: 24) } /// Fills an SVG path string scaled into the view's frame. Supports the diff --git a/TokenStepSwift/Sources/TokenStepSwift/Views/PopoverPanelView.swift b/TokenStepSwift/Sources/TokenStepSwift/Views/PopoverPanelView.swift index d6e6cc1..fd9980c 100644 --- a/TokenStepSwift/Sources/TokenStepSwift/Views/PopoverPanelView.swift +++ b/TokenStepSwift/Sources/TokenStepSwift/Views/PopoverPanelView.swift @@ -170,13 +170,6 @@ struct PopoverPanelView: View { Spacer(minLength: 4) - Text(appState.settings.refreshIntervalSeconds == 0 - ? L("手动刷新") - : LFormat("刷新 %@", TokenStepFormat.intervalLabel(appState.settings.refreshIntervalSeconds))) - .font(.system(size: 11)) - .foregroundStyle(.secondary) - .lineLimit(1) - if appState.availableUpdate != nil || appState.isCheckingForUpdates { let update = appState.updateActionVisualState Button { diff --git a/TokenStepSwift/Tests/Fixtures/CompactPopoverFixtureCheck.swift b/TokenStepSwift/Tests/Fixtures/CompactPopoverFixtureCheck.swift index 5acbc1a..7d032e0 100644 --- a/TokenStepSwift/Tests/Fixtures/CompactPopoverFixtureCheck.swift +++ b/TokenStepSwift/Tests/Fixtures/CompactPopoverFixtureCheck.swift @@ -94,7 +94,8 @@ struct CompactPopoverFixtureCheck { private static func checkWindowLengths() throws { try expect(row(.session, used: 50, resetsIn: hours(1)).elapsedFraction == nil, "unknown session length has no marker") - try expect(row(.cursorModels, used: 50, resetsIn: hours(1)).elapsedFraction == nil, "cursor model window has no marker") + let cursor = row(.cursorModels, used: 50, resetsIn: hours(24)).elapsedFraction ?? 0 + try expect(cursor > 0.9 && cursor < 1, "cursor model window follows the monthly billing cycle: \(cursor)") try expect(row(.fiveHour, used: 50, resetsIn: nil).elapsedFraction == nil, "no reset time has no marker") // Resets 2026-10-01 00:00 Shanghai; the window began 2026-09-01 00:00. let reset = ISO8601DateFormatter().date(from: "2026-09-30T16:00:00Z")! @@ -150,7 +151,8 @@ struct CompactPopoverFixtureCheck { try expect(rank == 34 && name == "Next" && gap == 4_120_001, "gap to #34 is \(gap)") try expect(abs(progress - 28_510_000.0 / 32_630_000) < 1e-9, "progress toward #34") try expect(section.etaMinutes == 26, "ETA rounds up: \(String(describing: section.etaMinutes))") - try expect(section.podium.map(\.leadOverMe) == [831_490_000, 687_490_000, 637_490_000], "podium leads over me") + try expect(section.podium.isEmpty, "no podium outside the top \(CompactPopoverModel.podiumRankLimit)") + try expect(section.behind == .init(rank: 36, name: "Below", lead: 8_510_000), "closest entry behind: \(String(describing: section.behind))") try expect(section.unsyncedTokens == nil && section.projectedRank == nil, "different days are not compared") } @@ -161,7 +163,8 @@ struct CompactPopoverFixtureCheck { ) try expect(section.chase == .leading(lead: 300), "first place shows its lead") try expect(section.etaMinutes == nil, "no ETA when leading") - try expect(section.podium.first?.isMe == true && section.podium.first?.leadOverMe == nil, "podium marks me") + try expect(section.podium.map(\.rank) == [1, 2, 3] && section.podium.first?.isMe == true, "podium near the top marks me") + try expect(section.behind == nil, "first place shows its lead instead of who is behind") } private static func checkOutsideBoard() throws { @@ -194,6 +197,12 @@ struct CompactPopoverFixtureCheck { ) try expect(section.unsyncedTokens == 150, "unsynced local tokens") try expect(section.projectedRank == 3, "350 would place third: \(String(describing: section.projectedRank))") + // Gaps count the local total, not the board's stale one. + guard case let .next(rank, _, gap, _) = section.chase else { + throw FixtureError("expected a next-rank chase, got \(String(describing: section.chase))") + } + try expect(rank == 2 && gap == 51, "local 350 chases #2 at 400: #\(rank) gap \(gap)") + try expect(section.behind == .init(rank: 3, name: "C", lead: 50), "local 350 leads #3: \(String(describing: section.behind))") let behind = CompactPopoverModel.rankSection( board: board, myUserID: 99, localTodayTokens: 180, tokensPerHour: 0, localTimeZoneIdentifier: "Asia/Shanghai" ) diff --git a/docs/RELEASE_NOTES_0.2.19.md b/docs/RELEASE_NOTES_0.2.19.md new file mode 100644 index 0000000..486d6dc --- /dev/null +++ b/docs/RELEASE_NOTES_0.2.19.md @@ -0,0 +1,11 @@ +# TokenStep 0.2.19 + +面板的额度和排名更精简,重点更突出。 + +- **额度条统一粗细**:带时间进度竖线的额度条和不带竖线的一样粗。Cursor 的两类模型额度按每月账单周期显示时间进度竖线。 +- **名称与图标**:Cursor 自有模型的额度改名为「自带」;没有安装 Grok App 时,额度区也显示 Grok 图标。 +- **排名更聚焦**:只有你在前 10 名时才显示前三名;去掉每行的「差多少」和「榜上」总数,改为显示离你最近的后一名以及你领先多少。北京时间下,追赶差距会把还没同步到榜单的本地用量算进去。 +- **数字更紧凑**:大数字后的「亿」「万」字号略小,紧贴数字。 +- **页脚更简洁**:去掉刷新间隔的文字。 + +Token、金额与额度的统计口径不变。 diff --git a/script/build_swiftui_and_run.sh b/script/build_swiftui_and_run.sh index dd3e07b..ac50204 100755 --- a/script/build_swiftui_and_run.sh +++ b/script/build_swiftui_and_run.sh @@ -23,7 +23,7 @@ HELPER_EXECUTABLE="$BUILD_DIR/$HELPER_NAME" ICON_FILE="$ROOT_DIR/TokenUsageMenuApp/assets/TokenStepIcon.icns" ODYSSEY_ASSET_DIR="$ROOT_DIR/TokenUsageMenuApp/assets/odyssey" INTERSTELLAR_ASSET_DIR="$ROOT_DIR/TokenUsageMenuApp/assets/interstellar" -VERSION="${TOKENSTEP_VERSION:-0.2.18}" +VERSION="${TOKENSTEP_VERSION:-0.2.19}" LAUNCH=true VERIFY=false