zwarm 3.0.1__py3-none-any.whl → 3.2.0__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.
zwarm/cli/main.py CHANGED
@@ -740,8 +740,14 @@ def init(
740
740
  [bold]Creates:[/]
741
741
  [cyan].zwarm/[/] State directory for sessions and events
742
742
  [cyan].zwarm/config.toml[/] Runtime settings (weave, adapter, watchers)
743
+ [cyan].zwarm/codex.toml[/] Codex CLI settings (model, reasoning effort)
743
744
  [cyan]zwarm.yaml[/] Project config (optional, with --with-project)
744
745
 
746
+ [bold]Configuration relationship:[/]
747
+ config.toml → Controls zwarm itself (tracing, which watchers run)
748
+ codex.toml → Controls the Codex CLI that runs executor sessions
749
+ zwarm.yaml → Project-specific context injected into orchestrator
750
+
745
751
  [bold]Examples:[/]
746
752
  [dim]# Interactive setup[/]
747
753
  $ zwarm init
@@ -788,6 +794,9 @@ def init(
788
794
  create_project_config = with_project
789
795
  project_description = ""
790
796
  project_context = ""
797
+ # Codex settings
798
+ codex_model = "gpt-5.1-codex-mini"
799
+ codex_reasoning = "high"
791
800
 
792
801
  if not non_interactive:
793
802
  console.print("[bold]Configuration[/]\n")
@@ -806,6 +815,42 @@ def init(
806
815
  type=str,
807
816
  )
808
817
 
818
+ # Codex model settings
819
+ console.print("\n [bold]Codex Model Settings[/] (.zwarm/codex.toml)")
820
+ console.print(" [dim]These control the underlying Codex CLI that runs executor sessions[/]\n")
821
+
822
+ console.print(" Available models:")
823
+ console.print(" [cyan]1[/] gpt-5.1-codex-mini [dim]- Fast, cheap, good for most tasks (Recommended)[/]")
824
+ console.print(" [cyan]2[/] gpt-5.1-codex [dim]- Balanced speed and capability[/]")
825
+ console.print(" [cyan]3[/] gpt-5.1-codex-max [dim]- Most capable, 400k context, expensive[/]")
826
+
827
+ model_choice = typer.prompt(
828
+ " Select model (1-3)",
829
+ default="1",
830
+ type=str,
831
+ )
832
+ model_map = {
833
+ "1": "gpt-5.1-codex-mini",
834
+ "2": "gpt-5.1-codex",
835
+ "3": "gpt-5.1-codex-max",
836
+ }
837
+ codex_model = model_map.get(model_choice, model_choice)
838
+ if model_choice not in model_map:
839
+ console.print(f" [dim]Using custom model: {codex_model}[/]")
840
+
841
+ console.print("\n Reasoning effort (how much the model \"thinks\" before responding):")
842
+ console.print(" [cyan]1[/] low [dim]- Minimal reasoning, fastest responses[/]")
843
+ console.print(" [cyan]2[/] medium [dim]- Balanced reasoning[/]")
844
+ console.print(" [cyan]3[/] high [dim]- Maximum reasoning, best for complex tasks (Recommended)[/]")
845
+
846
+ reasoning_choice = typer.prompt(
847
+ " Select reasoning effort (1-3)",
848
+ default="3",
849
+ type=str,
850
+ )
851
+ reasoning_map = {"1": "low", "2": "medium", "3": "high"}
852
+ codex_reasoning = reasoning_map.get(reasoning_choice, "high")
853
+
809
854
  # Watchers
810
855
  console.print("\n [bold]Watchers[/] (trajectory aligners)")
811
856
  available_watchers = ["progress", "budget", "delegation", "delegation_reminder", "scope", "pattern", "quality"]
@@ -857,10 +902,20 @@ def init(
857
902
 
858
903
  # Create codex.toml for isolated codex configuration
859
904
  codex_toml_path = state_dir / "codex.toml"
860
- if not codex_toml_path.exists():
861
- codex_content = _generate_codex_toml()
905
+ write_codex_toml = True
906
+ if codex_toml_path.exists():
907
+ if not non_interactive:
908
+ overwrite_codex = typer.confirm(" .zwarm/codex.toml exists. Overwrite?", default=False)
909
+ if not overwrite_codex:
910
+ write_codex_toml = False
911
+ console.print(" [dim]Skipping codex.toml[/]")
912
+ else:
913
+ write_codex_toml = False # Don't overwrite in non-interactive mode
914
+
915
+ if write_codex_toml:
916
+ codex_content = _generate_codex_toml(model=codex_model, reasoning_effort=codex_reasoning)
862
917
  codex_toml_path.write_text(codex_content)
863
- console.print(f" [green]✓[/] Created .zwarm/codex.toml (isolated codex config)")
918
+ console.print(f" [green]✓[/] Created .zwarm/codex.toml")
864
919
 
865
920
  # Create zwarm.yaml
866
921
  if create_project_config:
@@ -880,11 +935,23 @@ def init(
880
935
 
881
936
  # Summary
882
937
  console.print("\n[bold green]Done![/] zwarm is ready.\n")
938
+
939
+ # Explain config files
940
+ console.print("[bold]Configuration files:[/]")
941
+ console.print(" [cyan].zwarm/config.toml[/] - Runtime settings (Weave tracing, watchers)")
942
+ console.print(" [cyan].zwarm/codex.toml[/] - Codex CLI settings (model, reasoning effort)")
943
+ if create_project_config:
944
+ console.print(" [cyan]zwarm.yaml[/] - Project context and constraints")
945
+ console.print()
946
+ console.print(" [dim]Edit these files to customize behavior. Run 'zwarm init' again to reconfigure.[/]\n")
947
+
883
948
  console.print("[bold]Next steps:[/]")
884
949
  console.print(" [dim]# Run the orchestrator[/]")
885
950
  console.print(" $ zwarm orchestrate --task \"Your task here\"\n")
886
951
  console.print(" [dim]# Or test an executor directly[/]")
887
952
  console.print(" $ zwarm exec --task \"What is 2+2?\"\n")
953
+ console.print(" [dim]# Interactive session management[/]")
954
+ console.print(" $ zwarm interactive\n")
888
955
 
889
956
 
890
957
  def _generate_config_toml(
@@ -1240,897 +1307,183 @@ def clean(
1240
1307
  def interactive(
1241
1308
  default_dir: Annotated[Path, typer.Option("--dir", "-d", help="Default working directory")] = Path("."),
1242
1309
  model: Annotated[Optional[str], typer.Option("--model", help="Default model override")] = None,
1243
- adapter: Annotated[str, typer.Option("--adapter", "-a", help="Executor adapter")] = "codex_mcp",
1244
- state_dir: Annotated[Path, typer.Option("--state-dir", help="State directory for persistence")] = Path(".zwarm"),
1245
1310
  ):
1246
1311
  """
1247
- Universal multi-agent CLI for commanding coding agents.
1248
-
1249
- Spawn multiple agents across different directories, manage them interactively,
1250
- and view their outputs. You are the orchestrator.
1312
+ Interactive REPL for session management.
1251
1313
 
1252
- This uses the SAME code path as `zwarm orchestrate` - the adapter layer.
1253
- Interactive is the human-in-the-loop version, orchestrate is the LLM version.
1314
+ A clean, autocomplete-enabled interface for managing codex sessions.
1315
+ Tab-complete session IDs, watch live output, and manage the session lifecycle.
1254
1316
 
1255
1317
  [bold]Commands:[/]
1256
- [cyan]spawn[/] "task" [opts] Start a coding agent session (sync mode)
1257
- [cyan]async[/] "task" [opts] Start async session (fire-and-forget)
1258
- [cyan]ls[/] / [cyan]list[/] Dashboard of all sessions
1259
- [cyan]?[/] ID Quick peek (status + latest message)
1260
- [cyan]show[/] ID Full session details & history
1261
- [cyan]traj[/] ID Show trajectory (all steps taken)
1262
- [cyan]c[/] / [cyan]continue[/] ID "msg" Continue a sync conversation
1263
- [cyan]kill[/] ID Stop a session (keeps in history)
1264
- [cyan]rm[/] ID Delete session entirely
1265
- [cyan]killall[/] Stop all running sessions
1266
- [cyan]clean[/] Remove old sessions (>7 days)
1267
- [cyan]q[/] / [cyan]quit[/] Exit
1268
-
1269
- [bold]Spawn Options:[/]
1270
- spawn "task" --dir ~/project --model gpt-5.1-codex-max --async
1318
+ [cyan]spawn[/] "task" Start a new session
1319
+ [cyan]ls[/] Dashboard of all sessions
1320
+ [cyan]peek[/] ID / [cyan]?[/] ID Quick status check
1321
+ [cyan]show[/] ID Full session details
1322
+ [cyan]traj[/] ID Show trajectory (steps taken)
1323
+ [cyan]watch[/] ID Live follow session output
1324
+ [cyan]c[/] ID "msg" Continue conversation
1325
+ [cyan]kill[/] ID | all Stop session(s)
1326
+ [cyan]rm[/] ID | all Delete session(s)
1271
1327
 
1272
1328
  [bold]Examples:[/]
1273
1329
  $ zwarm interactive
1274
- > spawn "Build auth module" --dir ~/api
1275
- > spawn "Fix tests" --dir ~/api
1276
- > c abc123 "Now add error handling"
1330
+ > spawn "Build auth module"
1331
+ > watch abc123
1332
+ > c abc123 "Now add tests"
1277
1333
  > ls
1278
- > ? abc123
1279
1334
  """
1280
- from zwarm.adapters import get_adapter, list_adapters
1281
- from zwarm.core.models import ConversationSession, SessionStatus, SessionMode
1282
- import argparse
1335
+ from zwarm.cli.interactive import run_interactive
1283
1336
 
1284
- # Initialize adapter (same as orchestrator uses)
1285
1337
  default_model = model or "gpt-5.1-codex-mini"
1286
- default_adapter = adapter
1287
-
1288
- # Config path for isolated codex configuration
1289
- codex_config_path = state_dir / "codex.toml"
1290
- if not codex_config_path.exists():
1291
- # Try relative to working dir
1292
- codex_config_path = default_dir / ".zwarm" / "codex.toml"
1293
- if not codex_config_path.exists():
1294
- codex_config_path = None # Fall back to overrides
1295
-
1296
- # Session tracking - same pattern as orchestrator
1297
- sessions: dict[str, ConversationSession] = {}
1298
- adapters_cache: dict[str, Any] = {}
1299
-
1300
- def get_or_create_adapter(adapter_name: str, session_model: str | None = None) -> Any:
1301
- """Get or create an adapter instance with isolated config."""
1302
- cache_key = f"{adapter_name}:{session_model or default_model}"
1303
- if cache_key not in adapters_cache:
1304
- adapters_cache[cache_key] = get_adapter(
1305
- adapter_name,
1306
- model=session_model or default_model,
1307
- config_path=codex_config_path,
1308
- )
1309
- return adapters_cache[cache_key]
1310
-
1311
- def find_session(query: str) -> tuple[ConversationSession | None, str | None]:
1312
- """Find session by full or partial ID."""
1313
- if query in sessions:
1314
- return sessions[query], query
1315
- for sid, session in sessions.items():
1316
- if sid.startswith(query):
1317
- return session, sid
1318
- return None, None
1319
-
1320
- console.print("\n[bold cyan]zwarm interactive[/] - Multi-Agent Command Center\n")
1321
- console.print(f" Working dir: {default_dir.absolute()}")
1322
- console.print(f" Model: [cyan]{default_model}[/]")
1323
- console.print(f" Adapter: [cyan]{default_adapter}[/]")
1324
- if codex_config_path:
1325
- console.print(f" Config: [green]{codex_config_path}[/] (isolated)")
1326
- else:
1327
- console.print(f" Config: [yellow]using fallback overrides[/] (run 'zwarm init' for isolation)")
1328
- console.print(f" Available: {', '.join(list_adapters())}")
1329
- console.print("\n Type [cyan]help[/] for commands, [cyan]quit[/] to exit.\n")
1330
-
1331
- def show_help():
1332
- help_table = Table(show_header=False, box=None, padding=(0, 2))
1333
- help_table.add_column("Command", style="cyan", width=35)
1334
- help_table.add_column("Description")
1335
- help_table.add_row('spawn "task" [options]', "Start session (waits for completion)")
1336
- help_table.add_row(" --dir PATH", "Working directory")
1337
- help_table.add_row(" --model NAME", "Model override")
1338
- help_table.add_row(" --async", "Background mode (don't wait)")
1339
- help_table.add_row("", "")
1340
- help_table.add_row("ls / list", "Dashboard of all sessions")
1341
- help_table.add_row("? ID / peek ID", "Quick peek (status + latest message)")
1342
- help_table.add_row("show ID", "Full session details & messages")
1343
- help_table.add_row("traj ID [--full]", "Show trajectory (all steps taken)")
1344
- help_table.add_row('c ID "msg"', "Continue conversation (wait for response)")
1345
- help_table.add_row('ca ID "msg"', "Continue async (fire-and-forget)")
1346
- help_table.add_row("check ID", "Check session status")
1347
- help_table.add_row("kill ID", "Stop a running session")
1348
- help_table.add_row("rm ID", "Delete session entirely")
1349
- help_table.add_row("killall", "Stop all running sessions")
1350
- help_table.add_row("clean", "Remove old completed sessions")
1351
- help_table.add_row("q / quit", "Exit")
1352
- console.print(help_table)
1353
-
1354
- def show_sessions():
1355
- """List all sessions from CodexSessionManager (same as orchestrator)."""
1356
- from zwarm.sessions import CodexSessionManager, SessionStatus as SessStatus
1357
- from datetime import datetime
1358
-
1359
- manager = CodexSessionManager(default_dir / ".zwarm")
1360
- all_sessions = manager.list_sessions()
1361
-
1362
- if not all_sessions:
1363
- console.print(" [dim]No sessions. Use 'spawn \"task\"' to start one.[/]")
1364
- return
1365
-
1366
- # Status summary
1367
- running = sum(1 for s in all_sessions if s.status == SessStatus.RUNNING)
1368
- completed = sum(1 for s in all_sessions if s.status == SessStatus.COMPLETED)
1369
- failed = sum(1 for s in all_sessions if s.status == SessStatus.FAILED)
1370
- killed = sum(1 for s in all_sessions if s.status == SessStatus.KILLED)
1371
-
1372
- summary_parts = []
1373
- if running:
1374
- summary_parts.append(f"[yellow]{running} running[/]")
1375
- if completed:
1376
- summary_parts.append(f"[green]{completed} done[/]")
1377
- if failed:
1378
- summary_parts.append(f"[red]{failed} failed[/]")
1379
- if killed:
1380
- summary_parts.append(f"[dim]{killed} killed[/]")
1381
- if summary_parts:
1382
- console.print(" | ".join(summary_parts))
1383
- console.print()
1384
-
1385
- def time_ago(iso_str: str) -> str:
1386
- """Convert ISO timestamp to human-readable 'X ago' format."""
1387
- try:
1388
- dt = datetime.fromisoformat(iso_str)
1389
- delta = datetime.now() - dt
1390
- secs = delta.total_seconds()
1391
- if secs < 60:
1392
- return f"{int(secs)}s"
1393
- elif secs < 3600:
1394
- return f"{int(secs/60)}m"
1395
- elif secs < 86400:
1396
- return f"{secs/3600:.1f}h"
1397
- else:
1398
- return f"{secs/86400:.1f}d"
1399
- except:
1400
- return "?"
1401
-
1402
- table = Table(box=None, show_header=True, header_style="bold dim")
1403
- table.add_column("ID", style="cyan", width=10)
1404
- table.add_column("", width=2) # Status icon
1405
- table.add_column("T", width=2) # Turn
1406
- table.add_column("Task", max_width=30)
1407
- table.add_column("Updated", justify="right", width=8)
1408
- table.add_column("Last Message", max_width=40)
1409
-
1410
- status_icons = {
1411
- "running": "[yellow]●[/]",
1412
- "completed": "[green]✓[/]",
1413
- "failed": "[red]✗[/]",
1414
- "killed": "[dim]○[/]",
1415
- "pending": "[dim]◌[/]",
1416
- }
1417
-
1418
- for s in all_sessions:
1419
- icon = status_icons.get(s.status.value, "?")
1420
- task_preview = s.task[:27] + "..." if len(s.task) > 30 else s.task
1421
- updated = time_ago(s.updated_at)
1422
-
1423
- # Get last assistant message preview
1424
- messages = manager.get_messages(s.id)
1425
- last_msg = ""
1426
- for msg in reversed(messages):
1427
- if msg.role == "assistant":
1428
- last_msg = msg.content.replace("\n", " ")[:37]
1429
- if len(msg.content) > 37:
1430
- last_msg += "..."
1431
- break
1432
-
1433
- # Style the last message based on recency
1434
- if s.status == SessStatus.RUNNING:
1435
- last_msg_styled = f"[yellow]{last_msg or '(working...)'}[/]"
1436
- updated_styled = f"[yellow]{updated}[/]"
1437
- elif s.status == SessStatus.COMPLETED:
1438
- # Highlight if recently completed (< 60s)
1439
- try:
1440
- dt = datetime.fromisoformat(s.updated_at)
1441
- is_recent = (datetime.now() - dt).total_seconds() < 60
1442
- except:
1443
- is_recent = False
1444
- if is_recent:
1445
- last_msg_styled = f"[green bold]{last_msg or '(done)'}[/]"
1446
- updated_styled = f"[green bold]{updated} ★[/]"
1447
- else:
1448
- last_msg_styled = f"[green]{last_msg or '(done)'}[/]"
1449
- updated_styled = f"[dim]{updated}[/]"
1450
- elif s.status == SessStatus.FAILED:
1451
- last_msg_styled = f"[red]{s.error[:37] if s.error else '(failed)'}...[/]"
1452
- updated_styled = f"[red]{updated}[/]"
1453
- else:
1454
- last_msg_styled = f"[dim]{last_msg or '-'}[/]"
1455
- updated_styled = f"[dim]{updated}[/]"
1456
-
1457
- table.add_row(
1458
- s.short_id,
1459
- icon,
1460
- str(s.turn),
1461
- task_preview,
1462
- updated_styled,
1463
- last_msg_styled,
1464
- )
1465
-
1466
- console.print(table)
1467
-
1468
- def parse_spawn_args(args: list[str]) -> dict:
1469
- """Parse spawn command arguments."""
1470
- parser = argparse.ArgumentParser(add_help=False)
1471
- parser.add_argument("task", nargs="*")
1472
- parser.add_argument("--dir", "-d", type=Path, default=None)
1473
- parser.add_argument("--model", "-m", default=None)
1474
- parser.add_argument("--async", dest="async_mode", action="store_true", default=False)
1475
-
1476
- try:
1477
- parsed, _ = parser.parse_known_args(args)
1478
- return {
1479
- "task": " ".join(parsed.task) if parsed.task else "",
1480
- "dir": parsed.dir,
1481
- "model": parsed.model,
1482
- "async_mode": parsed.async_mode,
1483
- }
1484
- except SystemExit:
1485
- return {"error": "Invalid spawn arguments"}
1486
-
1487
- def do_spawn(args: list[str]):
1488
- """Spawn a new coding agent session using CodexSessionManager (same as orchestrator)."""
1489
- from zwarm.sessions import CodexSessionManager, SessionStatus as SessStatus
1490
- import time
1491
-
1492
- parsed = parse_spawn_args(args)
1493
-
1494
- if "error" in parsed:
1495
- console.print(f" [red]{parsed['error']}[/]")
1496
- console.print(" [dim]Usage: spawn \"task\" [--dir PATH] [--model NAME] [--async][/]")
1497
- return
1498
-
1499
- if not parsed["task"]:
1500
- console.print(" [red]Task required[/]")
1501
- console.print(" [dim]Usage: spawn \"task\" [--dir PATH] [--model NAME] [--async][/]")
1502
- return
1503
-
1504
- task = parsed["task"]
1505
- work_dir = (parsed["dir"] or default_dir).absolute()
1506
- session_model = parsed["model"] or default_model
1507
- is_async = parsed.get("async_mode", False)
1508
-
1509
- if not work_dir.exists():
1510
- console.print(f" [red]Directory not found:[/] {work_dir}")
1511
- return
1512
-
1513
- mode_str = "async" if is_async else "sync"
1514
- console.print(f"\n[dim]Spawning {mode_str} session...[/]")
1515
- console.print(f" [dim]Dir: {work_dir}[/]")
1516
- console.print(f" [dim]Model: {session_model}[/]")
1517
-
1518
- try:
1519
- # Use CodexSessionManager - SAME as orchestrator's delegate()
1520
- manager = CodexSessionManager(work_dir / ".zwarm")
1521
- session = manager.start_session(
1522
- task=task,
1523
- working_dir=work_dir,
1524
- model=session_model,
1525
- sandbox="workspace-write",
1526
- source="user",
1527
- adapter="codex",
1528
- )
1529
-
1530
- console.print(f"\n[green]✓[/] Session: [cyan]{session.short_id}[/]")
1531
- console.print(f" [dim]PID: {session.pid}[/]")
1532
-
1533
- # For sync mode, wait for completion and show response
1534
- if not is_async:
1535
- console.print(f"\n[dim]Waiting for completion...[/]")
1536
- timeout = 300.0
1537
- start = time.time()
1538
- while time.time() - start < timeout:
1539
- # get_session() auto-updates status based on output completion
1540
- session = manager.get_session(session.id)
1541
- if session.status != SessStatus.RUNNING:
1542
- break
1543
- time.sleep(1.0)
1544
-
1545
- # Get all assistant responses
1546
- messages = manager.get_messages(session.id)
1547
- assistant_msgs = [m for m in messages if m.role == "assistant"]
1548
- if assistant_msgs:
1549
- console.print(f"\n[bold]Response ({len(assistant_msgs)} message{'s' if len(assistant_msgs) > 1 else ''}):[/]")
1550
- for msg in assistant_msgs:
1551
- preview = msg.content[:300]
1552
- if len(msg.content) > 300:
1553
- preview += "..."
1554
- console.print(preview)
1555
- if len(assistant_msgs) > 1:
1556
- console.print() # Blank line between multiple messages
1557
-
1558
- console.print(f"\n[dim]Use 'show {session.short_id}' to see full details[/]")
1559
- console.print(f"[dim]Use 'c {session.short_id} \"message\"' to continue[/]")
1560
-
1561
- except Exception as e:
1562
- console.print(f" [red]Error:[/] {e}")
1563
- import traceback
1564
- console.print(f" [dim]{traceback.format_exc()}[/]")
1565
-
1566
- def do_orchestrate(args: list[str]):
1567
- """Spawn an orchestrator agent that delegates to sub-sessions."""
1568
- parsed = parse_spawn_args(args)
1569
-
1570
- if "error" in parsed:
1571
- console.print(f" [red]{parsed['error']}[/]")
1572
- console.print(" [dim]Usage: orchestrate \"task\" [--dir PATH][/]")
1573
- return
1338
+ run_interactive(working_dir=default_dir.absolute(), model=default_model)
1574
1339
 
1575
- if not parsed["task"]:
1576
- console.print(" [red]Task required[/]")
1577
- console.print(" [dim]Usage: orchestrate \"task\" [--dir PATH][/]")
1578
- return
1579
-
1580
- task = parsed["task"]
1581
- work_dir = (parsed["dir"] or default_dir).absolute()
1582
-
1583
- if not work_dir.exists():
1584
- console.print(f" [red]Directory not found:[/] {work_dir}")
1585
- return
1586
1340
 
1587
- console.print(f"\n[dim]Starting orchestrator...[/]")
1588
- console.print(f" [dim]Dir: {work_dir}[/]")
1589
- console.print(f" [dim]Task: {task[:60]}{'...' if len(task) > 60 else ''}[/]")
1341
+ @app.command()
1342
+ def sessions(
1343
+ working_dir: Annotated[Path, typer.Option("--dir", "-d", help="Working directory")] = Path("."),
1344
+ clean: Annotated[bool, typer.Option("--clean", "-c", help="Delete all non-running sessions")] = False,
1345
+ kill_all: Annotated[bool, typer.Option("--kill-all", "-k", help="Kill all running sessions")] = False,
1346
+ rm: Annotated[Optional[str], typer.Option("--rm", help="Delete specific session ID")] = None,
1347
+ kill: Annotated[Optional[str], typer.Option("--kill", help="Kill specific session ID")] = None,
1348
+ ):
1349
+ """
1350
+ Quick session management.
1590
1351
 
1591
- import subprocess
1592
- from uuid import uuid4
1352
+ By default, lists all sessions. Use flags for quick actions:
1593
1353
 
1594
- # Generate instance ID for tracking
1595
- instance_id = str(uuid4())[:8]
1354
+ [bold]Examples:[/]
1355
+ [dim]# List all sessions[/]
1356
+ $ zwarm sessions
1596
1357
 
1597
- # Build command to run orchestrator in background
1598
- cmd = [
1599
- sys.executable, "-m", "zwarm.cli.main", "orchestrate",
1600
- "--task", task,
1601
- "--working-dir", str(work_dir),
1602
- "--instance", instance_id,
1603
- ]
1358
+ [dim]# Delete all completed/failed sessions[/]
1359
+ $ zwarm sessions --clean
1360
+ $ zwarm sessions -c
1604
1361
 
1605
- # Create log file for orchestrator output
1606
- log_dir = state_dir / "orchestrators"
1607
- log_dir.mkdir(parents=True, exist_ok=True)
1608
- log_file = log_dir / f"{instance_id}.log"
1362
+ [dim]# Kill all running sessions[/]
1363
+ $ zwarm sessions --kill-all
1364
+ $ zwarm sessions -k
1609
1365
 
1610
- try:
1611
- with open(log_file, "w") as f:
1612
- proc = subprocess.Popen(
1613
- cmd,
1614
- cwd=work_dir,
1615
- stdout=f,
1616
- stderr=subprocess.STDOUT,
1617
- start_new_session=True,
1618
- )
1366
+ [dim]# Delete a specific session[/]
1367
+ $ zwarm sessions --rm abc123
1619
1368
 
1620
- console.print(f"\n[green]✓[/] Orchestrator started: [magenta]{instance_id}[/]")
1621
- console.print(f" PID: {proc.pid}")
1622
- console.print(f" Log: {log_file}")
1623
- console.print(f"\n[dim]Delegated sessions will appear with source 'orch:{instance_id[:4]}'[/]")
1624
- console.print(f"[dim]Use 'ls' to monitor progress[/]")
1625
-
1626
- except Exception as e:
1627
- console.print(f" [red]Error starting orchestrator:[/] {e}")
1628
-
1629
- def do_peek(session_id: str):
1630
- """Quick peek at session - just status + latest message (for fast polling)."""
1631
- from zwarm.sessions import CodexSessionManager, SessionStatus as SessStatus
1369
+ [dim]# Kill a specific session[/]
1370
+ $ zwarm sessions --kill abc123
1371
+ """
1372
+ from zwarm.sessions import CodexSessionManager, SessionStatus
1373
+ from zwarm.core.costs import estimate_session_cost, format_cost
1632
1374
 
1633
- manager = CodexSessionManager(default_dir / ".zwarm")
1634
- session = manager.get_session(session_id)
1375
+ manager = CodexSessionManager(working_dir / ".zwarm")
1635
1376
 
1377
+ # Handle --kill (specific session)
1378
+ if kill:
1379
+ session = manager.get_session(kill)
1636
1380
  if not session:
1637
- console.print(f" [red]Not found:[/] {session_id}")
1638
- return
1639
-
1640
- # Status icon
1641
- icon = {
1642
- "running": "[yellow]●[/]",
1643
- "completed": "[green]✓[/]",
1644
- "failed": "[red]✗[/]",
1645
- "killed": "[dim]○[/]",
1646
- }.get(session.status.value, "?")
1647
-
1648
- # Get latest assistant message
1649
- messages = manager.get_messages(session.id)
1650
- latest = None
1651
- for msg in reversed(messages):
1652
- if msg.role == "assistant":
1653
- latest = msg.content.replace("\n", " ")
1654
- break
1655
-
1656
- if session.status == SessStatus.RUNNING:
1657
- console.print(f"{icon} [cyan]{session.short_id}[/] [yellow](running...)[/]")
1658
- elif latest:
1659
- # Truncate for one-liner
1660
- preview = latest[:120] + "..." if len(latest) > 120 else latest
1661
- console.print(f"{icon} [cyan]{session.short_id}[/] {preview}")
1662
- elif session.status == SessStatus.FAILED:
1663
- error = (session.error or "unknown")[:80]
1664
- console.print(f"{icon} [cyan]{session.short_id}[/] [red]{error}[/]")
1381
+ console.print(f"[red]Session not found:[/] {kill}")
1382
+ raise typer.Exit(1)
1383
+ if manager.kill_session(session.id):
1384
+ console.print(f"[green]✓[/] Killed {session.short_id}")
1665
1385
  else:
1666
- console.print(f"{icon} [cyan]{session.short_id}[/] [dim](no response)[/]")
1667
-
1668
- def do_show(session_id: str):
1669
- """Show full session details and messages using CodexSessionManager."""
1670
- from zwarm.sessions import CodexSessionManager, SessionStatus as SessStatus
1671
-
1672
- manager = CodexSessionManager(default_dir / ".zwarm")
1673
- session = manager.get_session(session_id)
1674
-
1675
- if not session:
1676
- console.print(f" [red]Session not found:[/] {session_id}")
1677
- console.print(f" [dim]Use 'ls' to see available sessions[/]")
1678
- return
1679
-
1680
- # Status styling
1681
- status_display = {
1682
- "running": "[yellow]● running[/]",
1683
- "completed": "[green]✓ completed[/]",
1684
- "failed": "[red]✗ failed[/]",
1685
- "killed": "[dim]○ killed[/]",
1686
- "pending": "[dim]◌ pending[/]",
1687
- }.get(session.status.value, str(session.status.value))
1688
-
1689
- console.print()
1690
- console.print(f"[bold cyan]Session {session.short_id}[/] {status_display}")
1691
- console.print(f"[dim]Adapter:[/] {session.adapter} [dim]│[/] [dim]Model:[/] {session.model}")
1692
- console.print(f"[dim]Task:[/] {session.task}")
1693
- console.print(f"[dim]Dir:[/] {session.working_dir} [dim]│[/] [dim]Turn:[/] {session.turn}")
1694
- console.print(f"[dim]Source:[/] {session.source_display} [dim]│[/] [dim]Runtime:[/] {session.runtime}")
1695
- if session.pid:
1696
- console.print(f"[dim]PID:[/] {session.pid}")
1697
-
1698
- # Show log file path
1699
- log_path = default_dir / ".zwarm" / "sessions" / session.id / "turns" / f"turn_{session.turn}.jsonl"
1700
- console.print(f"[dim]Log:[/] {log_path}")
1701
- console.print()
1702
-
1703
- # Get messages from manager
1704
- messages = manager.get_messages(session.id)
1705
-
1706
- if not messages:
1707
- if session.is_running:
1708
- console.print("[yellow]Session is still running...[/]")
1709
- else:
1710
- console.print("[dim]No messages captured.[/]")
1711
- return
1712
-
1713
- # Display messages
1714
- for msg in messages:
1715
- if msg.role == "user":
1716
- console.print(Panel(msg.content, title="[bold blue]User[/]", border_style="blue"))
1717
- elif msg.role == "assistant":
1718
- content = msg.content
1719
- if len(content) > 2000:
1720
- content = content[:2000] + "\n\n[dim]... (truncated)[/]"
1721
- console.print(Panel(content, title="[bold green]Assistant[/]", border_style="green"))
1722
- elif msg.role == "tool":
1723
- console.print(f" [dim]Tool: {msg.content[:100]}[/]")
1724
-
1725
- console.print()
1726
- if session.token_usage:
1727
- tokens = session.token_usage
1728
- console.print(f"[dim]Tokens: {tokens.get('input_tokens', 0):,} in / {tokens.get('output_tokens', 0):,} out[/]")
1729
-
1730
- if session.error:
1731
- console.print(f"[red]Error:[/] {session.error}")
1732
-
1733
- def do_trajectory(session_id: str, full: bool = False):
1734
- """Show the full trajectory of a session - all steps in order."""
1735
- from zwarm.sessions import CodexSessionManager
1736
-
1737
- manager = CodexSessionManager(default_dir / ".zwarm")
1738
- session = manager.get_session(session_id)
1739
-
1740
- if not session:
1741
- console.print(f" [red]Session not found:[/] {session_id}")
1742
- return
1743
-
1744
- trajectory = manager.get_trajectory(session_id, full=full)
1745
-
1746
- if not trajectory:
1747
- console.print("[dim]No trajectory data available.[/]")
1748
- return
1749
-
1750
- mode = "[bold](full)[/] " if full else ""
1751
- console.print(f"\n[bold cyan]Trajectory: {session.short_id}[/] {mode}({len(trajectory)} steps)")
1752
- console.print(f"[dim]Task: {session.task[:60]}{'...' if len(session.task) > 60 else ''}[/]")
1753
- console.print()
1754
-
1755
- # Display each step
1756
- for step in trajectory:
1757
- turn = step.get("turn", 1)
1758
- step_num = step.get("step", 0)
1759
- step_type = step.get("type", "unknown")
1760
-
1761
- prefix = f"[dim]T{turn}.{step_num:02d}[/]"
1762
-
1763
- if step_type == "reasoning":
1764
- if full and step.get("full_text"):
1765
- console.print(f"{prefix} [yellow]thinking:[/]")
1766
- console.print(f" {step['full_text']}")
1767
- else:
1768
- summary = step.get("summary", "")
1769
- console.print(f"{prefix} [yellow]thinking:[/] {summary}")
1770
-
1771
- elif step_type == "command":
1772
- cmd = step.get("command", "")
1773
- output = step.get("output", "")
1774
- exit_code = step.get("exit_code", "?")
1775
- # Show command
1776
- console.print(f"{prefix} [cyan]$ {cmd}[/]")
1777
- if output:
1778
- if full:
1779
- # Show all output
1780
- for line in output.split("\n"):
1781
- console.print(f" [dim]{line}[/]")
1782
- else:
1783
- # Indent output, max 5 lines
1784
- for line in output.split("\n")[:5]:
1785
- console.print(f" [dim]{line}[/]")
1786
- if output.count("\n") > 5:
1787
- console.print(f" [dim]... ({output.count(chr(10))} lines)[/]")
1788
- if exit_code != 0 and exit_code is not None:
1789
- console.print(f" [red]exit: {exit_code}[/]")
1790
-
1791
- elif step_type == "tool_call":
1792
- tool = step.get("tool", "unknown")
1793
- if full and step.get("full_args"):
1794
- import json
1795
- console.print(f"{prefix} [magenta]tool:[/] {tool}")
1796
- console.print(f" {json.dumps(step['full_args'], indent=2)}")
1797
- else:
1798
- args = step.get("args_preview", "")
1799
- console.print(f"{prefix} [magenta]tool:[/] {tool}({args})")
1800
-
1801
- elif step_type == "tool_output":
1802
- output = step.get("output", "")
1803
- if not full:
1804
- output = output[:100]
1805
- console.print(f"{prefix} [dim]→ {output}[/]")
1806
-
1807
- elif step_type == "message":
1808
- if full and step.get("full_text"):
1809
- console.print(f"{prefix} [green]response:[/]")
1810
- console.print(f" {step['full_text']}")
1811
- else:
1812
- summary = step.get("summary", "")
1813
- full_len = step.get("full_length", 0)
1814
- console.print(f"{prefix} [green]response:[/] {summary}")
1815
- if full_len > 200:
1816
- console.print(f" [dim]({full_len} chars total)[/]")
1817
-
1818
- console.print()
1819
-
1820
- def do_continue(session_id: str, message: str, wait: bool = True):
1821
- """
1822
- Continue a conversation using CodexSessionManager.inject_message().
1823
-
1824
- Works for both sync and async sessions:
1825
- - If session was sync (or wait=True): wait for response
1826
- - If session was async (or wait=False): fire-and-forget, check later with '?'
1827
- """
1828
- from zwarm.sessions import CodexSessionManager, SessionStatus as SessStatus
1829
- import time
1830
-
1831
- manager = CodexSessionManager(default_dir / ".zwarm")
1832
- session = manager.get_session(session_id)
1833
-
1834
- if not session:
1835
- console.print(f" [red]Session not found:[/] {session_id}")
1836
- console.print(f" [dim]Use 'ls' to see available sessions[/]")
1837
- return
1838
-
1839
- if session.status == SessStatus.RUNNING:
1840
- console.print("[yellow]Session is still running.[/]")
1841
- console.print("[dim]Wait for it to complete, or use '?' to check progress.[/]")
1842
- return
1843
-
1844
- if session.status == SessStatus.KILLED:
1845
- console.print("[yellow]Session was killed.[/]")
1846
- console.print("[dim]Start a new session with 'spawn'.[/]")
1847
- return
1848
-
1849
- # Determine if we should wait based on session source
1850
- # Sessions from 'user' with --async flag have source='user'
1851
- # We'll use wait parameter to control this
1852
- should_wait = wait
1853
-
1854
- console.print(f"\n[dim]Sending message to {session.short_id}...[/]")
1855
-
1856
- try:
1857
- # Inject message - spawns new background process
1858
- updated_session = manager.inject_message(session.id, message)
1859
-
1860
- if not updated_session:
1861
- console.print(f" [red]Failed to inject message[/]")
1862
- return
1863
-
1864
- console.print(f"[green]✓[/] Message sent (turn {updated_session.turn})")
1865
-
1866
- if should_wait:
1867
- # Sync mode: wait for completion
1868
- console.print(f"[dim]Waiting for response...[/]")
1869
- timeout = 300.0
1870
- start = time.time()
1871
- while time.time() - start < timeout:
1872
- # get_session() auto-updates status based on output completion
1873
- session = manager.get_session(session.id)
1874
- if session.status != SessStatus.RUNNING:
1875
- break
1876
- time.sleep(1.0)
1877
-
1878
- # Get the response (last assistant message)
1879
- messages = manager.get_messages(session.id)
1880
- response = ""
1881
- for msg in reversed(messages):
1882
- if msg.role == "assistant":
1883
- response = msg.content
1884
- break
1885
-
1886
- console.print(f"\n[bold]Response:[/]")
1887
- if len(response) > 500:
1888
- console.print(response[:500] + "...")
1889
- console.print(f"\n[dim]Use 'show {session.short_id}' to see full response[/]")
1890
- else:
1891
- console.print(response or "(no response captured)")
1892
- else:
1893
- # Async mode: return immediately
1894
- console.print(f"[dim]Running in background (PID: {updated_session.pid})[/]")
1895
- console.print(f"[dim]Use '? {session.short_id}' to check progress[/]")
1896
-
1897
- except Exception as e:
1898
- console.print(f" [red]Error:[/] {e}")
1899
- import traceback
1900
- console.print(f" [dim]{traceback.format_exc()}[/]")
1901
-
1902
- def do_check(session_id: str):
1903
- """Check status of a session using CodexSessionManager (same as orchestrator)."""
1904
- from zwarm.sessions import CodexSessionManager
1905
-
1906
- manager = CodexSessionManager(default_dir / ".zwarm")
1907
- session = manager.get_session(session_id)
1908
-
1909
- if not session:
1910
- console.print(f" [red]Session not found:[/] {session_id}")
1911
- console.print(f" [dim]Use 'ls' to see available sessions[/]")
1912
- return
1913
-
1914
- status_icon = {
1915
- "running": "●",
1916
- "completed": "✓",
1917
- "failed": "✗",
1918
- "killed": "○",
1919
- "pending": "◌",
1920
- }.get(session.status.value, "?")
1921
-
1922
- console.print(f"\n[bold]Session {session.short_id}[/]: {status_icon} {session.status.value}")
1923
- console.print(f" [dim]Task: {session.task[:60]}{'...' if len(session.task) > 60 else ''}[/]")
1924
- console.print(f" [dim]Turn: {session.turn} | Runtime: {session.runtime}[/]")
1925
-
1926
- if session.status.value == "completed":
1927
- messages = manager.get_messages(session.id)
1928
- for msg in reversed(messages):
1929
- if msg.role == "assistant":
1930
- console.print(f"\n[bold]Response:[/]")
1931
- if len(msg.content) > 500:
1932
- console.print(msg.content[:500] + "...")
1933
- else:
1934
- console.print(msg.content)
1935
- break
1936
- elif session.status.value == "failed":
1937
- console.print(f"[red]Error:[/] {session.error or 'Unknown error'}")
1938
- elif session.status.value == "running":
1939
- console.print("[dim]Session is still running...[/]")
1940
- console.print(f" [dim]PID: {session.pid}[/]")
1941
-
1942
- def do_kill(session_id: str):
1943
- """Kill a running session using CodexSessionManager (same as orchestrator)."""
1944
- from zwarm.sessions import CodexSessionManager
1945
-
1946
- manager = CodexSessionManager(default_dir / ".zwarm")
1947
- session = manager.get_session(session_id)
1386
+ console.print(f"[yellow]Session not running or already stopped[/]")
1387
+ return
1948
1388
 
1389
+ # Handle --rm (specific session)
1390
+ if rm:
1391
+ session = manager.get_session(rm)
1949
1392
  if not session:
1950
- console.print(f" [red]Session not found:[/] {session_id}")
1951
- return
1952
-
1953
- if not session.is_running:
1954
- console.print(f"[yellow]Session {session.short_id} is not running ({session.status.value}).[/]")
1955
- return
1956
-
1957
- try:
1958
- killed = manager.kill_session(session.id)
1959
- if killed:
1960
- console.print(f"[green]✓[/] Stopped session {session.short_id}")
1961
- else:
1962
- console.print(f"[red]Failed to stop session[/]")
1963
- except Exception as e:
1964
- console.print(f"[red]Failed to stop session:[/] {e}")
1965
-
1966
- def do_killall():
1967
- """Kill all running sessions using CodexSessionManager."""
1968
- from zwarm.sessions import CodexSessionManager, SessionStatus as SessStatus
1969
-
1970
- manager = CodexSessionManager(default_dir / ".zwarm")
1971
- running_sessions = manager.list_sessions(status=SessStatus.RUNNING)
1393
+ console.print(f"[red]Session not found:[/] {rm}")
1394
+ raise typer.Exit(1)
1395
+ if manager.delete_session(session.id):
1396
+ console.print(f"[green]✓[/] Deleted {session.short_id}")
1397
+ else:
1398
+ console.print(f"[red]Failed to delete[/]")
1399
+ return
1972
1400
 
1973
- if not running_sessions:
1974
- console.print("[dim]No running sessions to kill.[/]")
1401
+ # Handle --kill-all
1402
+ if kill_all:
1403
+ running = manager.list_sessions(status=SessionStatus.RUNNING)
1404
+ if not running:
1405
+ console.print("[dim]No running sessions[/]")
1975
1406
  return
1976
-
1977
1407
  killed = 0
1978
- for session in running_sessions:
1979
- try:
1980
- if manager.kill_session(session.id):
1981
- killed += 1
1982
- except Exception as e:
1983
- console.print(f"[red]Failed to stop {session.short_id}:[/] {e}")
1984
-
1985
- console.print(f"[green]✓[/] Killed {killed} sessions")
1986
-
1987
- def do_clean():
1988
- """Remove old completed/failed sessions from disk."""
1989
- from zwarm.sessions import CodexSessionManager
1990
-
1991
- manager = CodexSessionManager(default_dir / ".zwarm")
1992
- cleaned = manager.cleanup_completed(keep_days=7)
1993
- console.print(f"[green]✓[/] Cleaned {cleaned} old sessions")
1994
-
1995
- def do_delete(session_id: str):
1996
- """Delete a session entirely (removes from disk and ls)."""
1997
- from zwarm.sessions import CodexSessionManager
1998
-
1999
- manager = CodexSessionManager(default_dir / ".zwarm")
2000
- session = manager.get_session(session_id)
1408
+ for s in running:
1409
+ if manager.kill_session(s.id):
1410
+ console.print(f" [green]✓[/] Killed {s.short_id}")
1411
+ killed += 1
1412
+ console.print(f"\n[green]Killed {killed} session(s)[/]")
1413
+ return
2001
1414
 
2002
- if not session:
2003
- console.print(f" [red]Session not found:[/] {session_id}")
1415
+ # Handle --clean
1416
+ if clean:
1417
+ all_sessions = manager.list_sessions()
1418
+ to_delete = [s for s in all_sessions if s.status != SessionStatus.RUNNING]
1419
+ if not to_delete:
1420
+ console.print("[dim]Nothing to clean[/]")
2004
1421
  return
1422
+ deleted = 0
1423
+ for s in to_delete:
1424
+ if manager.delete_session(s.id):
1425
+ deleted += 1
1426
+ console.print(f"[green]✓[/] Deleted {deleted} session(s)")
1427
+ return
2005
1428
 
2006
- if manager.delete_session(session.id):
2007
- console.print(f"[green]✓[/] Deleted session {session.short_id}")
2008
- else:
2009
- console.print(f"[red]Failed to delete session[/]")
2010
-
2011
- # REPL loop
2012
- import shlex
2013
-
2014
- while True:
2015
- try:
2016
- raw_input = console.input("[bold cyan]>[/] ").strip()
2017
- if not raw_input:
2018
- continue
2019
-
2020
- # Parse command
2021
- try:
2022
- parts = shlex.split(raw_input)
2023
- except ValueError:
2024
- parts = raw_input.split()
2025
-
2026
- cmd = parts[0].lower()
2027
- args = parts[1:]
2028
-
2029
- if cmd in ("q", "quit", "exit"):
2030
- active = [s for s in sessions.values() if s.status == SessionStatus.ACTIVE]
2031
- if active:
2032
- console.print(f" [yellow]Warning:[/] {len(active)} sessions still active")
2033
- console.print(" [dim]Use 'killall' first, or they'll continue in background[/]")
2034
-
2035
- # Cleanup adapters
2036
- for adapter_instance in adapters_cache.values():
2037
- try:
2038
- asyncio.run(adapter_instance.cleanup())
2039
- except Exception:
2040
- pass
2041
-
2042
- console.print("\n[dim]Goodbye![/]\n")
2043
- break
2044
-
2045
- elif cmd in ("h", "help"):
2046
- show_help()
2047
-
2048
- elif cmd in ("ls", "list"):
2049
- show_sessions()
2050
-
2051
- elif cmd == "spawn":
2052
- do_spawn(args)
2053
-
2054
- elif cmd == "async":
2055
- # Async spawn shorthand
2056
- do_spawn(args + ["--async"])
2057
-
2058
- elif cmd == "orchestrate":
2059
- do_orchestrate(args)
2060
-
2061
- elif cmd in ("?", "peek"):
2062
- if not args:
2063
- console.print(" [red]Usage:[/] ? SESSION_ID")
2064
- else:
2065
- do_peek(args[0])
2066
-
2067
- elif cmd in ("show", "check"):
2068
- if not args:
2069
- console.print(" [red]Usage:[/] show SESSION_ID")
2070
- else:
2071
- do_show(args[0])
2072
-
2073
- elif cmd in ("traj", "trajectory"):
2074
- if not args:
2075
- console.print(" [red]Usage:[/] traj SESSION_ID [--full]")
2076
- else:
2077
- full_mode = "--full" in args
2078
- session_arg = [a for a in args if a != "--full"]
2079
- if session_arg:
2080
- do_trajectory(session_arg[0], full=full_mode)
2081
- else:
2082
- console.print(" [red]Usage:[/] traj SESSION_ID [--full]")
2083
-
2084
- elif cmd in ("c", "continue"):
2085
- # Sync continue - waits for response
2086
- if len(args) < 2:
2087
- console.print(" [red]Usage:[/] c SESSION_ID \"message\"")
2088
- else:
2089
- do_continue(args[0], " ".join(args[1:]), wait=True)
1429
+ # Default: list all sessions
1430
+ all_sessions = manager.list_sessions()
2090
1431
 
2091
- elif cmd in ("ca", "async"):
2092
- # Async continue - fire and forget
2093
- if len(args) < 2:
2094
- console.print(" [red]Usage:[/] ca SESSION_ID \"message\"")
2095
- console.print(" [dim]Sends message and returns immediately (check with '?')[/]")
2096
- else:
2097
- do_continue(args[0], " ".join(args[1:]), wait=False)
1432
+ if not all_sessions:
1433
+ console.print("[dim]No sessions found.[/]")
1434
+ console.print("[dim]Start one with:[/] zwarm session start \"your task\"")
1435
+ return
2098
1436
 
2099
- elif cmd == "check":
2100
- if not args:
2101
- console.print(" [red]Usage:[/] check SESSION_ID")
2102
- else:
2103
- do_check(args[0])
1437
+ # Summary counts
1438
+ running = sum(1 for s in all_sessions if s.status == SessionStatus.RUNNING)
1439
+ completed = sum(1 for s in all_sessions if s.status == SessionStatus.COMPLETED)
1440
+ failed = sum(1 for s in all_sessions if s.status == SessionStatus.FAILED)
1441
+ killed_count = sum(1 for s in all_sessions if s.status == SessionStatus.KILLED)
1442
+
1443
+ parts = []
1444
+ if running:
1445
+ parts.append(f"[yellow]⟳ {running} running[/]")
1446
+ if completed:
1447
+ parts.append(f"[green]✓ {completed} completed[/]")
1448
+ if failed:
1449
+ parts.append(f"[red]✗ {failed} failed[/]")
1450
+ if killed_count:
1451
+ parts.append(f"[dim]⊘ {killed_count} killed[/]")
1452
+
1453
+ console.print(" │ ".join(parts))
1454
+ console.print()
2104
1455
 
2105
- elif cmd == "kill":
2106
- if not args:
2107
- console.print(" [red]Usage:[/] kill SESSION_ID")
2108
- else:
2109
- do_kill(args[0])
1456
+ # Table
1457
+ table = Table(box=None, show_header=True, header_style="bold dim")
1458
+ table.add_column("ID", style="cyan")
1459
+ table.add_column("", width=2)
1460
+ table.add_column("Task", max_width=45)
1461
+ table.add_column("Tokens", justify="right", style="dim")
1462
+ table.add_column("Cost", justify="right", style="dim")
2110
1463
 
2111
- elif cmd == "killall":
2112
- do_killall()
1464
+ status_icons = {
1465
+ SessionStatus.RUNNING: "[yellow]⟳[/]",
1466
+ SessionStatus.COMPLETED: "[green]✓[/]",
1467
+ SessionStatus.FAILED: "[red]✗[/]",
1468
+ SessionStatus.KILLED: "[dim]⊘[/]",
1469
+ SessionStatus.PENDING: "[dim]○[/]",
1470
+ }
2113
1471
 
2114
- elif cmd == "clean":
2115
- do_clean()
1472
+ for session in all_sessions:
1473
+ icon = status_icons.get(session.status, "?")
1474
+ task = session.task[:42] + "..." if len(session.task) > 45 else session.task
1475
+ tokens = session.token_usage.get("total_tokens", 0)
1476
+ tokens_str = f"{tokens:,}" if tokens else "-"
2116
1477
 
2117
- elif cmd in ("rm", "delete", "remove"):
2118
- if not args:
2119
- console.print(" [red]Usage:[/] rm SESSION_ID")
2120
- else:
2121
- do_delete(args[0])
1478
+ # Cost estimate
1479
+ cost_info = estimate_session_cost(session.model, session.token_usage)
1480
+ cost_str = format_cost(cost_info.get("cost"))
2122
1481
 
2123
- else:
2124
- console.print(f" [yellow]Unknown command:[/] {cmd}")
2125
- console.print(" [dim]Type 'help' for available commands[/]")
1482
+ table.add_row(session.short_id, icon, task, tokens_str, cost_str)
2126
1483
 
2127
- except KeyboardInterrupt:
2128
- console.print("\n[dim](Ctrl+C to exit, or type 'quit')[/]")
2129
- except EOFError:
2130
- console.print("\n[dim]Goodbye![/]\n")
2131
- break
2132
- except Exception as e:
2133
- console.print(f" [red]Error:[/] {e}")
1484
+ console.print(table)
1485
+ console.print()
1486
+ console.print("[dim]Quick actions: --clean (-c) delete old │ --kill-all (-k) stop running[/]")
2134
1487
 
2135
1488
 
2136
1489
  # =============================================================================