codex-flow 2.1.13__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. codex_flow/__init__.py +28 -0
  2. codex_flow/__main__.py +9 -0
  3. codex_flow/cli.py +242 -0
  4. codex_flow/data/LICENSE +21 -0
  5. codex_flow/data/README.en.md +303 -0
  6. codex_flow/data/README.md +305 -0
  7. codex_flow/data/VERSION +1 -0
  8. codex_flow/data/apps/chatgpt-mcp/README.md +86 -0
  9. codex_flow/data/apps/chatgpt-mcp/__init__.py +1 -0
  10. codex_flow/data/apps/chatgpt-mcp/adapter.py +458 -0
  11. codex_flow/data/apps/chatgpt-mcp/server.py +358 -0
  12. codex_flow/data/apps/chatgpt-mcp/widget.html +927 -0
  13. codex_flow/data/apps/macos-overlay/README.en.md +121 -0
  14. codex_flow/data/apps/macos-overlay/README.md +123 -0
  15. codex_flow/data/apps/macos-overlay/Sources/Controllers/OverlayRuntimeState.swift +126 -0
  16. codex_flow/data/apps/macos-overlay/Sources/Controllers/OverlayScreenGeometry.swift +82 -0
  17. codex_flow/data/apps/macos-overlay/Sources/Controllers/OverlayWindowController.swift +1052 -0
  18. codex_flow/data/apps/macos-overlay/Sources/Localization.swift +197 -0
  19. codex_flow/data/apps/macos-overlay/Sources/Models/TelemetryData.swift +1557 -0
  20. codex_flow/data/apps/macos-overlay/Sources/Services/AccountSnapshotService.swift +1101 -0
  21. codex_flow/data/apps/macos-overlay/Sources/Services/FlowPilotInstanceLock.swift +153 -0
  22. codex_flow/data/apps/macos-overlay/Sources/Services/IPCServer.swift +298 -0
  23. codex_flow/data/apps/macos-overlay/Sources/Services/TelemetryQueryEngine.swift +800 -0
  24. codex_flow/data/apps/macos-overlay/Sources/Services/TelemetryWatcher.swift +135 -0
  25. codex_flow/data/apps/macos-overlay/Sources/Services/UpdateService.swift +610 -0
  26. codex_flow/data/apps/macos-overlay/Sources/Views/AccountView.swift +610 -0
  27. codex_flow/data/apps/macos-overlay/Sources/Views/AnalyticsView.swift +566 -0
  28. codex_flow/data/apps/macos-overlay/Sources/Views/AutostartView.swift +293 -0
  29. codex_flow/data/apps/macos-overlay/Sources/Views/BubbleView.swift +317 -0
  30. codex_flow/data/apps/macos-overlay/Sources/Views/HistoryView.swift +1124 -0
  31. codex_flow/data/apps/macos-overlay/Sources/Views/HoverRevealText.swift +165 -0
  32. codex_flow/data/apps/macos-overlay/Sources/Views/InspectorSkillsToolsView.swift +121 -0
  33. codex_flow/data/apps/macos-overlay/Sources/Views/LogoView.swift +182 -0
  34. codex_flow/data/apps/macos-overlay/Sources/Views/SleekSwitch.swift +117 -0
  35. codex_flow/data/apps/macos-overlay/Sources/Views/StrategyModeView.swift +561 -0
  36. codex_flow/data/apps/macos-overlay/Sources/Views/SummaryView.swift +1273 -0
  37. codex_flow/data/apps/macos-overlay/Sources/Views/UpdateView.swift +352 -0
  38. codex_flow/data/apps/macos-overlay/Sources/main.swift +340 -0
  39. codex_flow/data/apps/macos-overlay/Tests/OverlayScreenGeometryTests.swift +163 -0
  40. codex_flow/data/apps/macos-overlay/Tests/TelemetryPhase1ContractTests.swift +357 -0
  41. codex_flow/data/apps/macos-overlay/Tests/TelemetryQueryEngineConcurrencyTests.swift +221 -0
  42. codex_flow/data/apps/macos-overlay/Tests/TelemetryQuotaSelectionTests.swift +158 -0
  43. codex_flow/data/apps/macos-overlay/Tests/TelemetryWorkerTokenTests.swift +122 -0
  44. codex_flow/data/apps/macos-overlay/build.sh +75 -0
  45. codex_flow/data/benchmark/corpus.json +103 -0
  46. codex_flow/data/benchmark/manifest.example.json +41 -0
  47. codex_flow/data/benchmark/manifest.schema.json +137 -0
  48. codex_flow/data/benchmark/prices/gpt-5.6-2026-08-30.json +5 -0
  49. codex_flow/data/benchmark/profiles.json +90 -0
  50. codex_flow/data/benchmark/schema.json +77 -0
  51. codex_flow/data/benchmark/tasks.json +50 -0
  52. codex_flow/data/completions/codex-flow.bash +34 -0
  53. codex_flow/data/completions/codex-flow.zsh +52 -0
  54. codex_flow/data/glama.json +6 -0
  55. codex_flow/data/install-release.ps1 +126 -0
  56. codex_flow/data/install-release.sh +155 -0
  57. codex_flow/data/install.ps1 +349 -0
  58. codex_flow/data/install.sh +362 -0
  59. codex_flow/data/policy/benchmark.toml +49 -0
  60. codex_flow/data/policy/defaults.toml +70 -0
  61. codex_flow/data/scripts/analyze-benchmark.py +510 -0
  62. codex_flow/data/scripts/benchmark-local.py +171 -0
  63. codex_flow/data/scripts/check-recommendation.py +277 -0
  64. codex_flow/data/scripts/doctor.py +449 -0
  65. codex_flow/data/scripts/generate-release-manifest.py +74 -0
  66. codex_flow/data/scripts/localization.py +192 -0
  67. codex_flow/data/scripts/manage-hooks.py +448 -0
  68. codex_flow/data/scripts/manage-instructions.py +389 -0
  69. codex_flow/data/scripts/manage-shell.py +151 -0
  70. codex_flow/data/scripts/materialize-corpus.py +193 -0
  71. codex_flow/data/scripts/menu.py +646 -0
  72. codex_flow/data/scripts/migrations/0001_update_settings.py +80 -0
  73. codex_flow/data/scripts/package-release.py +132 -0
  74. codex_flow/data/scripts/render-benchmark-report.py +292 -0
  75. codex_flow/data/scripts/run-benchmark.py +829 -0
  76. codex_flow/data/scripts/strategies/__init__.py +28 -0
  77. codex_flow/data/scripts/strategies/balanced.py +115 -0
  78. codex_flow/data/scripts/strategies/base.py +363 -0
  79. codex_flow/data/scripts/strategies/efficient.py +158 -0
  80. codex_flow/data/scripts/strategies/lifecycle_runtime.py +590 -0
  81. codex_flow/data/scripts/strategies/quality.py +209 -0
  82. codex_flow/data/scripts/strategies/speed.py +108 -0
  83. codex_flow/data/scripts/strategies/task_budget_runtime.py +644 -0
  84. codex_flow/data/scripts/strategies/task_phase_runtime.py +341 -0
  85. codex_flow/data/scripts/strategies/work_unit_runtime.py +421 -0
  86. codex_flow/data/scripts/strategy_runtime.py +1091 -0
  87. codex_flow/data/scripts/telemetry.py +400 -0
  88. codex_flow/data/scripts/telemetry_core/__init__.py +192 -0
  89. codex_flow/data/scripts/telemetry_core/app_server.py +1192 -0
  90. codex_flow/data/scripts/telemetry_core/collector.py +1247 -0
  91. codex_flow/data/scripts/telemetry_core/common.py +421 -0
  92. codex_flow/data/scripts/telemetry_core/latency.py +593 -0
  93. codex_flow/data/scripts/telemetry_core/query.py +427 -0
  94. codex_flow/data/scripts/telemetry_core/quota_ledger.py +598 -0
  95. codex_flow/data/scripts/telemetry_core/render.py +460 -0
  96. codex_flow/data/scripts/telemetry_core/repair.py +223 -0
  97. codex_flow/data/scripts/ui.py +266 -0
  98. codex_flow/data/scripts/update-homebrew-formula.py +146 -0
  99. codex_flow/data/scripts/update_runtime_config.py +134 -0
  100. codex_flow/data/scripts/updater.py +1718 -0
  101. codex_flow/data/smithery.yaml +18 -0
  102. codex_flow/data/templates/agents/worker-explorer.toml +24 -0
  103. codex_flow/data/templates/agents/worker-implementer.toml +49 -0
  104. codex_flow/data/templates/agents/worker-reviewer.toml +25 -0
  105. codex_flow/data/templates/flow-pilot-instructions.md +35 -0
  106. codex_flow/data/templates/skills/flow-pilot/SKILL.md +577 -0
  107. codex_flow/mcp.py +35 -0
  108. codex_flow-2.1.13.dist-info/METADATA +342 -0
  109. codex_flow-2.1.13.dist-info/RECORD +113 -0
  110. codex_flow-2.1.13.dist-info/WHEEL +5 -0
  111. codex_flow-2.1.13.dist-info/entry_points.txt +3 -0
  112. codex_flow-2.1.13.dist-info/licenses/LICENSE +21 -0
  113. codex_flow-2.1.13.dist-info/top_level.txt +1 -0
@@ -0,0 +1,460 @@
1
+ """Terminal presentation and summary card formatting for task runs and project aggregations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import time
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from .app_server import enrich_run_metadata, usage_group_identities
11
+ from .common import (
12
+ compact_text,
13
+ display_width,
14
+ fmt_duration_ms,
15
+ fmt_local_timestamp,
16
+ fmt_percent,
17
+ fmt_tokens,
18
+ numeric_ms,
19
+ pad_display,
20
+ text_value,
21
+ truncate_display,
22
+ window_label,
23
+ )
24
+
25
+ try:
26
+ from localization import resolve_language, tr
27
+ except ImportError:
28
+ from scripts.localization import resolve_language, tr # type: ignore
29
+
30
+ LANG = resolve_language(Path(os.environ.get("CODEX_HOME", Path.home() / ".codex")) / "codex-flow.toml")
31
+
32
+
33
+ def T(en: str, zh: str) -> str:
34
+ return tr(en, zh, lang=LANG)
35
+
36
+
37
+ def aggregate_usage_value(usages: list[dict[str, Any] | None], key: str) -> int | None:
38
+ values: list[int] = []
39
+ for usage in usages:
40
+ if not isinstance(usage, dict):
41
+ return None
42
+ value = usage.get(key)
43
+ if not isinstance(value, (int, float)):
44
+ return None
45
+ values.append(int(value))
46
+ return sum(values) if values else None
47
+
48
+
49
+ def participant_runtime_label(participant: dict[str, Any], usage: dict[str, Any] | None) -> str:
50
+ model = text_value(participant.get("model"))
51
+ effort = text_value(participant.get("reasoning_effort"))
52
+ identities = usage_group_identities(usage)
53
+ if not model:
54
+ models = {item_model for item_model, _ in identities if item_model}
55
+ if len(models) == 1:
56
+ model = next(iter(models))
57
+ if not effort:
58
+ efforts = {item_effort for _, item_effort in identities if item_effort}
59
+ if len(efforts) == 1:
60
+ effort = next(iter(efforts))
61
+ model = compact_text(model, 32) or T("unknown", "未知")
62
+ effort = compact_text(effort, 16) or T("effort n/a", "推理强度未知")
63
+ return f"{model} ({effort})"
64
+
65
+
66
+ def run_context(run: dict[str, Any]) -> tuple[str | None, str | None, str | None]:
67
+ thread = run.get("thread") if isinstance(run.get("thread"), dict) else {}
68
+ session = compact_text(
69
+ thread.get("name")
70
+ or thread.get("preview")
71
+ or run.get("summary")
72
+ or ("系统后台任务" if run.get("is_system_task") else None)
73
+ or run.get("session_id")
74
+ )
75
+ cwd = thread.get("cwd") or run.get("cwd")
76
+ project = None
77
+ if cwd:
78
+ try:
79
+ pname = Path(str(cwd)).name
80
+ if not pname and run.get("is_system_task"):
81
+ project = "系统任务"
82
+ else:
83
+ project = pname or str(cwd)
84
+ except (OSError, ValueError):
85
+ project = str(cwd)
86
+ git_info = thread.get("gitInfo")
87
+ branch = git_info.get("branch") if isinstance(git_info, dict) else None
88
+ return session, compact_text(project), compact_text(branch)
89
+
90
+
91
+ def render_summary(run: dict[str, Any]) -> str:
92
+ enrich_run_metadata(run)
93
+ workers = list((run.get("workers") or {}).values())
94
+ parent = run.get("parent") or {}
95
+ parent_usage = parent.get("usage_delta") if isinstance(parent, dict) else None
96
+ participant_usages = [parent_usage]
97
+ participant_usages.extend(worker.get("usage") if isinstance(worker, dict) else None for worker in workers)
98
+
99
+ p_count = T(
100
+ f"1 parent + {len(workers)} worker{'s' if len(workers) != 1 else ''}",
101
+ f"1 个 parent + {len(workers)} 个 worker",
102
+ )
103
+ p_runtime = participant_runtime_label(parent, parent_usage)
104
+ p_tokens = f"{fmt_tokens(parent_usage.get('total_tokens') if isinstance(parent_usage, dict) else None)} tokens"
105
+ total_tokens = aggregate_usage_value(participant_usages, "total_tokens")
106
+ credits = aggregate_usage_value(participant_usages, "estimated_credits_micros")
107
+ attr_tokens = f"{fmt_tokens(total_tokens)} tokens"
108
+ attr_credits = f" · {credits / 1_000_000:.3f} credits" if credits is not None else ""
109
+
110
+ def pad_line(content: str, width: int = 68) -> str:
111
+ pad = max(0, width - display_width(content))
112
+ return f" │ {content}{' ' * pad} │"
113
+
114
+ def append_wrapped_line(content: str, continuation: str = " ") -> None:
115
+ remaining = content
116
+ first = True
117
+ while remaining:
118
+ prefix = "" if first else continuation
119
+ available = max(1, 68 - len(prefix))
120
+ if len(remaining) <= available:
121
+ lines.append(pad_line(prefix + remaining))
122
+ return
123
+ split_at = remaining.rfind(" ", 0, available + 1)
124
+ if split_at <= 0:
125
+ split_at = available
126
+ lines.append(pad_line(prefix + remaining[:split_at].rstrip()))
127
+ remaining = remaining[split_at:].lstrip()
128
+ first = False
129
+
130
+ lines = ["", T("📊 FlowPilot Telemetry Summary", "📊 FlowPilot 遥测摘要"), ""]
131
+ lines.append(T(
132
+ " ╭─ Run Overview ────────────────────────────────────────────────────╮",
133
+ " ╭─ 任务概览 ───────────────────────────────────────────────────────╮",
134
+ ))
135
+
136
+ session, project, branch = run_context(run)
137
+ if session:
138
+ lines.append(pad_line(T(f"• Session: {session}", f"• 会话: {session}")))
139
+ if project:
140
+ project_text = f"{project} · {branch}" if branch else project
141
+ lines.append(pad_line(T(f"• Project: {project_text}", f"• 项目: {project_text}")))
142
+
143
+ started = fmt_local_timestamp(run.get("started_at_ms"), milliseconds=True)
144
+ finished = fmt_local_timestamp(run.get("finished_at_ms"), milliseconds=True)
145
+ if started:
146
+ lines.append(pad_line(T(f"• Started: {started}", f"• 开始时间: {started}")))
147
+ if finished:
148
+ lines.append(pad_line(T(f"• Finished: {finished}", f"• 完成时间: {finished}")))
149
+ started_ms = run.get("started_at_ms")
150
+ finished_ms = run.get("finished_at_ms")
151
+ if isinstance(started_ms, (int, float)) and isinstance(finished_ms, (int, float)):
152
+ duration = fmt_duration_ms(finished_ms - started_ms)
153
+ if duration:
154
+ lines.append(pad_line(T(f"• Duration: {duration}", f"• 持续时间: {duration}")))
155
+
156
+ lines.append(pad_line(T(f"• Participants: {p_count}", f"• 参与者: {p_count}")))
157
+ lines.append(pad_line(T(f"• Parent: {p_runtime} · {p_tokens}", f"• Parent: {p_runtime} · {p_tokens}")))
158
+ for worker in workers:
159
+ usage = worker.get("usage") or {}
160
+ state = worker.get("status") or "observed"
161
+ state_label = {
162
+ "completed": T("completed", "已完成"),
163
+ "observed": T("observed", "已观测"),
164
+ "running": T("running", "运行中"),
165
+ }.get(str(state), str(state))
166
+ w_type = worker.get("agent_type") or "worker"
167
+ w_runtime = participant_runtime_label(worker, usage)
168
+ w_tokens = f"{fmt_tokens(usage.get('total_tokens'))} tokens ({state_label})"
169
+ label = f"• Worker [{w_type}]:"
170
+ worker_line = f"{label} {w_runtime} · {w_tokens}"
171
+ if display_width(worker_line) <= 68:
172
+ lines.append(pad_line(worker_line))
173
+ else:
174
+ lines.append(pad_line(f"{label} {w_runtime}"))
175
+ lines.append(pad_line(f" ↳ {w_tokens}"))
176
+ worker_conclusion = compact_text(worker.get("conclusion"), 240)
177
+ if worker_conclusion:
178
+ append_wrapped_line(
179
+ T(
180
+ f" ↳ Result: {worker_conclusion}",
181
+ f" ↳ 结果: {worker_conclusion}",
182
+ ),
183
+ continuation=" ",
184
+ )
185
+ lines.append(pad_line(T(f"• Attributed: {attr_tokens}{attr_credits}", f"• 归因用量: {attr_tokens}{attr_credits}")))
186
+
187
+ windows = run.get("quota_change_during_run") or []
188
+ if windows:
189
+ pieces: list[str] = []
190
+ reset_pieces: list[str] = []
191
+ for window in windows:
192
+ before_match = next((item for item in run.get("quota_before", []) if item.get("window_duration_mins") == window.get("window_duration_mins")), None)
193
+ old = before_match.get("used_percent") if before_match else None
194
+ new = window.get("used_percent")
195
+ delta = window.get("delta_percentage_points")
196
+ label = window_label(window.get("window_duration_mins"))
197
+ if old is not None and new is not None:
198
+ old_text = fmt_percent(old) or "n/a"
199
+ new_text = fmt_percent(new) or "n/a"
200
+ if delta is None:
201
+ delta_text = "n/a"
202
+ elif delta == 0:
203
+ delta_text = T("no change", "无变化")
204
+ else:
205
+ delta_text = f"{delta:+g} pp"
206
+ remaining_pct = fmt_percent(max(0, 100 - new)) if isinstance(new, (int, float)) and not isinstance(new, bool) else None
207
+ if LANG == "zh":
208
+ remaining = f";剩余 {remaining_pct}%" if remaining_pct is not None else ""
209
+ pieces.append(f"{label} 已用 {old_text}% → {new_text}%({delta_text}{remaining})")
210
+ else:
211
+ remaining = f"; {remaining_pct}% remaining" if remaining_pct is not None else ""
212
+ pieces.append(f"{label} used {old_text}% → {new_text}% ({delta_text}{remaining})")
213
+ elif new is not None:
214
+ new_text = fmt_percent(new) or "n/a"
215
+ remaining_pct = fmt_percent(max(0, 100 - new)) if isinstance(new, (int, float)) and not isinstance(new, bool) else None
216
+ if LANG == "zh":
217
+ suffix = f"(剩余 {remaining_pct}%)" if remaining_pct is not None else ""
218
+ pieces.append(f"{label} 已用 {new_text}%{suffix}")
219
+ else:
220
+ suffix = f" ({remaining_pct}% remaining)" if remaining_pct is not None else ""
221
+ pieces.append(f"{label} used {new_text}%{suffix}")
222
+ reset = fmt_local_timestamp(window.get("resets_at"))
223
+ if reset:
224
+ reset_pieces.append(f"{label} {reset}")
225
+ if pieces:
226
+ lines.append(T(
227
+ " ├─ Quota & Windows ─────────────────────────────────────────────────┤",
228
+ " ├─ 额度与时间窗 ───────────────────────────────────────────────────┤",
229
+ ))
230
+ append_wrapped_line(T(f"• Account Quota: {' | '.join(pieces)}", f"• 账户额度: {' | '.join(pieces)}"))
231
+ if reset_pieces:
232
+ append_wrapped_line(T(f"• Resets: {' | '.join(reset_pieces)}", f"• 重置时间: {' | '.join(reset_pieces)}"))
233
+
234
+ skills = run.get("skills_used") or []
235
+ tools = run.get("tools_used") or []
236
+ summary_info = run.get("summary_info") or {}
237
+
238
+ if skills or tools or summary_info.get("conclusion"):
239
+ lines.append(T(
240
+ " ├─ Insights & Trajectory ───────────────────────────────────────────┤",
241
+ " ├─ 执行详情与技能洞察 ──────────────────────────────────────────────┤",
242
+ ))
243
+ if skills:
244
+ skills_str = ", ".join(f"{s.get('name')} (×{s.get('count', 1)})" for s in skills)
245
+ append_wrapped_line(T(f"• Skills Used: {skills_str}", f"• 调用技能: {skills_str}"))
246
+ if tools:
247
+ tools_str = ", ".join(f"{t.get('name')} (×{t.get('count', 1)})" for t in tools)
248
+ append_wrapped_line(T(f"• Tools / MCP: {tools_str}", f"• 工具 / MCP: {tools_str}"))
249
+ if summary_info.get("conclusion"):
250
+ conc = " ".join(summary_info["conclusion"].split())
251
+ if len(conc) > 100:
252
+ conc = conc[:97] + "..."
253
+ append_wrapped_line(T(f"• Conclusion: {conc}", f"• 交付结论: {conc}"))
254
+
255
+ lines.append(" ╰───────────────────────────────────────────────────────────────────╯")
256
+ lines.append(T(
257
+ " ℹ Note: Times use the local timezone; quota delta is account-wide; attributed usage is thread-based.",
258
+ " ℹ 说明:时间使用本地时区;额度变化为账户级;归因用量基于 thread 计算。",
259
+ ))
260
+ lines.append(T(
261
+ " 💡 Tip: Run `codex-flow usage last` to review or `codex-flow doctor` to verify hooks.\n",
262
+ " 💡 提示:运行 `codex-flow usage last` 可再次查看,运行 `codex-flow doctor` 可检查 hooks。\n",
263
+ ))
264
+ return "\n".join(lines)
265
+
266
+
267
+ def render_run_list(runs: list[dict[str, Any]], project: str | None = None, today: bool = False) -> str:
268
+ if not runs:
269
+ if LANG == "zh":
270
+ msg = "未找到 FlowPilot 遥测任务记录"
271
+ if project:
272
+ msg += f"(项目:{project})"
273
+ if today:
274
+ msg += "(今天)"
275
+ else:
276
+ msg = "No FlowPilot telemetry runs found"
277
+ if project:
278
+ msg += f" for project '{project}'"
279
+ if today:
280
+ msg += " today"
281
+ return f"{msg}.\n"
282
+
283
+ count = len(runs)
284
+ lines = ["", T(
285
+ f"📜 FlowPilot Task Telemetry History (Showing {count} task{'s' if count != 1 else ''})",
286
+ f"📜 FlowPilot 任务遥测历史(显示 {count} 个任务)",
287
+ ), ""]
288
+ if LANG == "zh":
289
+ hdr = f" {'#':<4} {'时间':<17} {'项目':<18} {'会话':<20} {'WORKERS':<9} {'TOKENS':<10} {'状态':<12}"
290
+ else:
291
+ hdr = f" {'#':<4} {'TIME':<17} {'PROJECT':<18} {'SESSION':<20} {'WORKERS':<9} {'TOKENS':<10} {'STATUS':<12}"
292
+ lines.append(hdr)
293
+ lines.append(" " + "─" * 94)
294
+
295
+ for i, run in enumerate(runs, 1):
296
+ enrich_run_metadata(run)
297
+ session, proj, branch = run_context(run)
298
+ idx_badge = f"{i}*" if i == 1 else str(i)
299
+ started_ms = numeric_ms(run.get("started_at_ms"))
300
+ finished_ms = numeric_ms(run.get("finished_at_ms"))
301
+ if started_ms:
302
+ lt = time.localtime(started_ms / 1000)
303
+ time_str = f"{lt.tm_mon:02d}-{lt.tm_mday:02d} {lt.tm_hour:02d}:{lt.tm_min:02d}"
304
+ else:
305
+ time_str = T("unknown", "未知")
306
+ dur_str = ""
307
+ if started_ms and finished_ms and finished_ms >= started_ms:
308
+ dur_str = f" ({fmt_duration_ms(finished_ms - started_ms)})"
309
+ time_text = truncate_display(time_str + dur_str, 17)
310
+
311
+ proj_text = ""
312
+ if proj:
313
+ proj_text = f"{proj} · {branch}" if branch else proj
314
+ elif run.get("cwd"):
315
+ try:
316
+ proj_text = Path(str(run.get("cwd"))).name or str(run.get("cwd"))
317
+ except Exception:
318
+ proj_text = str(run.get("cwd"))
319
+ proj_text = truncate_display(proj_text, 18)
320
+ session_text = truncate_display(session or run.get("session_id") or T("unknown", "未知"), 20)
321
+
322
+ workers = list((run.get("workers") or {}).values())
323
+ w_count = len(workers)
324
+ if w_count == 0:
325
+ w_text = "0"
326
+ else:
327
+ completed = sum(1 for w in workers if w.get("status") == "completed")
328
+ if LANG == "zh":
329
+ w_text = f"{w_count} (完成)" if completed == w_count else f"{w_count} (观测)"
330
+ else:
331
+ w_text = f"{w_count} (cmp)" if completed == w_count else f"{w_count} (obs)"
332
+ w_text = truncate_display(w_text, 9)
333
+
334
+ parent = run.get("parent") or {}
335
+ parent_usage = parent.get("usage_delta") if isinstance(parent, dict) else None
336
+ usages = [parent_usage]
337
+ usages.extend(w.get("usage") if isinstance(w, dict) else None for w in workers)
338
+ tot_tok = aggregate_usage_value(usages, "total_tokens")
339
+ tok_text = truncate_display(fmt_tokens(tot_tok) or "0", 10)
340
+ status_text = T("● completed", "● 已完成") if finished_ms else T("○ running", "○ 运行中")
341
+ row = (
342
+ f" {pad_display(idx_badge, 4)} "
343
+ f"{pad_display(time_text, 17)} "
344
+ f"{pad_display(proj_text, 18)} "
345
+ f"{pad_display(session_text, 20)} "
346
+ f"{pad_display(w_text, 9)} "
347
+ f"{pad_display(tok_text, 10)} "
348
+ f"{pad_display(status_text, 12)}"
349
+ )
350
+ lines.append(row)
351
+ lines.append("")
352
+ lines.append(T(
353
+ " 💡 Tip: Run `codex-flow usage show <#>` to view details of any task.",
354
+ " 💡 提示:运行 `codex-flow usage show <#>` 可查看任意任务详情。",
355
+ ))
356
+ lines.append("")
357
+ return "\n".join(lines)
358
+
359
+
360
+ def render_project_stats(stats: dict[str, Any]) -> str:
361
+ total_runs = stats.get("total_runs", 0)
362
+ days = stats.get("days", 30)
363
+ filter_proj = stats.get("project_filter")
364
+ if total_runs == 0:
365
+ if LANG == "zh":
366
+ msg = f"最近 {days} 天没有遥测数据"
367
+ if filter_proj:
368
+ msg += f"(项目:{filter_proj})"
369
+ else:
370
+ msg = f"No telemetry data recorded in the last {days} days"
371
+ if filter_proj:
372
+ msg += f" for project '{filter_proj}'"
373
+ return f"{msg}.\n"
374
+
375
+ def pad_line(content: str, width: int = 68) -> str:
376
+ pad = max(0, width - display_width(content))
377
+ return f" │ {content}{' ' * pad} │"
378
+
379
+ lines = [""]
380
+ title = T(
381
+ f"📊 FlowPilot Telemetry Aggregation (Last {days} Days)",
382
+ f"📊 FlowPilot 遥测聚合统计(最近 {days} 天)",
383
+ )
384
+ if filter_proj:
385
+ title = T(
386
+ f"📊 FlowPilot Telemetry Aggregation: {filter_proj} (Last {days} Days)",
387
+ f"📊 FlowPilot 遥测聚合统计:{filter_proj}(最近 {days} 天)",
388
+ )
389
+ lines += [title, "", T(
390
+ " ╭─ Overview ────────────────────────────────────────────────────────╮",
391
+ " ╭─ 概览 ───────────────────────────────────────────────────────────╮",
392
+ )]
393
+
394
+ delegated = stats.get("delegated_runs", 0)
395
+ direct = stats.get("direct_runs", 0)
396
+ lines.append(pad_line(T(
397
+ f"• Total Tasks: {total_runs} ({delegated} delegated, {direct} direct)",
398
+ f"• 任务总数: {total_runs}({delegated} 个委派,{direct} 个直跑)",
399
+ )))
400
+ dur_str = fmt_duration_ms(stats.get("total_duration_ms")) or "0s"
401
+ lines.append(pad_line(T(f"• Total Duration: {dur_str} active time", f"• 总持续时间: {dur_str} 活跃时间")))
402
+ tot_tokens = stats.get("total_tokens", 0)
403
+ credits_micros = stats.get("estimated_credits_micros")
404
+ cred_str = f" · {credits_micros / 1_000_000:.3f} credits" if credits_micros is not None else ""
405
+ lines.append(pad_line(T(f"• Attributed: {fmt_tokens(tot_tokens)} tokens{cred_str}", f"• 归因用量: {fmt_tokens(tot_tokens)} tokens{cred_str}")))
406
+ p_tokens = stats.get("parent_tokens", 0)
407
+ w_tokens = stats.get("worker_tokens", 0)
408
+ p_pct = (p_tokens / tot_tokens * 100) if tot_tokens > 0 else 0.0
409
+ w_pct = (w_tokens / tot_tokens * 100) if tot_tokens > 0 else 0.0
410
+ lines.append(pad_line(T(f"• Parent Tokens: {fmt_tokens(p_tokens)} tokens ({p_pct:.1f}%)", f"• Parent Tokens: {fmt_tokens(p_tokens)} tokens ({p_pct:.1f}%)")))
411
+ lines.append(pad_line(T(f"• Worker Tokens: {fmt_tokens(w_tokens)} tokens ({w_pct:.1f}%)", f"• Worker Tokens: {fmt_tokens(w_tokens)} tokens ({w_pct:.1f}%)")))
412
+
413
+ lines.append(T(
414
+ " ├─ Cache & Efficiency ──────────────────────────────────────────────┤",
415
+ " ├─ 缓存与效率 ─────────────────────────────────────────────────────┤",
416
+ ))
417
+ cache_ratio = stats.get("cache_ratio", 0.0)
418
+ cached_in = stats.get("cached_input_tokens", 0)
419
+ lines.append(pad_line(T(
420
+ f"• Cache Ratio: {cache_ratio:.1f}% ({fmt_tokens(cached_in)} cached input tokens)",
421
+ f"• 缓存比例: {cache_ratio:.1f}%({fmt_tokens(cached_in)} cached input tokens)",
422
+ )))
423
+ offload = stats.get("worker_offload_ratio", 0.0)
424
+ lines.append(pad_line(T(
425
+ f"• Worker Offload: {offload:.1f}% compute offloaded to worker",
426
+ f"• Worker 分流: {offload:.1f}% 计算量下放给 worker",
427
+ )))
428
+
429
+ models = stats.get("models") or {}
430
+ if models:
431
+ lines.append(T(
432
+ " ├─ Model Breakdown ─────────────────────────────────────────────────┤",
433
+ " ├─ 模型分布 ───────────────────────────────────────────────────────┤",
434
+ ))
435
+ sorted_models = sorted(models.items(), key=lambda item: item[1].get("tokens", 0), reverse=True)
436
+ for model_name, info in sorted_models:
437
+ m_tok = info.get("tokens", 0)
438
+ m_pct = (m_tok / tot_tokens * 100) if tot_tokens > 0 else 0.0
439
+ m_calls = info.get("calls", 0)
440
+ roles = "/".join(info.get("roles", []))
441
+ call_label = T("calls", "次调用")
442
+ lines.append(pad_line(f"• {pad_display(model_name, 16)} {pad_display(fmt_tokens(m_tok), 7, align='right')} tokens ({m_pct:>4.1f}% | {m_calls} {call_label} · {roles})"))
443
+
444
+ projects = stats.get("projects") or {}
445
+ if len(projects) > 1 and not filter_proj:
446
+ lines.append(T(
447
+ " ├─ Projects Distribution ───────────────────────────────────────────┤",
448
+ " ├─ 项目分布 ───────────────────────────────────────────────────────┤",
449
+ ))
450
+ sorted_projs = sorted(projects.items(), key=lambda item: item[1].get("tokens", 0), reverse=True)
451
+ for p_name, p_info in sorted_projs:
452
+ p_r = p_info.get("runs", 0)
453
+ p_tok = p_info.get("tokens", 0)
454
+ p_dur = fmt_duration_ms(p_info.get("duration_ms")) or "0s"
455
+ task_label = T("tasks", "个任务")
456
+ lines.append(pad_line(f"• {pad_display(p_name, 16)} {p_r:>2} {task_label} · {pad_display(fmt_tokens(p_tok), 7, align='right')} tokens · {p_dur}"))
457
+
458
+ lines.append(" ╰───────────────────────────────────────────────────────────────────╯")
459
+ lines.append("")
460
+ return "\n".join(lines)