cdx-manager 0.7.2 → 0.7.3

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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # CDX Manager
2
2
 
3
- [![License](https://img.shields.io/badge/license-MIT-4C8BF5)](LICENSE) ![Version](https://img.shields.io/badge/version-v0.7.2-4C8BF5) ![Python](https://img.shields.io/badge/python-3.9%2B-3776AB?logo=python&logoColor=white)
3
+ [![License](https://img.shields.io/badge/license-MIT-4C8BF5)](LICENSE) ![Version](https://img.shields.io/badge/version-v0.7.3-4C8BF5) ![Python](https://img.shields.io/badge/python-3.9%2B-3776AB?logo=python&logoColor=white)
4
4
 
5
5
  **Run multiple Codex, Claude, Antigravity, and Ollama sessions from one terminal. Switch between accounts instantly.**
6
6
 
@@ -134,7 +134,7 @@ For a specific version:
134
134
 
135
135
  ```bash
136
136
  curl -fsSL https://raw.githubusercontent.com/AlexAgo83/cdx-manager/main/install.sh -o install.sh
137
- CDX_VERSION=v0.7.2 sh install.sh
137
+ CDX_VERSION=v0.7.3 sh install.sh
138
138
  ```
139
139
 
140
140
  From source:
@@ -0,0 +1,37 @@
1
+ # Changelog (`0.7.2 -> 0.7.3`)
2
+
3
+ Release date: 2026-05-31
4
+
5
+ ## Status Display
6
+
7
+ - Rounded Codex credit balances in the `cdx status` `CR` column to two decimal places.
8
+ - Applied the same credit formatting to the single-session status detail view.
9
+ - Parallelized status resolution across sessions with a bounded worker pool while preserving cache behavior and final row ordering.
10
+ - Added visible per-session progress for text status checks, including `Checked <session> (x/y)` messages for refreshed sessions.
11
+
12
+ ## Stats and History Output
13
+
14
+ - Improved `cdx stats` text output with colorized headings, sessions, token totals, durations, and metadata when color is enabled.
15
+ - Reformatted long duration values into readable day/hour/minute forms such as `2h 05m`.
16
+ - Marked active sessions with `*` in `cdx stats`, matching `cdx status`.
17
+ - Improved `cdx history` and `cdx history --summary` with colorized headings, success/failure states, durations, and metadata.
18
+ - Marked active sessions with `*` in `cdx history` text output without changing JSON payloads.
19
+
20
+ ## Coverage and Tests
21
+
22
+ - Added regression coverage for status credit rounding, parallel status refresh, status progress messages, colorized stats/history output, readable duration formatting, and active-session markers.
23
+ - Increased the Python test suite to 262 tests.
24
+
25
+ ## Release Metadata
26
+
27
+ - Updated package metadata, CLI version output, README badge, pinned installer example, and release changelog to `v0.7.3`.
28
+
29
+ ## Validation and Regression Evidence
30
+
31
+ - `npm run lint`
32
+ - `npm test`
33
+ - `npm pack --dry-run`
34
+ - `node bin/cdx.js -v`
35
+ - `python3 -m unittest discover -s test`
36
+ - `python3 -m build`
37
+ - `python3 -m twine check dist/cdx_manager-0.7.3*`
@@ -36,6 +36,10 @@
36
36
  "v0.7.1": {
37
37
  "github_tarball_sha256": "276cf2f094405bef674290ff8d1f2401f99afc10981c97efe4fe8293801715c8",
38
38
  "github_zip_sha256": "5d5cdefec3ebcd61d4149b71895f4d5b79e604cf874b2d567910e578a8164670"
39
+ },
40
+ "v0.7.2": {
41
+ "github_tarball_sha256": "9e993b15386c7b47895cfaca9c7e92165967ef34ecf8337c64823a5b0f9eb9e0",
42
+ "github_zip_sha256": "1e5fd926a16811f84137636830e0b27776b9f8f8d2eb8953b620023c8bfe6fdd"
39
43
  }
40
44
  }
41
45
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cdx-manager",
3
- "version": "0.7.2",
3
+ "version": "0.7.3",
4
4
  "description": "Terminal session manager for Codex and Claude accounts.",
5
5
  "license": "MIT",
6
6
  "author": "Alexandre Agostini",
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "cdx-manager"
7
- version = "0.7.2"
7
+ version = "0.7.3"
8
8
  description = "Terminal session manager for Codex and Claude accounts."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
package/src/cli.py CHANGED
@@ -58,7 +58,7 @@ from .status_view import (
58
58
  )
59
59
  from .update_check import check_for_update
60
60
 
61
- VERSION = "0.7.2"
61
+ VERSION = "0.7.3"
62
62
 
63
63
 
64
64
  # ---------------------------------------------------------------------------
@@ -8,7 +8,7 @@ import time
8
8
  from datetime import datetime, timedelta
9
9
 
10
10
  from .claude_refresh import _refresh_claude_sessions
11
- from .cli_render import _dim, _info, _success, _warn
11
+ from .cli_render import _dim, _info, _style, _success, _warn
12
12
  from .config import PROVIDER_ANTIGRAVITY, PROVIDER_CLAUDE, PROVIDER_CODEX, PROVIDER_OLLAMA, PROVIDERS
13
13
  from .context_store import (
14
14
  clear_context,
@@ -180,15 +180,24 @@ def _make_export_progress(ctx):
180
180
 
181
181
 
182
182
  def _make_status_progress(ctx):
183
+ progress_state = {"checked": 0, "total": 0}
184
+
183
185
  def progress(event):
184
186
  kind = event.get("event")
185
187
  if kind == "status_started":
188
+ progress_state["checked"] = 0
189
+ progress_state["total"] = event.get("check_count", event.get("session_count", 0)) or 0
186
190
  message = f"Resolving status for {event.get('session_count', 0)} session(s)..."
187
191
  ctx["out"](f"{_info(message, ctx['use_color'])}\n")
188
192
  elif kind == "session_started":
189
193
  provider = event.get("provider") or "session"
190
194
  message = f"Checking {event.get('session_name')} ({provider})..."
191
195
  ctx["out"](f"{_dim(message, ctx['use_color'])}\n")
196
+ elif kind == "session_finished" and not event.get("cache_hit"):
197
+ progress_state["checked"] += 1
198
+ total = progress_state["total"] or progress_state["checked"]
199
+ message = f"Checked {event.get('session_name')} ({progress_state['checked']}/{total})."
200
+ ctx["out"](f"{_dim(message, ctx['use_color'])}\n")
192
201
  elif kind == "status_finished":
193
202
  message = f"Resolved {event.get('row_count', 0)} status row(s)."
194
203
  ctx["out"](f"{_dim(message, ctx['use_color'])}\n")
@@ -196,18 +205,27 @@ def _make_status_progress(ctx):
196
205
 
197
206
 
198
207
  def _make_notify_progress(ctx):
208
+ progress_state = {"checked": 0, "total": 0}
209
+
199
210
  def progress(event):
200
211
  kind = event.get("event")
201
212
  if kind == "notify_check_started":
202
213
  target = event.get("session_name") or "next ready session"
203
214
  ctx["out"](f"{_info(f'Checking notification target: {target}...', ctx['use_color'])}\n")
204
215
  elif kind == "status_started":
216
+ progress_state["checked"] = 0
217
+ progress_state["total"] = event.get("check_count", event.get("session_count", 0)) or 0
205
218
  message = f"Loading status for {event.get('session_count', 0)} session(s)..."
206
219
  ctx["out"](f"{_dim(message, ctx['use_color'])}\n")
207
220
  elif kind == "session_started":
208
221
  provider = event.get("provider") or "session"
209
222
  message = f"Checking {event.get('session_name')} ({provider})..."
210
223
  ctx["out"](f"{_dim(message, ctx['use_color'])}\n")
224
+ elif kind == "session_finished" and not event.get("cache_hit"):
225
+ progress_state["checked"] += 1
226
+ total = progress_state["total"] or progress_state["checked"]
227
+ message = f"Checked {event.get('session_name')} ({progress_state['checked']}/{total})."
228
+ ctx["out"](f"{_dim(message, ctx['use_color'])}\n")
211
229
  elif kind == "notify_waiting":
212
230
  message = f"{event.get('message')}; checking again in {event.get('poll')}s..."
213
231
  ctx["out"](f"{_dim(message, ctx['use_color'])}\n")
@@ -1529,7 +1547,15 @@ def _format_duration_ms(value):
1529
1547
  return f"{seconds:.1f}s"
1530
1548
  minutes = int(seconds // 60)
1531
1549
  remaining = int(seconds % 60)
1532
- return f"{minutes}m{remaining:02d}s"
1550
+ if minutes < 60:
1551
+ return f"{minutes}m {remaining:02d}s"
1552
+ hours = minutes // 60
1553
+ remaining_minutes = minutes % 60
1554
+ if hours < 24:
1555
+ return f"{hours}h {remaining_minutes:02d}m"
1556
+ days = hours // 24
1557
+ remaining_hours = hours % 24
1558
+ return f"{days}d {remaining_hours:02d}h"
1533
1559
 
1534
1560
 
1535
1561
  def _summarize_history(entries):
@@ -1656,49 +1682,57 @@ def _format_period_display(value):
1656
1682
  return parsed.strftime("%Y-%m-%d %H:%M")
1657
1683
 
1658
1684
 
1659
- def _format_history_summary(entries, period=None, use_color=False):
1685
+ def _format_history_summary(entries, period=None, use_color=False, active_sessions=None):
1660
1686
  from .cli_render import _format_relative_age, _pad_table
1661
1687
 
1688
+ active_sessions = active_sessions or set()
1662
1689
  summary = _summarize_history(entries)
1663
1690
  if not summary:
1664
1691
  return "No launch history for this period." if _has_history_period(period or {}) else "No launch history."
1665
- rows = [["SESSION", "PROV.", "LAUNCHES", "OK", "FAIL", "TIME", "LAST"]]
1692
+ rows = [[_style(value, "1", use_color) for value in ["SESSION", "PROV.", "LAUNCHES", "OK", "FAIL", "TIME", "LAST"]]]
1666
1693
  for row in summary:
1694
+ session_name = row["session_name"]
1695
+ display_name = f"{session_name}*" if session_name in active_sessions else session_name
1667
1696
  rows.append([
1668
- row["session_name"],
1669
- row["provider"],
1670
- str(row["launches"]),
1671
- str(row["successes"]),
1672
- str(row["failures"]),
1673
- _format_duration_ms(row["duration_ms"]),
1674
- _format_relative_age(row.get("last_started_at")),
1697
+ _style(display_name, "36", use_color),
1698
+ _dim(row["provider"], use_color),
1699
+ _style(str(row["launches"]), "1", use_color),
1700
+ _style(str(row["successes"]), "32" if row["successes"] else "2", use_color),
1701
+ _style(str(row["failures"]), "31" if row["failures"] else "2", use_color),
1702
+ _style(_format_duration_ms(row["duration_ms"]), "33" if row["duration_ms"] else "2", use_color),
1703
+ _dim(_format_relative_age(row.get("last_started_at")), use_color),
1675
1704
  ])
1676
- lines = ["Assistant time:"]
1705
+ lines = [_style("Assistant time:", "1", use_color)]
1677
1706
  period_line = _format_history_period(period or {})
1678
1707
  if period_line:
1679
- lines.extend([period_line, ""])
1708
+ lines.extend([_dim(period_line, use_color), ""])
1680
1709
  lines.append(_pad_table(rows))
1681
1710
  return "\n".join(lines)
1682
1711
 
1683
1712
 
1684
- def _format_history(entries, use_color=False):
1713
+ def _format_history(entries, use_color=False, active_sessions=None):
1685
1714
  from .cli_render import _format_relative_age, _pad_table
1686
1715
 
1716
+ active_sessions = active_sessions or set()
1687
1717
  if not entries:
1688
1718
  return "No launch history."
1689
- rows = [["SESSION", "PROV.", "RESULT", "DURATION", "WHEN", "TRANSCRIPT"]]
1719
+ rows = [[_style(value, "1", use_color) for value in ["SESSION", "PROV.", "RESULT", "DURATION", "WHEN", "TRANSCRIPT"]]]
1690
1720
  for entry in entries:
1691
1721
  transcript_path = entry.get("transcript_path")
1722
+ session_name = entry.get("session_name") or "-"
1723
+ display_name = f"{session_name}*" if session_name in active_sessions else session_name
1724
+ status = entry.get("status") or "-"
1725
+ status_color = "32" if status == "success" else "31" if status == "failed" else "2"
1692
1726
  rows.append([
1693
- entry.get("session_name") or "-",
1694
- entry.get("provider") or "-",
1695
- entry.get("status") or "-",
1696
- _format_duration_ms(entry.get("duration_ms")),
1697
- _format_relative_age(entry.get("started_at")),
1698
- os.path.basename(transcript_path) if transcript_path else "-",
1727
+ _style(display_name, "36", use_color),
1728
+ _dim(entry.get("provider") or "-", use_color),
1729
+ _style(status, status_color, use_color),
1730
+ _style(_format_duration_ms(entry.get("duration_ms")), "33" if entry.get("duration_ms") else "2", use_color),
1731
+ _dim(_format_relative_age(entry.get("started_at")), use_color),
1732
+ _dim(os.path.basename(transcript_path) if transcript_path else "-", use_color),
1699
1733
  ])
1700
1734
  return "\n".join([
1701
- "Recent launches:",
1735
+ _style("Recent launches:", "1", use_color),
1702
1736
  _pad_table(rows),
1703
1737
  "",
1704
1738
  _dim("Full transcript paths and cwd are available with --json.", use_color),
@@ -1717,29 +1751,34 @@ def _format_token_count(value):
1717
1751
  return str(amount)
1718
1752
 
1719
1753
 
1720
- def _format_stats(rows, totals, period=None, use_color=False):
1754
+ def _format_stats(rows, totals, period=None, use_color=False, active_sessions=None):
1721
1755
  from .cli_render import _format_relative_age, _pad_table
1722
1756
 
1757
+ active_sessions = active_sessions or set()
1723
1758
  if not rows:
1724
1759
  return "No launch stats for this period." if _has_history_period(period or {}) else "No launch stats."
1725
- table = [["SESSION", "PROV.", "RUNS", "USAGE", "IN", "OUT", "REASON", "TOTAL", "TIME", "LAST"]]
1760
+ table = [[_style(value, "1", use_color) for value in [
1761
+ "SESSION", "PROV.", "RUNS", "USAGE", "IN", "OUT", "REASON", "TOTAL", "TIME", "LAST"
1762
+ ]]]
1726
1763
  for row in rows:
1764
+ session_name = row["session_name"]
1765
+ display_name = f"{session_name}*" if session_name in active_sessions else session_name
1727
1766
  table.append([
1728
- row["session_name"],
1729
- row["provider"],
1730
- str(row["launches"]),
1731
- str(row["usage_runs"]),
1732
- _format_token_count(row["input_tokens"]),
1733
- _format_token_count(row["output_tokens"]),
1734
- _format_token_count(row["reasoning_tokens"]),
1735
- _format_token_count(row["total_tokens"]),
1736
- _format_duration_ms(row["duration_ms"]),
1737
- _format_relative_age(row.get("last_started_at")),
1767
+ _style(display_name, "36", use_color),
1768
+ _dim(row["provider"], use_color),
1769
+ _style(str(row["launches"]), "1", use_color),
1770
+ _style(str(row["usage_runs"]), "32" if row["usage_runs"] else "2", use_color),
1771
+ _style(_format_token_count(row["input_tokens"]), "96" if row["input_tokens"] else "2", use_color),
1772
+ _style(_format_token_count(row["output_tokens"]), "96" if row["output_tokens"] else "2", use_color),
1773
+ _style(_format_token_count(row["reasoning_tokens"]), "95" if row["reasoning_tokens"] else "2", use_color),
1774
+ _style(_format_token_count(row["total_tokens"]), "1;96" if row["total_tokens"] else "2", use_color),
1775
+ _style(_format_duration_ms(row["duration_ms"]), "33" if row["duration_ms"] else "2", use_color),
1776
+ _dim(_format_relative_age(row.get("last_started_at")), use_color),
1738
1777
  ])
1739
- lines = ["Assistant stats:"]
1778
+ lines = [_style("Assistant stats:", "1", use_color)]
1740
1779
  period_line = _format_history_period(period or {})
1741
1780
  if period_line:
1742
- lines.extend([period_line, ""])
1781
+ lines.extend([_dim(period_line, use_color), ""])
1743
1782
  lines.append(_pad_table(table))
1744
1783
  lines.extend([
1745
1784
  "",
@@ -1754,6 +1793,14 @@ def _format_stats(rows, totals, period=None, use_color=False):
1754
1793
  return "\n".join(lines)
1755
1794
 
1756
1795
 
1796
+ def _active_session_names(ctx):
1797
+ return {
1798
+ row["name"]
1799
+ for row in ctx["service"]["format_list_rows"]()
1800
+ if row.get("active")
1801
+ }
1802
+
1803
+
1757
1804
  def _apply_launch_settings(parsed, ctx, action="set"):
1758
1805
  targets = _resolve_bulk_launch_targets(parsed, ctx["service"])
1759
1806
  sessions = [
@@ -1859,10 +1906,11 @@ def handle_history(rest, ctx):
1859
1906
  payload["summary"] = _summarize_history(entries)
1860
1907
  _write_json(ctx, _json_success("history", message, **payload))
1861
1908
  return 0
1909
+ active_sessions = _active_session_names(ctx)
1862
1910
  if parsed["summary"]:
1863
- ctx["out"](f"{_format_history_summary(entries, period=parsed['period'], use_color=ctx['use_color'])}\n")
1911
+ ctx["out"](f"{_format_history_summary(entries, period=parsed['period'], use_color=ctx['use_color'], active_sessions=active_sessions)}\n")
1864
1912
  return 0
1865
- ctx["out"](f"{_format_history(entries, use_color=ctx['use_color'])}\n")
1913
+ ctx["out"](f"{_format_history(entries, use_color=ctx['use_color'], active_sessions=active_sessions)}\n")
1866
1914
  return 0
1867
1915
 
1868
1916
 
@@ -1888,7 +1936,8 @@ def handle_stats(rest, ctx):
1888
1936
  totals=totals,
1889
1937
  ))
1890
1938
  return 0
1891
- ctx["out"](f"{_format_stats(rows, totals, period=parsed['period'], use_color=ctx['use_color'])}\n")
1939
+ active_sessions = _active_session_names(ctx)
1940
+ ctx["out"](f"{_format_stats(rows, totals, period=parsed['period'], use_color=ctx['use_color'], active_sessions=active_sessions)}\n")
1892
1941
  return 0
1893
1942
 
1894
1943
 
@@ -5,6 +5,7 @@ import base64
5
5
  import sys
6
6
  import tempfile
7
7
  import uuid
8
+ from concurrent.futures import ThreadPoolExecutor, as_completed
8
9
  from datetime import datetime, timezone
9
10
  from urllib.parse import quote
10
11
 
@@ -55,6 +56,7 @@ RESERVED_SESSION_NAMES = {
55
56
  }
56
57
  STATUS_CACHE_TTL_SECONDS = 60
57
58
  CLAUDE_STATUS_CACHE_TTL_SECONDS = 10 * 60
59
+ MAX_STATUS_WORKERS = 8
58
60
  LAUNCH_POWER_VALUES = {"low", "medium", "high", "xhigh", "max"}
59
61
  LAUNCH_REASONING_EFFORT_VALUES = {"low", "medium", "high"}
60
62
  LAUNCH_PERMISSION_VALUES = {"review", "default", "auto", "full"}
@@ -939,14 +941,9 @@ def create_session_service(options=None):
939
941
 
940
942
  def get_status_rows(progress_callback=None, force_refresh=False, cache_ttl_seconds=STATUS_CACHE_TTL_SECONDS):
941
943
  sessions = list_sessions()
942
- if progress_callback:
943
- progress_callback({
944
- "event": "status_started",
945
- "session_count": len(sessions),
946
- })
947
- resolved = []
948
- for s in sessions:
949
- cache_hit = (
944
+
945
+ def _status_cache_hit(s):
946
+ return (
950
947
  s.get("enabled", True) is False
951
948
  or (
952
949
  s.get("lastStatus")
@@ -954,28 +951,56 @@ def create_session_service(options=None):
954
951
  and _is_status_cache_fresh(s, ttl_seconds=cache_ttl_seconds)
955
952
  )
956
953
  )
957
- if progress_callback and not cache_hit:
958
- progress_callback({
959
- "event": "session_started",
960
- "session_name": s["name"],
961
- "provider": s["provider"],
962
- })
954
+
955
+ cache_hits = {
956
+ s["name"]: _status_cache_hit(s)
957
+ for s in sessions
958
+ }
959
+ if progress_callback:
960
+ progress_callback({
961
+ "event": "status_started",
962
+ "session_count": len(sessions),
963
+ "check_count": sum(1 for cache_hit in cache_hits.values() if not cache_hit),
964
+ })
965
+
966
+ def _resolve_row_session(s):
963
967
  status = _resolve_session_status(
964
968
  s,
965
969
  force_refresh=force_refresh,
966
970
  cache_ttl_seconds=cache_ttl_seconds,
967
971
  )
968
- if progress_callback:
969
- progress_callback({
970
- "event": "session_finished",
971
- "session_name": s["name"],
972
- "has_status": bool(status),
973
- })
974
- resolved.append({
972
+ return {
975
973
  **s,
976
974
  "lastStatus": status,
977
975
  "lastStatusAt": (status and status.get("updated_at")) or s.get("lastStatusAt"),
978
- })
976
+ }
977
+
978
+ resolved_by_name = {}
979
+ if sessions:
980
+ max_workers = min(MAX_STATUS_WORKERS, len(sessions))
981
+ with ThreadPoolExecutor(max_workers=max_workers) as executor:
982
+ futures = {}
983
+ for s in sessions:
984
+ cache_hit = cache_hits[s["name"]]
985
+ if progress_callback and not cache_hit:
986
+ progress_callback({
987
+ "event": "session_started",
988
+ "session_name": s["name"],
989
+ "provider": s["provider"],
990
+ })
991
+ futures[executor.submit(_resolve_row_session, s)] = s
992
+ for future in as_completed(futures):
993
+ s = futures[future]
994
+ resolved = future.result()
995
+ resolved_by_name[s["name"]] = resolved
996
+ if progress_callback:
997
+ progress_callback({
998
+ "event": "session_finished",
999
+ "session_name": s["name"],
1000
+ "has_status": bool(resolved.get("lastStatus")),
1001
+ "cache_hit": cache_hits[s["name"]],
1002
+ })
1003
+ resolved = [resolved_by_name[s["name"]] for s in sessions]
979
1004
 
980
1005
  def sort_key(s):
981
1006
  at = s.get("lastStatusAt") or ""
@@ -1,4 +1,5 @@
1
1
  from datetime import datetime
2
+ from decimal import Decimal, InvalidOperation, ROUND_HALF_UP
2
3
 
3
4
  from .cli_render import (
4
5
  _dim,
@@ -61,6 +62,17 @@ def _style_reset_time(value, use_color=False):
61
62
  return text
62
63
 
63
64
 
65
+ def _format_credits(value, empty="n/a"):
66
+ if value is None:
67
+ return empty
68
+ try:
69
+ normalized = str(value).strip().replace(",", "")
70
+ rounded = Decimal(normalized).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
71
+ return f"{rounded:.2f}"
72
+ except (InvalidOperation, ValueError):
73
+ return str(value)
74
+
75
+
64
76
  def _format_status_rows(rows, use_color=False, small=False):
65
77
  has_provider = len({r["provider"] for r in rows}) > 1 and not small
66
78
  if small:
@@ -114,7 +126,7 @@ def _format_status_rows(rows, use_color=False, small=False):
114
126
  base += usage_columns
115
127
  else:
116
128
  block = "-" if r.get("enabled", True) is False else _format_blocking_quota(r)
117
- credits = str(r["credits"]) if r.get("credits") is not None else "-"
129
+ credits = _format_credits(r.get("credits"), empty="-")
118
130
  base += usage_columns[:3] + [
119
131
  _style(block, "33" if block not in ("?", "-") else "2", use_color),
120
132
  _style(credits, "33" if r.get("credits") is not None else "2", use_color),
@@ -332,7 +344,7 @@ def _format_status_detail(row, use_color=False):
332
344
  f"{_style('5h left:', '1', use_color)} {_style_pct(row.get('remaining_5h_pct'), use_color)}",
333
345
  f"{_style('Week left:', '1', use_color)} {_style_pct(row.get('remaining_week_pct'), use_color)}",
334
346
  f"{_style('Block:', '1', use_color)} {_style(_format_blocking_quota(row), '33', use_color)}",
335
- f"{_style('Credits:', '1', use_color)} {_style(row['credits'] if row.get('credits') is not None else 'n/a', '33' if row.get('credits') is not None else '2', use_color)}",
347
+ f"{_style('Credits:', '1', use_color)} {_style(_format_credits(row.get('credits')), '33' if row.get('credits') is not None else '2', use_color)}",
336
348
  f"{_style('5h reset:', '1', use_color)} {_style_reset_time(row.get('reset_5h_at'), use_color)}",
337
349
  f"{_style('Week reset:', '1', use_color)} {_style_reset_time(row.get('reset_week_at'), use_color)}",
338
350
  f"{_style('Updated:', '1', use_color)} {_dim(_format_relative_age(row.get('updated_at')), use_color)}",