zwarm 3.4.0__py3-none-any.whl → 3.7.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/interactive.py CHANGED
@@ -153,22 +153,28 @@ STATUS_ICONS = {
153
153
  def cmd_help():
154
154
  """Show help."""
155
155
  table = Table(show_header=False, box=None, padding=(0, 2))
156
- table.add_column("Command", style="cyan", width=30)
156
+ table.add_column("Command", style="cyan", width=35)
157
157
  table.add_column("Description")
158
158
 
159
159
  table.add_row("[bold]Session Lifecycle[/]", "")
160
- table.add_row('spawn "task" [--dir PATH] [--model M]', "Start new session")
160
+ table.add_row('spawn "task" [--model M] [--adapter A]', "Start new session")
161
161
  table.add_row('c ID "message"', "Continue conversation")
162
162
  table.add_row("kill ID | all", "Stop session(s)")
163
163
  table.add_row("rm ID | all", "Delete session(s)")
164
164
  table.add_row("", "")
165
165
  table.add_row("[bold]Viewing[/]", "")
166
166
  table.add_row("ls", "Dashboard of all sessions")
167
- table.add_row("? ID / peek ID", "Quick peek (status + latest)")
168
- table.add_row("show ID", "Full session details")
169
- table.add_row("traj ID [--full]", "Show trajectory (steps taken)")
167
+ table.add_row("? ID / peek ID", "Quick peek (status + latest preview)")
168
+ table.add_row("show ID [-v]", "Full response from agent (-v: verbose)")
169
+ table.add_row("traj ID [--full]", "Trajectory (--full: all data)")
170
170
  table.add_row("watch ID", "Live follow session output")
171
171
  table.add_row("", "")
172
+ table.add_row("[bold]Configuration[/]", "")
173
+ table.add_row("models", "List available models and adapters")
174
+ table.add_row("", "")
175
+ table.add_row("[bold]Shell[/]", "")
176
+ table.add_row("!<command>", "Run shell command (e.g., !ls, !git status)")
177
+ table.add_row("", "")
172
178
  table.add_row("[bold]Meta[/]", "")
173
179
  table.add_row("help", "Show this help")
174
180
  table.add_row("quit", "Exit")
@@ -176,6 +182,37 @@ def cmd_help():
176
182
  console.print(table)
177
183
 
178
184
 
185
+ def cmd_models():
186
+ """Show available models."""
187
+ from zwarm.core.registry import list_models, list_adapters
188
+
189
+ table = Table(title="Available Models", box=None)
190
+ table.add_column("Adapter", style="cyan")
191
+ table.add_column("Model", style="green")
192
+ table.add_column("Aliases", style="dim")
193
+ table.add_column("Price ($/1M)", justify="right")
194
+ table.add_column("Description")
195
+
196
+ for adapter in list_adapters():
197
+ first = True
198
+ for model in list_models(adapter):
199
+ default_mark = " *" if model.is_default else ""
200
+ price = f"{model.input_per_million:.2f}/{model.output_per_million:.2f}"
201
+ aliases = ", ".join(model.aliases)
202
+ table.add_row(
203
+ adapter if first else "",
204
+ f"{model.canonical}{default_mark}",
205
+ aliases,
206
+ price,
207
+ model.description,
208
+ )
209
+ first = False
210
+
211
+ console.print(table)
212
+ console.print("\n[dim]* = default for adapter. Price = input/output per 1M tokens.[/]")
213
+ console.print("[dim]Use --model <name> or --adapter <adapter> with spawn.[/]")
214
+
215
+
179
216
  def cmd_ls(manager):
180
217
  """List all sessions."""
181
218
  from zwarm.sessions import SessionStatus
@@ -221,23 +258,35 @@ def cmd_ls(manager):
221
258
  table = Table(box=None, show_header=True, header_style="bold dim")
222
259
  table.add_column("ID", style="cyan", width=10)
223
260
  table.add_column("", width=2)
261
+ table.add_column("Model", width=12)
224
262
  table.add_column("T", width=2)
225
- table.add_column("Task", max_width=30)
263
+ table.add_column("Task", max_width=26)
226
264
  table.add_column("Updated", justify="right", width=8)
227
- table.add_column("Last Message", max_width=40)
265
+ table.add_column("Last Message", max_width=36)
228
266
 
229
267
  for s in sessions:
230
268
  icon = STATUS_ICONS.get(s.status.value, "?")
231
- task_preview = s.task[:27] + "..." if len(s.task) > 30 else s.task
269
+ task_preview = s.task[:23] + "..." if len(s.task) > 26 else s.task
232
270
  updated = time_ago(s.updated_at)
233
271
 
272
+ # Short model name (e.g., "gpt-5.1-codex-mini" -> "codex-mini")
273
+ model_short = s.model or "?"
274
+ if "codex" in model_short.lower():
275
+ # Extract codex variant: gpt-5.1-codex-mini -> codex-mini
276
+ parts = model_short.split("-")
277
+ codex_idx = next((i for i, p in enumerate(parts) if "codex" in p.lower()), -1)
278
+ if codex_idx >= 0:
279
+ model_short = "-".join(parts[codex_idx:])
280
+ elif len(model_short) > 12:
281
+ model_short = model_short[:10] + ".."
282
+
234
283
  # Get last assistant message
235
284
  messages = manager.get_messages(s.id)
236
285
  last_msg = ""
237
286
  for msg in reversed(messages):
238
287
  if msg.role == "assistant":
239
- last_msg = msg.content.replace("\n", " ")[:37]
240
- if len(msg.content) > 37:
288
+ last_msg = msg.content.replace("\n", " ")[:33]
289
+ if len(msg.content) > 33:
241
290
  last_msg += "..."
242
291
  break
243
292
 
@@ -258,14 +307,110 @@ def cmd_ls(manager):
258
307
  last_msg_styled = f"[green]{last_msg or '(done)'}[/]"
259
308
  updated_styled = f"[dim]{updated}[/]"
260
309
  elif s.status == SessionStatus.FAILED:
261
- err = s.error[:37] if s.error else "(failed)"
310
+ err = s.error[:33] if s.error else "(failed)"
262
311
  last_msg_styled = f"[red]{err}...[/]"
263
312
  updated_styled = f"[red]{updated}[/]"
264
313
  else:
265
314
  last_msg_styled = f"[dim]{last_msg or '-'}[/]"
266
315
  updated_styled = f"[dim]{updated}[/]"
267
316
 
268
- table.add_row(s.short_id, icon, str(s.turn), task_preview, updated_styled, last_msg_styled)
317
+ table.add_row(s.short_id, icon, f"[dim]{model_short}[/]", str(s.turn), task_preview, updated_styled, last_msg_styled)
318
+
319
+ console.print(table)
320
+
321
+
322
+ def cmd_ls_multi(sessions: list, managers: dict | None = None):
323
+ """
324
+ List sessions from multiple managers.
325
+
326
+ Args:
327
+ sessions: List of Session objects
328
+ managers: Optional dict of adapter -> manager for getting messages
329
+ """
330
+ from zwarm.sessions import SessionStatus
331
+ from zwarm.core.costs import estimate_session_cost, format_cost
332
+
333
+ if not sessions:
334
+ console.print(" [dim]No sessions. Use 'spawn \"task\"' to start one.[/]")
335
+ return
336
+
337
+ # Summary counts
338
+ running = sum(1 for s in sessions if s.status == SessionStatus.RUNNING)
339
+ completed = sum(1 for s in sessions if s.status == SessionStatus.COMPLETED)
340
+ failed = sum(1 for s in sessions if s.status == SessionStatus.FAILED)
341
+ killed = sum(1 for s in sessions if s.status == SessionStatus.KILLED)
342
+
343
+ # Total cost and tokens
344
+ total_cost = 0.0
345
+ total_tokens = 0
346
+ for s in sessions:
347
+ cost_info = estimate_session_cost(s.model, s.token_usage)
348
+ if cost_info["cost"] is not None:
349
+ total_cost += cost_info["cost"]
350
+ total_tokens += s.token_usage.get("total_tokens", 0)
351
+
352
+ parts = []
353
+ if running:
354
+ parts.append(f"[yellow]{running} running[/]")
355
+ if completed:
356
+ parts.append(f"[green]{completed} done[/]")
357
+ if failed:
358
+ parts.append(f"[red]{failed} failed[/]")
359
+ if killed:
360
+ parts.append(f"[dim]{killed} killed[/]")
361
+ parts.append(f"[cyan]{total_tokens:,} tokens[/]")
362
+ parts.append(f"[green]{format_cost(total_cost)}[/]")
363
+ if parts:
364
+ console.print(" | ".join(parts))
365
+ console.print()
366
+
367
+ # Table
368
+ table = Table(box=None, show_header=True, header_style="bold dim")
369
+ table.add_column("ID", style="cyan", width=10)
370
+ table.add_column("", width=2)
371
+ table.add_column("Adapter", width=7)
372
+ table.add_column("Model", width=12)
373
+ table.add_column("T", width=2)
374
+ table.add_column("Task", max_width=24)
375
+ table.add_column("Updated", justify="right", width=8)
376
+
377
+ for s in sessions:
378
+ icon = STATUS_ICONS.get(s.status.value, "?")
379
+ task_preview = s.task[:21] + "..." if len(s.task) > 24 else s.task
380
+ updated = time_ago(s.updated_at)
381
+
382
+ # Short model name
383
+ model_short = s.model or "?"
384
+ if "codex" in model_short.lower():
385
+ parts = model_short.split("-")
386
+ codex_idx = next((i for i, p in enumerate(parts) if "codex" in p.lower()), -1)
387
+ if codex_idx >= 0:
388
+ model_short = "-".join(parts[codex_idx:])
389
+ elif len(model_short) > 12:
390
+ model_short = model_short[:10] + ".."
391
+
392
+ # Adapter short name
393
+ adapter_short = getattr(s, "adapter", "?")[:7]
394
+
395
+ # Style based on status
396
+ if s.status == SessionStatus.RUNNING:
397
+ updated_styled = f"[yellow]{updated}[/]"
398
+ elif s.status == SessionStatus.COMPLETED:
399
+ try:
400
+ dt = datetime.fromisoformat(s.updated_at)
401
+ is_recent = (datetime.now() - dt).total_seconds() < 60
402
+ except Exception:
403
+ is_recent = False
404
+ if is_recent:
405
+ updated_styled = f"[green bold]{updated} ★[/]"
406
+ else:
407
+ updated_styled = f"[dim]{updated}[/]"
408
+ elif s.status == SessionStatus.FAILED:
409
+ updated_styled = f"[red]{updated}[/]"
410
+ else:
411
+ updated_styled = f"[dim]{updated}[/]"
412
+
413
+ table.add_row(s.short_id, icon, f"[dim]{adapter_short}[/]", f"[dim]{model_short}[/]", str(s.turn), task_preview, updated_styled)
269
414
 
270
415
  console.print(table)
271
416
 
@@ -280,7 +425,7 @@ def cmd_peek(manager, session_id: str):
280
425
  icon = STATUS_ICONS.get(session.status.value, "?")
281
426
  console.print(f"\n{icon} [cyan]{session.short_id}[/] ({session.status.value})")
282
427
  console.print(f" [dim]Task:[/] {session.task[:60]}...")
283
- console.print(f" [dim]Turn:[/] {session.turn} | [dim]Updated:[/] {time_ago(session.updated_at)}")
428
+ console.print(f" [dim]Model:[/] {session.model} | [dim]Turn:[/] {session.turn} | [dim]Updated:[/] {time_ago(session.updated_at)}")
284
429
 
285
430
  # Latest message
286
431
  messages = manager.get_messages(session.id)
@@ -294,8 +439,15 @@ def cmd_peek(manager, session_id: str):
294
439
  console.print()
295
440
 
296
441
 
297
- def cmd_show(manager, session_id: str):
298
- """Full session details with messages."""
442
+ def cmd_show(manager, session_id: str, verbose: bool = False):
443
+ """
444
+ Full session details with messages.
445
+
446
+ Args:
447
+ manager: Session manager
448
+ session_id: Session to show
449
+ verbose: If True, show everything including full system messages
450
+ """
299
451
  from zwarm.core.costs import estimate_session_cost
300
452
 
301
453
  session = manager.get_session(session_id)
@@ -306,8 +458,8 @@ def cmd_show(manager, session_id: str):
306
458
  # Header
307
459
  icon = STATUS_ICONS.get(session.status.value, "?")
308
460
  console.print(f"\n{icon} [bold cyan]{session.short_id}[/] - {session.status.value}")
309
- console.print(f" [dim]Task:[/] {session.task}")
310
- console.print(f" [dim]Turn:[/] {session.turn} | [dim]Runtime:[/] {session.runtime}")
461
+ console.print(f" [dim]Task:[/] {session.task[:100]}..." if len(session.task) > 100 else f" [dim]Task:[/] {session.task}")
462
+ console.print(f" [dim]Model:[/] {session.model} | [dim]Turn:[/] {session.turn} | [dim]Runtime:[/] {session.runtime}")
311
463
 
312
464
  # Token usage with cost estimate
313
465
  usage = session.token_usage
@@ -323,28 +475,40 @@ def cmd_show(manager, session_id: str):
323
475
  if session.error:
324
476
  console.print(f" [red]Error:[/] {session.error}")
325
477
 
326
- # Messages
478
+ # Messages - show FULL assistant response (that's the point of show)
327
479
  messages = manager.get_messages(session.id)
328
480
  if messages:
329
481
  console.print(f"\n[bold]Messages ({len(messages)}):[/]")
330
482
  for msg in messages:
331
483
  role = msg.role
332
- content = msg.content[:200]
333
- if len(msg.content) > 200:
334
- content += "..."
484
+ content = msg.content
335
485
 
336
486
  if role == "user":
487
+ # User messages (task) can be truncated unless verbose
488
+ if not verbose and len(content) > 200:
489
+ content = content[:200] + "..."
337
490
  console.print(f" [blue]USER:[/] {content}")
338
491
  elif role == "assistant":
492
+ # FULL assistant response - this is what users need to see
339
493
  console.print(f" [green]ASSISTANT:[/] {content}")
340
494
  else:
341
- console.print(f" [dim]{role.upper()}:[/] {content[:100]}")
495
+ # System/other messages truncated unless verbose
496
+ if not verbose and len(content) > 100:
497
+ content = content[:100] + "..."
498
+ console.print(f" [dim]{role.upper()}:[/] {content}")
342
499
 
343
500
  console.print()
344
501
 
345
502
 
346
503
  def cmd_traj(manager, session_id: str, full: bool = False):
347
- """Show session trajectory."""
504
+ """
505
+ Show session trajectory.
506
+
507
+ Args:
508
+ manager: Session manager
509
+ session_id: Session to show trajectory for
510
+ full: If True, show full untruncated content for all steps
511
+ """
348
512
  session = manager.get_session(session_id)
349
513
  if not session:
350
514
  console.print(f" [red]Session not found:[/] {session_id}")
@@ -352,7 +516,8 @@ def cmd_traj(manager, session_id: str, full: bool = False):
352
516
 
353
517
  trajectory = manager.get_trajectory(session_id, full=full)
354
518
 
355
- console.print(f"\n[bold]Trajectory for {session.short_id}[/] ({len(trajectory)} steps)")
519
+ mode_str = "[bold green](FULL)[/]" if full else "[dim](summary - use --full for complete)[/]"
520
+ console.print(f"\n[bold]Trajectory for {session.short_id}[/] ({len(trajectory)} steps) {mode_str}")
356
521
  console.print(f" [dim]Task:[/] {session.task[:60]}...")
357
522
  console.print()
358
523
 
@@ -363,33 +528,63 @@ def cmd_traj(manager, session_id: str, full: bool = False):
363
528
  text = step.get("full_text") if full else step.get("summary", "")
364
529
  console.print(f" [dim]{i+1}.[/] [magenta]💭 thinking[/]")
365
530
  if text:
366
- console.print(f" {text[:150]}{'...' if len(text) > 150 else ''}")
531
+ if full:
532
+ # Full mode: show everything, handle multiline
533
+ for line in text.split("\n"):
534
+ console.print(f" {line}")
535
+ else:
536
+ console.print(f" {text[:150]}{'...' if len(text) > 150 else ''}")
367
537
 
368
538
  elif step_type == "command":
369
539
  cmd = step.get("command", "")
370
540
  output = step.get("output", "")
371
541
  exit_code = step.get("exit_code", 0)
372
542
  console.print(f" [dim]{i+1}.[/] [yellow]$ {cmd}[/]")
373
- if output and (full or len(output) < 100):
374
- console.print(f" {output[:200]}")
543
+ if output:
544
+ if full:
545
+ # Full mode: show complete output
546
+ for line in output.split("\n")[:50]: # Cap at 50 lines for sanity
547
+ console.print(f" {line}")
548
+ if output.count("\n") > 50:
549
+ console.print(f" [dim]... ({output.count(chr(10)) - 50} more lines)[/]")
550
+ else:
551
+ console.print(f" {output[:100]}{'...' if len(output) > 100 else ''}")
375
552
  if exit_code and exit_code != 0:
376
553
  console.print(f" [red](exit: {exit_code})[/]")
377
554
 
378
555
  elif step_type == "tool_call":
379
556
  tool = step.get("tool", "unknown")
380
- args_preview = step.get("args_preview", "")
381
- console.print(f" [dim]{i+1}.[/] [cyan]🔧 {tool}[/]({args_preview})")
557
+ if full and step.get("full_args"):
558
+ import json
559
+ args_str = json.dumps(step["full_args"], indent=2)
560
+ console.print(f" [dim]{i+1}.[/] [cyan]🔧 {tool}[/]")
561
+ for line in args_str.split("\n"):
562
+ console.print(f" {line}")
563
+ else:
564
+ args_preview = step.get("args_preview", "")
565
+ console.print(f" [dim]{i+1}.[/] [cyan]🔧 {tool}[/]({args_preview})")
382
566
 
383
567
  elif step_type == "tool_output":
384
568
  output = step.get("output", "")
385
- preview = output[:100] if not full else output[:300]
386
- console.print(f" [dim]→ {preview}[/]")
569
+ if full:
570
+ # Full mode: show complete output
571
+ for line in output.split("\n")[:30]:
572
+ console.print(f" [dim]→ {line}[/]")
573
+ if output.count("\n") > 30:
574
+ console.print(f" [dim]... ({output.count(chr(10)) - 30} more lines)[/]")
575
+ else:
576
+ console.print(f" [dim]→ {output[:100]}{'...' if len(output) > 100 else ''}[/]")
387
577
 
388
578
  elif step_type == "message":
389
579
  text = step.get("full_text") if full else step.get("summary", "")
390
580
  console.print(f" [dim]{i+1}.[/] [green]💬 response[/]")
391
581
  if text:
392
- console.print(f" {text[:150]}{'...' if len(text) > 150 else ''}")
582
+ if full:
583
+ # Full mode: show everything
584
+ for line in text.split("\n"):
585
+ console.print(f" {line}")
586
+ else:
587
+ console.print(f" {text[:150]}{'...' if len(text) > 150 else ''}")
393
588
 
394
589
  console.print()
395
590
 
@@ -409,6 +604,7 @@ def cmd_watch(manager, session_id: str):
409
604
 
410
605
  console.print(f"\n[bold]Watching {session.short_id}[/]...")
411
606
  console.print(f" [dim]Task:[/] {session.task[:60]}...")
607
+ console.print(f" [dim]Model:[/] {session.model}")
412
608
  console.print(f" [dim]Press Ctrl+C to stop watching[/]\n")
413
609
 
414
610
  seen_steps = 0
@@ -471,20 +667,52 @@ def cmd_watch(manager, session_id: str):
471
667
  console.print()
472
668
 
473
669
 
474
- def cmd_spawn(manager, task: str, working_dir: Path, model: str):
475
- """Spawn a new session."""
670
+ def cmd_spawn(managers: dict, task: str, working_dir: Path, model: str, adapter: str | None = None):
671
+ """
672
+ Spawn a new session.
673
+
674
+ Args:
675
+ managers: Dict of adapter name -> session manager
676
+ task: Task description
677
+ working_dir: Working directory
678
+ model: Model name or alias
679
+ adapter: Adapter override (auto-detected from model if None)
680
+ """
681
+ from zwarm.core.registry import get_adapter_for_model, get_default_model, resolve_model
682
+
683
+ # Auto-detect adapter from model if not specified
684
+ if adapter is None:
685
+ detected = get_adapter_for_model(model)
686
+ if detected:
687
+ adapter = detected
688
+ else:
689
+ # Default to codex if model not recognized
690
+ adapter = "codex"
691
+
692
+ # Resolve model alias to canonical name if needed
693
+ model_info = resolve_model(model)
694
+ effective_model = model_info.canonical if model_info else model
695
+
696
+ # Get the right manager
697
+ if adapter not in managers:
698
+ console.print(f" [red]Unknown adapter:[/] {adapter}")
699
+ console.print(f" [dim]Available: {', '.join(managers.keys())}[/]")
700
+ return
701
+
702
+ manager = managers[adapter]
703
+
476
704
  console.print(f"\n[dim]Spawning session...[/]")
705
+ console.print(f" [dim]Adapter:[/] {adapter}")
706
+ console.print(f" [dim]Model:[/] {effective_model}")
477
707
  console.print(f" [dim]Dir:[/] {working_dir}")
478
- console.print(f" [dim]Model:[/] {model}")
479
708
 
480
709
  try:
481
710
  session = manager.start_session(
482
711
  task=task,
483
712
  working_dir=working_dir,
484
- model=model,
713
+ model=effective_model,
485
714
  sandbox="workspace-write",
486
715
  source="user",
487
- adapter="codex",
488
716
  )
489
717
 
490
718
  console.print(f"\n[green]✓[/] Session: [cyan]{session.short_id}[/]")
@@ -613,13 +841,34 @@ def run_interactive(
613
841
  working_dir: Default working directory for sessions
614
842
  model: Default model for sessions
615
843
  """
616
- from zwarm.sessions import CodexSessionManager
844
+ from zwarm.sessions import get_session_manager
845
+ from zwarm.core.registry import get_adapter_for_model, list_adapters
617
846
 
618
- manager = CodexSessionManager(working_dir / ".zwarm")
847
+ # Initialize managers for all adapters
848
+ state_dir = working_dir / ".zwarm"
849
+ managers = {}
850
+ for adapter in list_adapters():
851
+ try:
852
+ managers[adapter] = get_session_manager(adapter, str(state_dir))
853
+ except Exception:
854
+ pass # Adapter not available
855
+
856
+ if not managers:
857
+ console.print("[red]No adapters available. Run 'zwarm init' first.[/]")
858
+ return
859
+
860
+ # Primary manager for listing (aggregates across all adapters)
861
+ primary_adapter = get_adapter_for_model(model) or "codex"
862
+ if primary_adapter not in managers:
863
+ primary_adapter = list(managers.keys())[0]
619
864
 
620
865
  # Setup prompt with autocomplete
621
866
  def get_sessions():
622
- return manager.list_sessions()
867
+ # Aggregate sessions from all managers
868
+ all_sessions = []
869
+ for mgr in managers.values():
870
+ all_sessions.extend(mgr.list_sessions())
871
+ return all_sessions
623
872
 
624
873
  completer = SessionCompleter(get_sessions)
625
874
  style = Style.from_dict({
@@ -637,7 +886,8 @@ def run_interactive(
637
886
  console.print("\n[bold cyan]zwarm interactive[/] - Session Manager\n")
638
887
  console.print(f" [dim]Dir:[/] {working_dir.absolute()}")
639
888
  console.print(f" [dim]Model:[/] {model}")
640
- console.print(f"\n Type [cyan]help[/] for commands, [cyan]quit[/] to exit.")
889
+ console.print(f" [dim]Adapters:[/] {', '.join(managers.keys())}")
890
+ console.print(f"\n Type [cyan]help[/] for commands, [cyan]models[/] to see available models.")
641
891
  console.print(f" [dim]Tab to autocomplete session IDs[/]\n")
642
892
 
643
893
  # REPL
@@ -647,6 +897,29 @@ def run_interactive(
647
897
  if not raw:
648
898
  continue
649
899
 
900
+ # Bang command: !cmd runs shell command
901
+ if raw.startswith("!"):
902
+ import subprocess
903
+ shell_cmd = raw[1:].strip()
904
+ if shell_cmd:
905
+ try:
906
+ result = subprocess.run(
907
+ shell_cmd,
908
+ shell=True,
909
+ cwd=working_dir,
910
+ capture_output=True,
911
+ text=True,
912
+ )
913
+ if result.stdout:
914
+ console.print(result.stdout.rstrip())
915
+ if result.stderr:
916
+ console.print(f"[red]{result.stderr.rstrip()}[/]")
917
+ if result.returncode != 0:
918
+ console.print(f"[dim](exit code: {result.returncode})[/]")
919
+ except Exception as e:
920
+ console.print(f"[red]Error:[/] {e}")
921
+ continue
922
+
650
923
  try:
651
924
  parts = shlex.split(raw)
652
925
  except ValueError:
@@ -655,6 +928,26 @@ def run_interactive(
655
928
  cmd = parts[0].lower()
656
929
  args = parts[1:]
657
930
 
931
+ # Helper to find session and return the correct manager for its adapter
932
+ def find_session(sid: str):
933
+ # First, find the session (any manager can load it)
934
+ session = None
935
+ for mgr in managers.values():
936
+ session = mgr.get_session(sid)
937
+ if session:
938
+ break
939
+
940
+ if not session:
941
+ return None, None
942
+
943
+ # Return the manager that matches the session's adapter
944
+ adapter = getattr(session, "adapter", "codex")
945
+ if adapter in managers:
946
+ return managers[adapter], session
947
+ else:
948
+ # Fallback to whichever manager found it
949
+ return mgr, session
950
+
658
951
  # Dispatch
659
952
  if cmd in ("q", "quit", "exit"):
660
953
  console.print("\n[dim]Goodbye![/]\n")
@@ -663,20 +956,45 @@ def run_interactive(
663
956
  elif cmd in ("h", "help"):
664
957
  cmd_help()
665
958
 
959
+ elif cmd == "models":
960
+ cmd_models()
961
+
666
962
  elif cmd in ("ls", "list"):
667
- cmd_ls(manager)
963
+ # Aggregate sessions from all managers
964
+ from zwarm.sessions import SessionStatus
965
+ from zwarm.core.costs import estimate_session_cost, format_cost
966
+
967
+ all_sessions = []
968
+ for mgr in managers.values():
969
+ all_sessions.extend(mgr.list_sessions())
970
+
971
+ if not all_sessions:
972
+ console.print(" [dim]No sessions. Use 'spawn \"task\"' to start one.[/]")
973
+ else:
974
+ # Use first manager's cmd_ls logic but with aggregated sessions
975
+ cmd_ls_multi(all_sessions, managers)
668
976
 
669
977
  elif cmd in ("?", "peek"):
670
978
  if not args:
671
979
  console.print(" [red]Usage:[/] peek ID")
672
980
  else:
673
- cmd_peek(manager, args[0])
981
+ mgr, _ = find_session(args[0])
982
+ if mgr:
983
+ cmd_peek(mgr, args[0])
984
+ else:
985
+ console.print(f" [red]Session not found:[/] {args[0]}")
674
986
 
675
987
  elif cmd == "show":
676
988
  if not args:
677
- console.print(" [red]Usage:[/] show ID")
989
+ console.print(" [red]Usage:[/] show ID [-v]")
678
990
  else:
679
- cmd_show(manager, args[0])
991
+ verbose = "-v" in args or "--verbose" in args
992
+ sid = [a for a in args if not a.startswith("-")][0]
993
+ mgr, _ = find_session(sid)
994
+ if mgr:
995
+ cmd_show(mgr, sid, verbose=verbose)
996
+ else:
997
+ console.print(f" [red]Session not found:[/] {sid}")
680
998
 
681
999
  elif cmd in ("traj", "trajectory"):
682
1000
  if not args:
@@ -684,22 +1002,31 @@ def run_interactive(
684
1002
  else:
685
1003
  full = "--full" in args
686
1004
  sid = [a for a in args if not a.startswith("-")][0]
687
- cmd_traj(manager, sid, full=full)
1005
+ mgr, _ = find_session(sid)
1006
+ if mgr:
1007
+ cmd_traj(mgr, sid, full=full)
1008
+ else:
1009
+ console.print(f" [red]Session not found:[/] {sid}")
688
1010
 
689
1011
  elif cmd == "watch":
690
1012
  if not args:
691
1013
  console.print(" [red]Usage:[/] watch ID")
692
1014
  else:
693
- cmd_watch(manager, args[0])
1015
+ mgr, _ = find_session(args[0])
1016
+ if mgr:
1017
+ cmd_watch(mgr, args[0])
1018
+ else:
1019
+ console.print(f" [red]Session not found:[/] {args[0]}")
694
1020
 
695
1021
  elif cmd == "spawn":
696
1022
  if not args:
697
- console.print(" [red]Usage:[/] spawn \"task\" [--dir PATH] [--search]")
1023
+ console.print(" [red]Usage:[/] spawn \"task\" [--model M] [--adapter A]")
698
1024
  else:
699
1025
  # Parse spawn args
700
1026
  task_parts = []
701
1027
  spawn_dir = working_dir
702
1028
  spawn_model = model
1029
+ spawn_adapter = None
703
1030
  i = 0
704
1031
  while i < len(args):
705
1032
  if args[i] in ("--dir", "-d") and i + 1 < len(args):
@@ -708,13 +1035,16 @@ def run_interactive(
708
1035
  elif args[i] in ("--model", "-m") and i + 1 < len(args):
709
1036
  spawn_model = args[i + 1]
710
1037
  i += 2
1038
+ elif args[i] in ("--adapter", "-a") and i + 1 < len(args):
1039
+ spawn_adapter = args[i + 1]
1040
+ i += 2
711
1041
  else:
712
1042
  task_parts.append(args[i])
713
1043
  i += 1
714
1044
 
715
1045
  task = " ".join(task_parts)
716
1046
  if task:
717
- cmd_spawn(manager, task, spawn_dir, spawn_model)
1047
+ cmd_spawn(managers, task, spawn_dir, spawn_model, spawn_adapter)
718
1048
  else:
719
1049
  console.print(" [red]Task required[/]")
720
1050
 
@@ -722,19 +1052,57 @@ def run_interactive(
722
1052
  if len(args) < 2:
723
1053
  console.print(" [red]Usage:[/] c ID \"message\"")
724
1054
  else:
725
- cmd_continue(manager, args[0], " ".join(args[1:]))
1055
+ mgr, _ = find_session(args[0])
1056
+ if mgr:
1057
+ cmd_continue(mgr, args[0], " ".join(args[1:]))
1058
+ else:
1059
+ console.print(f" [red]Session not found:[/] {args[0]}")
726
1060
 
727
1061
  elif cmd == "kill":
728
1062
  if not args:
729
1063
  console.print(" [red]Usage:[/] kill ID | all")
1064
+ elif args[0].lower() == "all":
1065
+ # Kill all running across all managers
1066
+ killed = 0
1067
+ for mgr in managers.values():
1068
+ from zwarm.sessions import SessionStatus
1069
+ for s in mgr.list_sessions(status=SessionStatus.RUNNING):
1070
+ if mgr.kill_session(s.id):
1071
+ killed += 1
1072
+ console.print(f" [green]✓[/] Killed {s.short_id}")
1073
+ if killed:
1074
+ console.print(f"\n[green]Killed {killed} session(s)[/]")
1075
+ else:
1076
+ console.print(" [dim]No running sessions[/]")
730
1077
  else:
731
- cmd_kill(manager, args[0])
1078
+ mgr, _ = find_session(args[0])
1079
+ if mgr:
1080
+ cmd_kill(mgr, args[0])
1081
+ else:
1082
+ console.print(f" [red]Session not found:[/] {args[0]}")
732
1083
 
733
1084
  elif cmd in ("rm", "delete"):
734
1085
  if not args:
735
1086
  console.print(" [red]Usage:[/] rm ID | all")
1087
+ elif args[0].lower() == "all":
1088
+ # Delete all non-running across all managers
1089
+ deleted = 0
1090
+ for mgr in managers.values():
1091
+ from zwarm.sessions import SessionStatus
1092
+ for s in mgr.list_sessions():
1093
+ if s.status != SessionStatus.RUNNING:
1094
+ if mgr.delete_session(s.id):
1095
+ deleted += 1
1096
+ if deleted:
1097
+ console.print(f"[green]✓[/] Deleted {deleted} session(s)")
1098
+ else:
1099
+ console.print(" [dim]Nothing to delete[/]")
736
1100
  else:
737
- cmd_rm(manager, args[0])
1101
+ mgr, _ = find_session(args[0])
1102
+ if mgr:
1103
+ cmd_rm(mgr, args[0])
1104
+ else:
1105
+ console.print(f" [red]Session not found:[/] {args[0]}")
738
1106
 
739
1107
  else:
740
1108
  console.print(f" [yellow]Unknown command:[/] {cmd}")