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 +2 -2
- package/changelogs/CHANGELOGS_0_7_3.md +37 -0
- package/checksums/release-archives.json +4 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/cli.py +1 -1
- package/src/cli_commands.py +88 -39
- package/src/session_service.py +47 -22
- package/src/status_view.py +14 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# CDX Manager
|
|
2
2
|
|
|
3
|
-
[](LICENSE) ](LICENSE)  
|
|
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.
|
|
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
package/pyproject.toml
CHANGED
package/src/cli.py
CHANGED
package/src/cli_commands.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1694
|
-
entry.get("provider") or "-",
|
|
1695
|
-
|
|
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 = [[
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
package/src/session_service.py
CHANGED
|
@@ -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
|
-
|
|
943
|
-
|
|
944
|
-
|
|
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
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
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
|
-
|
|
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 ""
|
package/src/status_view.py
CHANGED
|
@@ -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 =
|
|
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
|
|
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)}",
|