cdx-manager 0.7.2 → 0.7.4

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.
@@ -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,
@@ -40,8 +40,9 @@ from .provider_runtime import (
40
40
  )
41
41
  from .repair import format_repair_report, repair_health
42
42
  from .backup_bundle import read_bundle_meta
43
- from .run_usage import empty_usage, extract_run_usage
44
- from .status_view import _format_status_detail, _format_status_rows
43
+ from .run_usage import extract_run_usage
44
+ from .run_command import read_run_prompt, run_cdx_error_code, run_result_payload
45
+ from .status_view import _format_status_detail, _format_status_rows, format_priority_instruction, recommend_priority_rows
45
46
  from .update_check import LatestReleaseCheckError, fetch_latest_release, fetch_latest_release_or_raise, is_newer_version
46
47
  from .update_manager import build_update_plan, format_update_failure, run_update_plan, verify_updated_command
47
48
 
@@ -58,10 +59,12 @@ SET_USAGE = "Usage: cdx set <name>|--sessions all|a,b|--provider PROVIDER [--pow
58
59
  UNSET_USAGE = "Usage: cdx unset <name>|--sessions all|a,b|--provider PROVIDER (--power|--permission|--fast|--rtk|--model|--priority|--all) [--json]"
59
60
  SETTING_ALIAS_USAGE = "Usage: cdx power|perm|fast|model <name|all|provider:PROVIDER|a,b> <value|default> [--json]"
60
61
  CONFIG_USAGE = "Usage: cdx config <name> [--json]"
62
+ CONFIGS_USAGE = "Usage: cdx configs [--json]"
61
63
  HISTORY_USAGE = "Usage: cdx history [name] [--limit N] [--summary] [--since 7d|today|DATE] [--from DATE] [--to DATE] [--json]"
62
64
  STATS_USAGE = "Usage: cdx stats [name] [--since 7d|today|DATE] [--from DATE] [--to DATE] [--json]"
63
65
  LAST_USAGE = "Usage: cdx last [--json]"
64
66
  SELECT_USAGE = "Usage: cdx select --provider PROVIDER [--min-reasoning-effort low|medium|high] [--min-power low|medium|high] [--require-ready] [--refresh] --json"
67
+ NEXT_USAGE = "Usage: cdx next [--json] [--refresh]"
65
68
  RUN_USAGE = "Usage: cdx run [session] --cwd PATH (--prompt-file PATH|--prompt TEXT) [--provider PROVIDER] [--model MODEL] [--reasoning-effort low|medium|high] [--power low|medium|high] [--permission review|default|auto|full|workspace-write|read-only|danger-full-access] [--timeout-seconds N] --json"
66
69
  API_SCHEMA_VERSION = 1
67
70
  HANDOFF_TRANSCRIPT_CHARS = 120000
@@ -180,15 +183,24 @@ def _make_export_progress(ctx):
180
183
 
181
184
 
182
185
  def _make_status_progress(ctx):
186
+ progress_state = {"checked": 0, "total": 0}
187
+
183
188
  def progress(event):
184
189
  kind = event.get("event")
185
190
  if kind == "status_started":
191
+ progress_state["checked"] = 0
192
+ progress_state["total"] = event.get("check_count", event.get("session_count", 0)) or 0
186
193
  message = f"Resolving status for {event.get('session_count', 0)} session(s)..."
187
194
  ctx["out"](f"{_info(message, ctx['use_color'])}\n")
188
195
  elif kind == "session_started":
189
196
  provider = event.get("provider") or "session"
190
197
  message = f"Checking {event.get('session_name')} ({provider})..."
191
198
  ctx["out"](f"{_dim(message, ctx['use_color'])}\n")
199
+ elif kind == "session_finished" and not event.get("cache_hit"):
200
+ progress_state["checked"] += 1
201
+ total = progress_state["total"] or progress_state["checked"]
202
+ message = f"Checked {event.get('session_name')} ({progress_state['checked']}/{total})."
203
+ ctx["out"](f"{_dim(message, ctx['use_color'])}\n")
192
204
  elif kind == "status_finished":
193
205
  message = f"Resolved {event.get('row_count', 0)} status row(s)."
194
206
  ctx["out"](f"{_dim(message, ctx['use_color'])}\n")
@@ -196,18 +208,27 @@ def _make_status_progress(ctx):
196
208
 
197
209
 
198
210
  def _make_notify_progress(ctx):
211
+ progress_state = {"checked": 0, "total": 0}
212
+
199
213
  def progress(event):
200
214
  kind = event.get("event")
201
215
  if kind == "notify_check_started":
202
216
  target = event.get("session_name") or "next ready session"
203
217
  ctx["out"](f"{_info(f'Checking notification target: {target}...', ctx['use_color'])}\n")
204
218
  elif kind == "status_started":
219
+ progress_state["checked"] = 0
220
+ progress_state["total"] = event.get("check_count", event.get("session_count", 0)) or 0
205
221
  message = f"Loading status for {event.get('session_count', 0)} session(s)..."
206
222
  ctx["out"](f"{_dim(message, ctx['use_color'])}\n")
207
223
  elif kind == "session_started":
208
224
  provider = event.get("provider") or "session"
209
225
  message = f"Checking {event.get('session_name')} ({provider})..."
210
226
  ctx["out"](f"{_dim(message, ctx['use_color'])}\n")
227
+ elif kind == "session_finished" and not event.get("cache_hit"):
228
+ progress_state["checked"] += 1
229
+ total = progress_state["total"] or progress_state["checked"]
230
+ message = f"Checked {event.get('session_name')} ({progress_state['checked']}/{total})."
231
+ ctx["out"](f"{_dim(message, ctx['use_color'])}\n")
211
232
  elif kind == "notify_waiting":
212
233
  message = f"{event.get('message')}; checking again in {event.get('poll')}s..."
213
234
  ctx["out"](f"{_dim(message, ctx['use_color'])}\n")
@@ -580,6 +601,13 @@ def _parse_config_args(args):
580
601
  return {"name": parsed["names"][0], "json": parsed["json"]}
581
602
 
582
603
 
604
+ def _parse_configs_args(args):
605
+ parsed = _parse_flag_args(args, {
606
+ "--json": {"key": "json", "type": "bool", "default": False},
607
+ }, CONFIGS_USAGE)
608
+ return {"json": parsed["json"]}
609
+
610
+
583
611
  def _parse_select_args(args):
584
612
  parsed = _parse_flag_args(args, {
585
613
  "--provider": {"key": "provider", "type": "str", "default": None, "transform": lambda value: _parse_provider_filter(value, SELECT_USAGE)},
@@ -605,6 +633,14 @@ def _parse_select_args(args):
605
633
  }
606
634
 
607
635
 
636
+ def _parse_next_args(args):
637
+ parsed = _parse_flag_args(args, {
638
+ "--refresh": {"key": "refresh", "type": "bool", "default": False},
639
+ "--json": {"key": "json", "type": "bool", "default": False},
640
+ }, NEXT_USAGE)
641
+ return {"json": parsed["json"], "refresh": parsed["refresh"]}
642
+
643
+
608
644
  def _parse_timeout_seconds(value):
609
645
  try:
610
646
  parsed = float(value)
@@ -1211,70 +1247,73 @@ def handle_select(rest, ctx):
1211
1247
  return 0
1212
1248
 
1213
1249
 
1214
- def _read_run_prompt(parsed):
1215
- if parsed.get("prompt") is not None:
1216
- return parsed["prompt"]
1217
- try:
1218
- with open(parsed["prompt_file"], "r", encoding="utf-8") as handle:
1219
- return handle.read()
1220
- except OSError as error:
1221
- raise CdxError(f"Unable to read prompt file: {parsed['prompt_file']}") from error
1222
-
1223
-
1224
- def _run_cdx_error_code(error):
1225
- message = str(error)
1226
- if message.startswith("Usage:"):
1227
- return "invalid_request"
1228
- if message.startswith("Invalid cwd:"):
1229
- return "invalid_cwd"
1230
- if message.startswith("Session is disabled:"):
1231
- return "session_disabled"
1232
- if "CLI not found on PATH" in message:
1233
- return "provider_cli_not_found"
1234
- if message.startswith("Failed to start "):
1235
- return "provider_start_failed"
1236
- if (
1237
- message.startswith("Unsupported reasoning effort:")
1238
- or message.startswith("Unsupported power:")
1239
- or "--reasoning-effort and --power must match" in message
1240
- ):
1241
- return "invalid_reasoning_effort"
1242
- return "cdx_error"
1243
-
1244
-
1245
- def _run_result_payload(ok, parsed, session, run_info=None, error=None, error_source=None, error_code=None):
1246
- run_info = run_info or {}
1247
- usage = run_info.get("usage") if isinstance(run_info.get("usage"), dict) else (
1248
- extract_run_usage(session.get("provider"), run_info.get("stdout_path"))
1249
- if session else
1250
- empty_usage()
1251
- )
1252
- return {
1253
- "schema_version": API_SCHEMA_VERSION,
1254
- "ok": bool(ok),
1255
- "action": "run",
1256
- "launcher": "cdx",
1257
- "session": session.get("name") if session else None,
1258
- "provider": session.get("provider") if session else parsed.get("provider"),
1259
- "model": parsed.get("model") or ((session.get("launch") or {}).get("model") if session else None),
1260
- "reasoning_effort": parsed.get("reasoning_effort") or ((session.get("launch") or {}).get("reasoning_effort") if session else None),
1261
- "power": parsed.get("power") or ((session.get("launch") or {}).get("power") if session else None),
1262
- "cwd": os.path.abspath(parsed.get("cwd") or os.getcwd()),
1263
- "run_id": run_info.get("run_id"),
1264
- "exit_code": run_info.get("returncode"),
1265
- "duration_seconds": (run_info.get("duration_ms") / 1000.0) if run_info.get("duration_ms") is not None else None,
1266
- "transcript_path": run_info.get("transcript_path"),
1267
- "stdout_path": run_info.get("stdout_path"),
1268
- "stderr_path": run_info.get("stderr_path"),
1269
- "usage": usage,
1270
- "warnings": [],
1271
- "error": None if ok else {
1272
- "source": error_source or "cdx",
1273
- "code": error_code or "cdx_error",
1274
- "message": str(error) if error else "Run failed.",
1275
- "provider_code": run_info.get("returncode") if error_source == "provider" else None,
1276
- },
1277
- }
1250
+ def _format_next_pct(value):
1251
+ return "n/a" if value is None else f"{value}%"
1252
+
1253
+
1254
+ def _next_action(row):
1255
+ instruction = format_priority_instruction(row, "first")
1256
+ return "refresh" if instruction.startswith("refresh ") else "use"
1257
+
1258
+
1259
+ def _format_next_selection(session, row, use_color=False):
1260
+ from .cli_render import _pad_table
1261
+
1262
+ action = _next_action(row)
1263
+ command = f"cdx status {session['name']} --refresh" if action == "refresh" else f"cdx {session['name']}"
1264
+ rows = [[_style(value, "1", use_color) for value in ["SESSION", "PROVIDER", "OK", "5H", "WEEK", "REASON"]]]
1265
+ rows.append([
1266
+ _style(session["name"], "36", use_color),
1267
+ _dim(session.get("provider") or "-", use_color),
1268
+ _format_next_pct(row.get("available_pct")),
1269
+ _format_next_pct(row.get("remaining_5h_pct")),
1270
+ _format_next_pct(row.get("remaining_week_pct")),
1271
+ format_priority_instruction(row, "first"),
1272
+ ])
1273
+ return "\n".join([
1274
+ _style("Next assistant:", "1", use_color),
1275
+ _pad_table(rows),
1276
+ "",
1277
+ f"{_style('Run:', '1', use_color)} {_style(command, '36', use_color)}",
1278
+ ])
1279
+
1280
+
1281
+ def handle_next(rest, ctx):
1282
+ parsed = _parse_next_args(rest)
1283
+ rows = ctx["service"]["get_status_rows"](force_refresh=parsed["refresh"])
1284
+ priority = recommend_priority_rows(rows)
1285
+ if not priority:
1286
+ message = "No usable session status yet."
1287
+ if parsed["json"]:
1288
+ _write_json(ctx, _json_failure("next", "no_suitable_session", message, selection_policy="status_priority"))
1289
+ return 1
1290
+ ctx["out"](f"{_warn(message, ctx['use_color'])}\n")
1291
+ return 1
1292
+ row = priority[0]
1293
+ session_name = row.get("session_name")
1294
+ session = ctx["service"]["get_session"](session_name)
1295
+ if not session:
1296
+ message = f"Selected status row has no matching session: {session_name}"
1297
+ if parsed["json"]:
1298
+ _write_json(ctx, _json_failure("next", "missing_session", message, selection_policy="status_priority", row=row))
1299
+ return 1
1300
+ raise CdxError(message)
1301
+ action = _next_action(row)
1302
+ message = f"Selected next session {session['name']}"
1303
+ if parsed["json"]:
1304
+ _write_json(ctx, _json_success(
1305
+ "next",
1306
+ message,
1307
+ session=session,
1308
+ row=row,
1309
+ recommended_action=action,
1310
+ command=f"cdx status {session['name']} --refresh" if action == "refresh" else f"cdx {session['name']}",
1311
+ reason=format_priority_instruction(row, "first"),
1312
+ selection_policy="status_priority",
1313
+ ))
1314
+ return 0
1315
+ ctx["out"](f"{_format_next_selection(session, row, ctx['use_color'])}\n")
1316
+ return 0
1278
1317
 
1279
1318
 
1280
1319
  def handle_run(rest, ctx):
@@ -1309,7 +1348,7 @@ def handle_run(rest, ctx):
1309
1348
  cwd = os.path.abspath(parsed["cwd"])
1310
1349
  if not os.path.isdir(cwd):
1311
1350
  raise CdxError(f"Invalid cwd: {parsed['cwd']}")
1312
- prompt = _read_run_prompt(parsed)
1351
+ prompt = read_run_prompt(parsed)
1313
1352
  launch_updates = {}
1314
1353
  if parsed.get("model"):
1315
1354
  launch_updates["model"] = parsed["model"]
@@ -1334,6 +1373,7 @@ def handle_run(rest, ctx):
1334
1373
  stdin_is_tty=False,
1335
1374
  behavior="launch",
1336
1375
  signal_emitter=ctx.get("signal_emitter"),
1376
+ trust_local_credentials=False,
1337
1377
  )
1338
1378
  run_info = _run_headless_provider_command(
1339
1379
  run_session,
@@ -1353,7 +1393,7 @@ def handle_run(rest, ctx):
1353
1393
  "exit_code": 0,
1354
1394
  **run_info,
1355
1395
  })
1356
- _write_json(ctx, _run_result_payload(True, parsed, run_session, run_info=run_info))
1396
+ _write_json(ctx, run_result_payload(API_SCHEMA_VERSION, True, parsed, run_session, run_info=run_info))
1357
1397
  return 0
1358
1398
  message = "Provider process timed out." if run_info.get("timed_out") else "Provider process exited with a non-zero status."
1359
1399
  error = CdxError(message, run_info.get("returncode") or 1)
@@ -1364,7 +1404,8 @@ def handle_run(rest, ctx):
1364
1404
  "exit_code": error.exit_code,
1365
1405
  **run_info,
1366
1406
  })
1367
- _write_json(ctx, _run_result_payload(
1407
+ _write_json(ctx, run_result_payload(
1408
+ API_SCHEMA_VERSION,
1368
1409
  False,
1369
1410
  parsed,
1370
1411
  run_session,
@@ -1376,28 +1417,102 @@ def handle_run(rest, ctx):
1376
1417
  return error.exit_code or 1
1377
1418
  except CdxError as error:
1378
1419
  run_info = getattr(error, "run_info", None)
1379
- _write_json(ctx, _run_result_payload(
1420
+ _write_json(ctx, run_result_payload(
1421
+ API_SCHEMA_VERSION,
1380
1422
  False,
1381
1423
  locals().get("parsed", {}) or {},
1382
1424
  locals().get("session"),
1383
1425
  run_info=run_info,
1384
1426
  error=error,
1385
1427
  error_source="cdx",
1386
- error_code=_run_cdx_error_code(error),
1428
+ error_code=run_cdx_error_code(error),
1387
1429
  ))
1388
1430
  return error.exit_code
1389
1431
 
1390
1432
 
1391
- def _format_launch_config(session):
1433
+ def _format_launch_config(session, use_color=False):
1434
+ from .cli_render import _dim, _pad_table, _style
1435
+
1392
1436
  launch = session.get("launch") or {}
1437
+ rows = [[_style("SETTING", "1", use_color), _style("VALUE", "1", use_color)]]
1438
+ for key, label in [
1439
+ ("power", "Power"),
1440
+ ("permission", "Permission"),
1441
+ ("fast", "Fast"),
1442
+ ("rtk", "RTK"),
1443
+ ("model", "Model"),
1444
+ ("priority", "Priority"),
1445
+ ]:
1446
+ rows.append([
1447
+ _dim(label, use_color),
1448
+ _format_launch_setting_value(launch, key, use_color=use_color),
1449
+ ])
1450
+ provider_label = f"({session['provider']})"
1393
1451
  return "\n".join([
1394
- f"{session['name']} ({session['provider']})",
1395
- f"power: {launch.get('power') or 'default'}",
1396
- f"permission: {launch.get('permission') or 'default'}",
1397
- f"fast: {'on' if launch.get('fast') is True else 'off' if launch.get('fast') is False else 'default'}",
1398
- f"rtk: {'on' if launch.get('rtk') is True else 'off' if launch.get('rtk') is False else 'default'}",
1399
- f"model: {launch.get('model') or 'default'}",
1400
- f"priority: {launch.get('priority') if launch.get('priority') is not None else 'default'}",
1452
+ _style("Launch settings:", "1", use_color),
1453
+ f"{_style(session['name'], '36', use_color)} {_dim(provider_label, use_color)}",
1454
+ _pad_table(rows),
1455
+ "",
1456
+ _dim(_format_launch_settings_hint(session["name"]), use_color),
1457
+ ])
1458
+
1459
+
1460
+ def _format_launch_settings_hint(name="<name>"):
1461
+ return (
1462
+ f"Set a value: cdx set {name} --power medium --permission auto "
1463
+ "--fast on --rtk on --model MODEL --priority 80"
1464
+ )
1465
+
1466
+
1467
+ def _format_launch_setting_value(launch, key, use_color=False):
1468
+ if key == "fast" or key == "rtk":
1469
+ if launch.get(key) is True:
1470
+ return _style("on", "32", use_color)
1471
+ if launch.get(key) is False:
1472
+ return _style("off", "2", use_color)
1473
+ return _dim("default", use_color)
1474
+ value = launch.get(key)
1475
+ if value is None or value == "":
1476
+ return _dim("default", use_color)
1477
+ if key == "priority":
1478
+ return _style(str(value), "33", use_color)
1479
+ if key == "power":
1480
+ return _style(str(value), "96", use_color)
1481
+ if key == "permission":
1482
+ return _style(str(value), "32", use_color)
1483
+ return str(value)
1484
+
1485
+
1486
+ def _format_launch_configs(sessions, use_color=False):
1487
+ from .cli_render import _dim, _pad_table, _style
1488
+
1489
+ if not sessions:
1490
+ return "\n".join([
1491
+ _style("Launch settings:", "1", use_color),
1492
+ "No sessions.",
1493
+ "",
1494
+ _dim(_format_launch_settings_hint(), use_color),
1495
+ ])
1496
+ rows = [[_style(value, "1", use_color) for value in [
1497
+ "SESSION", "PROVIDER", "POWER", "PERMISSION", "FAST", "RTK", "MODEL", "PRIORITY"
1498
+ ]]]
1499
+ for session in sessions:
1500
+ launch = session.get("launch") or {}
1501
+ rows.append([
1502
+ _style(session["name"], "36", use_color),
1503
+ _dim(session.get("provider") or "-", use_color),
1504
+ _format_launch_setting_value(launch, "power", use_color),
1505
+ _format_launch_setting_value(launch, "permission", use_color),
1506
+ _format_launch_setting_value(launch, "fast", use_color),
1507
+ _format_launch_setting_value(launch, "rtk", use_color),
1508
+ _format_launch_setting_value(launch, "model", use_color),
1509
+ _format_launch_setting_value(launch, "priority", use_color),
1510
+ ])
1511
+ return "\n".join([
1512
+ _style("Launch settings:", "1", use_color),
1513
+ _pad_table(rows),
1514
+ "",
1515
+ _dim(_format_launch_settings_hint(), use_color),
1401
1516
  ])
1402
1517
 
1403
1518
 
@@ -1529,7 +1644,15 @@ def _format_duration_ms(value):
1529
1644
  return f"{seconds:.1f}s"
1530
1645
  minutes = int(seconds // 60)
1531
1646
  remaining = int(seconds % 60)
1532
- return f"{minutes}m{remaining:02d}s"
1647
+ if minutes < 60:
1648
+ return f"{minutes}m {remaining:02d}s"
1649
+ hours = minutes // 60
1650
+ remaining_minutes = minutes % 60
1651
+ if hours < 24:
1652
+ return f"{hours}h {remaining_minutes:02d}m"
1653
+ days = hours // 24
1654
+ remaining_hours = hours % 24
1655
+ return f"{days}d {remaining_hours:02d}h"
1533
1656
 
1534
1657
 
1535
1658
  def _summarize_history(entries):
@@ -1656,49 +1779,57 @@ def _format_period_display(value):
1656
1779
  return parsed.strftime("%Y-%m-%d %H:%M")
1657
1780
 
1658
1781
 
1659
- def _format_history_summary(entries, period=None, use_color=False):
1782
+ def _format_history_summary(entries, period=None, use_color=False, active_sessions=None):
1660
1783
  from .cli_render import _format_relative_age, _pad_table
1661
1784
 
1785
+ active_sessions = active_sessions or set()
1662
1786
  summary = _summarize_history(entries)
1663
1787
  if not summary:
1664
1788
  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"]]
1789
+ rows = [[_style(value, "1", use_color) for value in ["SESSION", "PROV.", "LAUNCHES", "OK", "FAIL", "TIME", "LAST"]]]
1666
1790
  for row in summary:
1791
+ session_name = row["session_name"]
1792
+ display_name = f"{session_name}*" if session_name in active_sessions else session_name
1667
1793
  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")),
1794
+ _style(display_name, "36", use_color),
1795
+ _dim(row["provider"], use_color),
1796
+ _style(str(row["launches"]), "1", use_color),
1797
+ _style(str(row["successes"]), "32" if row["successes"] else "2", use_color),
1798
+ _style(str(row["failures"]), "31" if row["failures"] else "2", use_color),
1799
+ _style(_format_duration_ms(row["duration_ms"]), "33" if row["duration_ms"] else "2", use_color),
1800
+ _dim(_format_relative_age(row.get("last_started_at")), use_color),
1675
1801
  ])
1676
- lines = ["Assistant time:"]
1802
+ lines = [_style("Assistant time:", "1", use_color)]
1677
1803
  period_line = _format_history_period(period or {})
1678
1804
  if period_line:
1679
- lines.extend([period_line, ""])
1805
+ lines.extend([_dim(period_line, use_color), ""])
1680
1806
  lines.append(_pad_table(rows))
1681
1807
  return "\n".join(lines)
1682
1808
 
1683
1809
 
1684
- def _format_history(entries, use_color=False):
1810
+ def _format_history(entries, use_color=False, active_sessions=None):
1685
1811
  from .cli_render import _format_relative_age, _pad_table
1686
1812
 
1813
+ active_sessions = active_sessions or set()
1687
1814
  if not entries:
1688
1815
  return "No launch history."
1689
- rows = [["SESSION", "PROV.", "RESULT", "DURATION", "WHEN", "TRANSCRIPT"]]
1816
+ rows = [[_style(value, "1", use_color) for value in ["SESSION", "PROV.", "RESULT", "DURATION", "WHEN", "TRANSCRIPT"]]]
1690
1817
  for entry in entries:
1691
1818
  transcript_path = entry.get("transcript_path")
1819
+ session_name = entry.get("session_name") or "-"
1820
+ display_name = f"{session_name}*" if session_name in active_sessions else session_name
1821
+ status = entry.get("status") or "-"
1822
+ status_color = "32" if status == "success" else "31" if status == "failed" else "2"
1692
1823
  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 "-",
1824
+ _style(display_name, "36", use_color),
1825
+ _dim(entry.get("provider") or "-", use_color),
1826
+ _style(status, status_color, use_color),
1827
+ _style(_format_duration_ms(entry.get("duration_ms")), "33" if entry.get("duration_ms") else "2", use_color),
1828
+ _dim(_format_relative_age(entry.get("started_at")), use_color),
1829
+ _dim(os.path.basename(transcript_path) if transcript_path else "-", use_color),
1699
1830
  ])
1700
1831
  return "\n".join([
1701
- "Recent launches:",
1832
+ _style("Recent launches:", "1", use_color),
1702
1833
  _pad_table(rows),
1703
1834
  "",
1704
1835
  _dim("Full transcript paths and cwd are available with --json.", use_color),
@@ -1717,29 +1848,34 @@ def _format_token_count(value):
1717
1848
  return str(amount)
1718
1849
 
1719
1850
 
1720
- def _format_stats(rows, totals, period=None, use_color=False):
1851
+ def _format_stats(rows, totals, period=None, use_color=False, active_sessions=None):
1721
1852
  from .cli_render import _format_relative_age, _pad_table
1722
1853
 
1854
+ active_sessions = active_sessions or set()
1723
1855
  if not rows:
1724
1856
  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"]]
1857
+ table = [[_style(value, "1", use_color) for value in [
1858
+ "SESSION", "PROV.", "RUNS", "USAGE", "IN", "OUT", "REASON", "TOTAL", "TIME", "LAST"
1859
+ ]]]
1726
1860
  for row in rows:
1861
+ session_name = row["session_name"]
1862
+ display_name = f"{session_name}*" if session_name in active_sessions else session_name
1727
1863
  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")),
1864
+ _style(display_name, "36", use_color),
1865
+ _dim(row["provider"], use_color),
1866
+ _style(str(row["launches"]), "1", use_color),
1867
+ _style(str(row["usage_runs"]), "32" if row["usage_runs"] else "2", use_color),
1868
+ _style(_format_token_count(row["input_tokens"]), "96" if row["input_tokens"] else "2", use_color),
1869
+ _style(_format_token_count(row["output_tokens"]), "96" if row["output_tokens"] else "2", use_color),
1870
+ _style(_format_token_count(row["reasoning_tokens"]), "95" if row["reasoning_tokens"] else "2", use_color),
1871
+ _style(_format_token_count(row["total_tokens"]), "1;96" if row["total_tokens"] else "2", use_color),
1872
+ _style(_format_duration_ms(row["duration_ms"]), "33" if row["duration_ms"] else "2", use_color),
1873
+ _dim(_format_relative_age(row.get("last_started_at")), use_color),
1738
1874
  ])
1739
- lines = ["Assistant stats:"]
1875
+ lines = [_style("Assistant stats:", "1", use_color)]
1740
1876
  period_line = _format_history_period(period or {})
1741
1877
  if period_line:
1742
- lines.extend([period_line, ""])
1878
+ lines.extend([_dim(period_line, use_color), ""])
1743
1879
  lines.append(_pad_table(table))
1744
1880
  lines.extend([
1745
1881
  "",
@@ -1754,6 +1890,14 @@ def _format_stats(rows, totals, period=None, use_color=False):
1754
1890
  return "\n".join(lines)
1755
1891
 
1756
1892
 
1893
+ def _active_session_names(ctx):
1894
+ return {
1895
+ row["name"]
1896
+ for row in ctx["service"]["format_list_rows"]()
1897
+ if row.get("active")
1898
+ }
1899
+
1900
+
1757
1901
  def _apply_launch_settings(parsed, ctx, action="set"):
1758
1902
  targets = _resolve_bulk_launch_targets(parsed, ctx["service"])
1759
1903
  sessions = [
@@ -1778,7 +1922,7 @@ def _apply_launch_settings(parsed, ctx, action="set"):
1778
1922
  return 0
1779
1923
  ctx["out"](f"{_success(message, ctx['use_color'])}\n")
1780
1924
  if len(sessions) == 1:
1781
- ctx["out"](f"{_format_launch_config(sessions[0])}\n")
1925
+ ctx["out"](f"{_format_launch_config(sessions[0], ctx['use_color'])}\n")
1782
1926
  return 0
1783
1927
 
1784
1928
 
@@ -1806,7 +1950,7 @@ def _clear_launch_settings(parsed, ctx, action="unset"):
1806
1950
  return 0
1807
1951
  ctx["out"](f"{_success(message, ctx['use_color'])}\n")
1808
1952
  if len(sessions) == 1:
1809
- ctx["out"](f"{_format_launch_config(sessions[0])}\n")
1953
+ ctx["out"](f"{_format_launch_config(sessions[0], ctx['use_color'])}\n")
1810
1954
  return 0
1811
1955
 
1812
1956
 
@@ -1834,7 +1978,18 @@ def handle_config(rest, ctx):
1834
1978
  if parsed["json"]:
1835
1979
  _write_json(ctx, _json_success("config", message, session=session, launch=session.get("launch") or {}))
1836
1980
  return 0
1837
- ctx["out"](f"{_format_launch_config(session)}\n")
1981
+ ctx["out"](f"{_format_launch_config(session, ctx['use_color'])}\n")
1982
+ return 0
1983
+
1984
+
1985
+ def handle_configs(rest, ctx):
1986
+ parsed = _parse_configs_args(rest)
1987
+ sessions = ctx["service"]["list_sessions"]()
1988
+ message = f"Listed launch settings for {len(sessions)} session{'s' if len(sessions) != 1 else ''}"
1989
+ if parsed["json"]:
1990
+ _write_json(ctx, _json_success("configs", message, count=len(sessions), sessions=sessions))
1991
+ return 0
1992
+ ctx["out"](f"{_format_launch_configs(sessions, ctx['use_color'])}\n")
1838
1993
  return 0
1839
1994
 
1840
1995
 
@@ -1859,10 +2014,11 @@ def handle_history(rest, ctx):
1859
2014
  payload["summary"] = _summarize_history(entries)
1860
2015
  _write_json(ctx, _json_success("history", message, **payload))
1861
2016
  return 0
2017
+ active_sessions = _active_session_names(ctx)
1862
2018
  if parsed["summary"]:
1863
- ctx["out"](f"{_format_history_summary(entries, period=parsed['period'], use_color=ctx['use_color'])}\n")
2019
+ ctx["out"](f"{_format_history_summary(entries, period=parsed['period'], use_color=ctx['use_color'], active_sessions=active_sessions)}\n")
1864
2020
  return 0
1865
- ctx["out"](f"{_format_history(entries, use_color=ctx['use_color'])}\n")
2021
+ ctx["out"](f"{_format_history(entries, use_color=ctx['use_color'], active_sessions=active_sessions)}\n")
1866
2022
  return 0
1867
2023
 
1868
2024
 
@@ -1888,7 +2044,8 @@ def handle_stats(rest, ctx):
1888
2044
  totals=totals,
1889
2045
  ))
1890
2046
  return 0
1891
- ctx["out"](f"{_format_stats(rows, totals, period=parsed['period'], use_color=ctx['use_color'])}\n")
2047
+ active_sessions = _active_session_names(ctx)
2048
+ ctx["out"](f"{_format_stats(rows, totals, period=parsed['period'], use_color=ctx['use_color'], active_sessions=active_sessions)}\n")
1892
2049
  return 0
1893
2050
 
1894
2051
 
package/src/cli_render.py CHANGED
@@ -146,6 +146,9 @@ def _format_sessions(service, use_color=False):
146
146
  lines += [
147
147
  _style("Next actions:", "1", use_color),
148
148
  f" {_style('cdx status', '36', use_color)}",
149
+ f" {_style('cdx next', '36', use_color)}",
150
+ f" {_style('cdx configs', '36', use_color)}",
151
+ f" {_style('cdx stats', '36', use_color)}",
149
152
  f" {_style('cdx ready', '36', use_color)}",
150
153
  f" {_style('cdx perm all default', '36', use_color)}",
151
154
  f" {_style('cdx handoff <source> <target>', '36', use_color)}",