Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ TokenStep 是一个 macOS 菜单栏 App,用来本地统计你在 Codex、Claud

下载最新版 DMG,打开后把 `TokenStep.app` 拖进「应用程序」即可使用:

[下载 TokenStep 最新版](https://github.com/Backtthefuture/TokenStep/releases/latest/download/TokenStep-0.1.45.dmg)
[下载 TokenStep 最新版](https://github.com/Backtthefuture/TokenStep/releases/latest/download/TokenStep-0.1.47.dmg)

也可以从 Release 页面查看所有版本:

Expand Down Expand Up @@ -89,7 +89,7 @@ TokenStep 默认只做本地统计。

## 安装方式

1. 下载 [TokenStep 最新版 DMG](https://github.com/Backtthefuture/TokenStep/releases/latest/download/TokenStep-0.1.45.dmg)。
1. 下载 [TokenStep 最新版 DMG](https://github.com/Backtthefuture/TokenStep/releases/latest/download/TokenStep-0.1.47.dmg)。
2. 打开 DMG。
3. 把 `TokenStep.app` 拖到「应用程序」。
4. 启动 TokenStep。
Expand Down Expand Up @@ -149,15 +149,15 @@ TokenStepSwift/dist/TokenStep.app
Developer ID 签名:

```bash
TOKENSTEP_VERSION=0.1.46 \
TOKENSTEP_VERSION=0.1.47 \
CODE_SIGN_IDENTITY="Developer ID Application: Your Name (TEAMID)" \
./script/package_release.sh
```

签名 + Apple 公证:

```bash
TOKENSTEP_VERSION=0.1.46 \
TOKENSTEP_VERSION=0.1.47 \
CODE_SIGN_IDENTITY="Developer ID Application: Your Name (TEAMID)" \
TOKENSTEP_NOTARY_PROFILE="tokenstep-notary" \
./script/package_release.sh --notarize
Expand Down
60 changes: 54 additions & 6 deletions TokenStepSwift/Sources/TokenStepSwift/Models/UsageModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -785,6 +785,29 @@ enum TokenStepLanguage: String, CaseIterable, Identifiable, Codable {
}
}

enum AgentWorkRankVisibility: String, Codable, CaseIterable, Identifiable {
case automatic
case visible
case hidden

var id: String { rawValue }

var readsLocalIdentity: Bool {
self != .hidden
}

func shouldShow(hasLocalIdentity: Bool) -> Bool {
switch self {
case .automatic:
return hasLocalIdentity
case .visible:
return true
case .hidden:
return false
}
}
}

struct TokenStepSettings: Codable {
var dailyGoalTokens: Int
var refreshIntervalSeconds: Int
Expand All @@ -796,7 +819,7 @@ struct TokenStepSettings: Codable {
var tokenIslandEnabled: Bool
var tokenIslandPlacement: TokenIslandDisplayPlacement
var showCodexQuota: Bool
var showAgentWorkRank: Bool
var agentWorkRankVisibility: AgentWorkRankVisibility
var showExperimentalAgentSources: Bool
var language: TokenStepLanguage
var skippedUpdateVersion: String?
Expand All @@ -812,7 +835,8 @@ struct TokenStepSettings: Codable {
case tokenIslandEnabled = "token_island_enabled"
case tokenIslandPlacement = "token_island_placement"
case showCodexQuota = "show_codex_quota"
case showAgentWorkRank = "show_agent_work_rank"
case agentWorkRankVisibility = "agent_work_rank_visibility"
case legacyShowAgentWorkRank = "show_agent_work_rank"
case showExperimentalAgentSources = "show_experimental_agent_sources"
case language
case skippedUpdateVersion = "skipped_update_version"
Expand All @@ -829,7 +853,7 @@ struct TokenStepSettings: Codable {
tokenIslandEnabled: false,
tokenIslandPlacement: .menuBar,
showCodexQuota: false,
showAgentWorkRank: false,
agentWorkRankVisibility: .automatic,
showExperimentalAgentSources: false,
language: .system,
skippedUpdateVersion: nil
Expand All @@ -846,7 +870,7 @@ struct TokenStepSettings: Codable {
tokenIslandEnabled: Bool,
tokenIslandPlacement: TokenIslandDisplayPlacement,
showCodexQuota: Bool,
showAgentWorkRank: Bool,
agentWorkRankVisibility: AgentWorkRankVisibility,
showExperimentalAgentSources: Bool,
language: TokenStepLanguage,
skippedUpdateVersion: String?
Expand All @@ -861,7 +885,7 @@ struct TokenStepSettings: Codable {
self.tokenIslandEnabled = tokenIslandEnabled
self.tokenIslandPlacement = tokenIslandPlacement
self.showCodexQuota = showCodexQuota
self.showAgentWorkRank = showAgentWorkRank
self.agentWorkRankVisibility = agentWorkRankVisibility
self.showExperimentalAgentSources = showExperimentalAgentSources
self.language = language
self.skippedUpdateVersion = skippedUpdateVersion
Expand All @@ -888,9 +912,33 @@ struct TokenStepSettings: Codable {
tokenIslandPlacement = defaults.tokenIslandPlacement
}
showCodexQuota = try container.decodeIfPresent(Bool.self, forKey: .showCodexQuota) ?? defaults.showCodexQuota
showAgentWorkRank = try container.decodeIfPresent(Bool.self, forKey: .showAgentWorkRank) ?? defaults.showAgentWorkRank
if let visibility = try container.decodeIfPresent(AgentWorkRankVisibility.self, forKey: .agentWorkRankVisibility) {
agentWorkRankVisibility = visibility
} else if let legacyVisible = try container.decodeIfPresent(Bool.self, forKey: .legacyShowAgentWorkRank) {
agentWorkRankVisibility = legacyVisible ? .visible : .automatic
} else {
agentWorkRankVisibility = defaults.agentWorkRankVisibility
}
showExperimentalAgentSources = try container.decodeIfPresent(Bool.self, forKey: .showExperimentalAgentSources) ?? defaults.showExperimentalAgentSources
language = try container.decodeIfPresent(TokenStepLanguage.self, forKey: .language) ?? defaults.language
skippedUpdateVersion = try container.decodeIfPresent(String.self, forKey: .skippedUpdateVersion)
}

func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(dailyGoalTokens, forKey: .dailyGoalTokens)
try container.encode(refreshIntervalSeconds, forKey: .refreshIntervalSeconds)
try container.encode(historyDays, forKey: .historyDays)
try container.encode(theme, forKey: .theme)
try container.encode(autoUpdateEnabled, forKey: .autoUpdateEnabled)
try container.encode(askBeforeDownloadingUpdates, forKey: .askBeforeDownloadingUpdates)
try container.encode(requireVerifiedUpdates, forKey: .requireVerifiedUpdates)
try container.encode(tokenIslandEnabled, forKey: .tokenIslandEnabled)
try container.encode(tokenIslandPlacement, forKey: .tokenIslandPlacement)
try container.encode(showCodexQuota, forKey: .showCodexQuota)
try container.encode(agentWorkRankVisibility, forKey: .agentWorkRankVisibility)
try container.encode(showExperimentalAgentSources, forKey: .showExperimentalAgentSources)
try container.encode(language, forKey: .language)
try container.encodeIfPresent(skippedUpdateVersion, forKey: .skippedUpdateVersion)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ enum DataService {
tokenIslandEnabled: placement != .menuBar,
tokenIslandPlacement: placement,
showCodexQuota: settings.showCodexQuota,
showAgentWorkRank: settings.showAgentWorkRank,
agentWorkRankVisibility: settings.agentWorkRankVisibility,
showExperimentalAgentSources: settings.showExperimentalAgentSources,
language: settings.language,
skippedUpdateVersion: settings.skippedUpdateVersion
Expand Down
29 changes: 22 additions & 7 deletions TokenStepSwift/Sources/TokenStepSwift/Stores/AppState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,10 @@ final class AppState: ObservableObject {
"\(settings.theme.id)-\(settings.language.resolved.id)"
}

var shouldShowAgentWorkRank: Bool {
settings.agentWorkRankVisibility.shouldShow(hasLocalIdentity: agentWorkRankIdentity != nil)
}

func load() {
defer { MemoryPressure.relieveAllocatorPressure() }
let loadedSettings = DataService.loadSettings()
Expand All @@ -132,8 +136,14 @@ final class AppState: ObservableObject {
codexQuota = .unavailable
claudeQuota = .unavailable
}
if !loadedSettings.showAgentWorkRank {
if !loadedSettings.agentWorkRankVisibility.readsLocalIdentity {
clearTokenRankState()
} else {
agentWorkRankIdentity = AgentWorkRankService.loadLocalIdentity()
if loadedSettings.agentWorkRankVisibility == .automatic,
agentWorkRankIdentity == nil {
clearTokenRankState()
}
}
autostartEnabled = AutostartService.isEnabled
}
Expand Down Expand Up @@ -296,10 +306,10 @@ final class AppState: ObservableObject {
}
}

func setAgentWorkRankVisible(_ visible: Bool) {
settings.showAgentWorkRank = visible
func setAgentWorkRankVisibility(_ visibility: AgentWorkRankVisibility) {
settings.agentWorkRankVisibility = visibility
saveSettingsAndReload()
if settings.showAgentWorkRank {
if shouldShowAgentWorkRank {
refreshTokenRank(force: true)
} else {
clearTokenRankState()
Expand All @@ -313,7 +323,12 @@ final class AppState: ObservableObject {
}

func refreshTokenRank(force: Bool = false) {
guard settings.showAgentWorkRank else {
guard settings.agentWorkRankVisibility.readsLocalIdentity else {
clearTokenRankState()
return
}
agentWorkRankIdentity = AgentWorkRankService.loadLocalIdentity()
guard shouldShowAgentWorkRank else {
clearTokenRankState()
return
}
Expand All @@ -332,14 +347,14 @@ final class AppState: ObservableObject {
}
do {
let leaderboard = try await AgentWorkRankService.fetchLeaderboard()
guard settings.showAgentWorkRank else {
guard shouldShowAgentWorkRank else {
clearTokenRankState()
return
}
tokenRank = leaderboard
tokenRankError = nil
} catch {
guard settings.showAgentWorkRank else {
guard shouldShowAgentWorkRank else {
clearTokenRankState()
return
}
Expand Down
28 changes: 28 additions & 0 deletions TokenStepSwift/Sources/TokenStepSwift/Support/Localization.swift
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,14 @@ enum TokenStepLocalization {
"Agent 额度显示": "Show Agent Quota",
"Agent 消耗榜": "Agent Usage Rank",
"Agent 消耗榜显示": "Show Agent Usage Rank",
"显示": "Show",
"隐藏": "Hide",
"自动检测": "Auto Detect",
"已自动显示": "Shown automatically",
"已显示": "Shown",
"已手动隐藏": "Hidden manually",
"未检测到 Token Rank": "Token Rank not detected",
"不读取身份与榜单": "No identity or rank access",
"尚未关联": "Not linked",
"已自动关联": "Automatically linked",
"安装 Token Rank 后自动识别": "Detected after Token Rank is installed",
Expand All @@ -280,13 +288,19 @@ enum TokenStepLocalization {
"暂时无法读取榜单": "Leaderboard unavailable",
"榜单同步失败,显示上次结果": "Sync failed, showing last result",
"匿名用户": "Anonymous",
"今日排名": "Today Rank",
"今日排名 #%d": "Today rank #%d",
"第 %d / %d · 超过 %d%% 参榜用户": "#%d of %d · Ahead of %d%%",
"%d 人 · %@": "%d users · %@",
"今日未上榜": "Not ranked today",
"榜单暂不可用": "Leaderboard unavailable",
"点击查看今日榜单": "View today’s leaderboard",
"点击打开榜单页": "Open leaderboard",
"今日榜首": "Top today",
"今日榜单": "Today board",
"我的今日 Token": "My Today Tokens",
"全榜今日 Token": "All Today Tokens",
"主力": "Main",
"榜单": "Board",
"上榜:": "Ranked:",
"实验 Agent 来源": "Experimental Agent Sources",
Expand Down Expand Up @@ -618,6 +632,14 @@ enum TokenStepLocalization {
"Agent 额度显示": "顯示 Agent 額度",
"Agent 消耗榜": "Agent 消耗榜",
"Agent 消耗榜显示": "顯示 Agent 消耗榜",
"显示": "顯示",
"隐藏": "隱藏",
"自动检测": "自動偵測",
"已自动显示": "已自動顯示",
"已显示": "已顯示",
"已手动隐藏": "已手動隱藏",
"未检测到 Token Rank": "未偵測到 Token Rank",
"不读取身份与榜单": "不讀取身分與榜單",
"尚未关联": "尚未關聯",
"已自动关联": "已自動關聯",
"安装 Token Rank 后自动识别": "安裝 Token Rank 後自動識別",
Expand All @@ -633,13 +655,19 @@ enum TokenStepLocalization {
"暂时无法读取榜单": "暫時無法讀取榜單",
"榜单同步失败,显示上次结果": "榜單同步失敗,顯示上次結果",
"匿名用户": "匿名使用者",
"今日排名": "今日排名",
"今日排名 #%d": "今日排名 #%d",
"第 %d / %d · 超过 %d%% 参榜用户": "第 %d / %d · 超過 %d%% 參榜使用者",
"%d 人 · %@": "%d 人 · %@",
"今日未上榜": "今日未上榜",
"榜单暂不可用": "榜單暫不可用",
"点击查看今日榜单": "點擊查看今日榜單",
"点击打开榜单页": "點擊開啟榜單頁",
"今日榜首": "今日榜首",
"今日榜单": "今日榜單",
"我的今日 Token": "我的今日 Token",
"全榜今日 Token": "全榜今日 Token",
"主力": "主力",
"榜单": "榜單",
"上榜:": "上榜:",
"实验 Agent 来源": "實驗 Agent 來源",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,23 @@ struct PopoverTodayRingCard: View {
.frame(maxWidth: .infinity, alignment: .leading)
}

if let summary = todayToolSummary {
Text(summary)
.font(.caption.weight(.semibold))
.foregroundStyle(.secondary)
.monospacedDigit()
.lineLimit(1)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.top, 1)
if !todaySourceRows.isEmpty {
VStack(alignment: .leading, spacing: 7) {
Text(L("今日来源"))
.font(.caption2.weight(.heavy))
.foregroundStyle(.secondary)
HStack(spacing: 10) {
ForEach(todaySourceRows.indices, id: \.self) { index in
let row = todaySourceRows[index]
if index > 0 {
Divider()
.frame(height: 30)
}
TodaySourceMetric(name: row.name, tokens: row.tokens)
}
}
}
.padding(.top, 1)
}
}
}
Expand All @@ -71,12 +80,49 @@ struct PopoverTodayRingCard: View {
TokenStepLocalization.language == .en ? "\(count)d" : "\(count) 天"
}

private var todayToolSummary: String? {
guard appState.today.totalTokens > 0 else { return nil }
let orderedTools = [("Codex", "Codex"), ("Claude Code", "Claude")]
let parts = orderedTools.map { tool, label in
"\(label) \(TokenStepFormat.tokens(appState.today.tools[tool] ?? 0, compact: true))"
private var todaySourceRows: [(name: String, tokens: Int)] {
var rows = appState.today.tools
.filter { $0.value > 0 }
.map { (name: $0.key, tokens: $0.value) }
.sorted { $0.tokens > $1.tokens }
guard rows.count > 3 else { return rows }

var selected = Array(rows.prefix(3))
if let workBuddy = rows.first(where: { $0.name == "WorkBuddy" }),
!selected.contains(where: { $0.name == "WorkBuddy" }) {
selected[2] = workBuddy
}
rows = selected.sorted { $0.tokens > $1.tokens }
return rows
}
}

private struct TodaySourceMetric: View {
var name: String
var tokens: Int

var body: some View {
VStack(alignment: .leading, spacing: 3) {
HStack(spacing: 5) {
Circle()
.fill(tokenToolColor(name))
.frame(width: 6, height: 6)
Text(displayName)
.font(.caption2.weight(.bold))
.foregroundStyle(.secondary)
.lineLimit(1)
}
Text(TokenStepFormat.tokens(tokens, compact: true))
.font(.caption.weight(.heavy))
.foregroundStyle(Color.tokenInk.opacity(0.82))
.monospacedDigit()
.lineLimit(1)
.minimumScaleFactor(0.72)
}
return "\(L("今日")) \(parts.joined(separator: " · "))"
.frame(maxWidth: .infinity, alignment: .leading)
}

private var displayName: String {
name == "Claude Code" ? "Claude" : name
}
}
Loading
Loading