zwarm 3.2.1__py3-none-any.whl → 3.6.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,11 +153,11 @@ 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]', "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)")
@@ -169,6 +169,12 @@ def cmd_help():
169
169
  table.add_row("traj ID [--full]", "Show trajectory (steps taken)")
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)
@@ -307,7 +452,7 @@ def cmd_show(manager, session_id: str):
307
452
  icon = STATUS_ICONS.get(session.status.value, "?")
308
453
  console.print(f"\n{icon} [bold cyan]{session.short_id}[/] - {session.status.value}")
309
454
  console.print(f" [dim]Task:[/] {session.task}")
310
- console.print(f" [dim]Turn:[/] {session.turn} | [dim]Runtime:[/] {session.runtime:.1f}s")
455
+ console.print(f" [dim]Model:[/] {session.model} | [dim]Turn:[/] {session.turn} | [dim]Runtime:[/] {session.runtime}")
311
456
 
312
457
  # Token usage with cost estimate
313
458
  usage = session.token_usage
@@ -409,6 +554,7 @@ def cmd_watch(manager, session_id: str):
409
554
 
410
555
  console.print(f"\n[bold]Watching {session.short_id}[/]...")
411
556
  console.print(f" [dim]Task:[/] {session.task[:60]}...")
557
+ console.print(f" [dim]Model:[/] {session.model}")
412
558
  console.print(f" [dim]Press Ctrl+C to stop watching[/]\n")
413
559
 
414
560
  seen_steps = 0
@@ -471,20 +617,52 @@ def cmd_watch(manager, session_id: str):
471
617
  console.print()
472
618
 
473
619
 
474
- def cmd_spawn(manager, task: str, working_dir: Path, model: str):
475
- """Spawn a new session."""
620
+ def cmd_spawn(managers: dict, task: str, working_dir: Path, model: str, adapter: str | None = None):
621
+ """
622
+ Spawn a new session.
623
+
624
+ Args:
625
+ managers: Dict of adapter name -> session manager
626
+ task: Task description
627
+ working_dir: Working directory
628
+ model: Model name or alias
629
+ adapter: Adapter override (auto-detected from model if None)
630
+ """
631
+ from zwarm.core.registry import get_adapter_for_model, get_default_model, resolve_model
632
+
633
+ # Auto-detect adapter from model if not specified
634
+ if adapter is None:
635
+ detected = get_adapter_for_model(model)
636
+ if detected:
637
+ adapter = detected
638
+ else:
639
+ # Default to codex if model not recognized
640
+ adapter = "codex"
641
+
642
+ # Resolve model alias to canonical name if needed
643
+ model_info = resolve_model(model)
644
+ effective_model = model_info.canonical if model_info else model
645
+
646
+ # Get the right manager
647
+ if adapter not in managers:
648
+ console.print(f" [red]Unknown adapter:[/] {adapter}")
649
+ console.print(f" [dim]Available: {', '.join(managers.keys())}[/]")
650
+ return
651
+
652
+ manager = managers[adapter]
653
+
476
654
  console.print(f"\n[dim]Spawning session...[/]")
655
+ console.print(f" [dim]Adapter:[/] {adapter}")
656
+ console.print(f" [dim]Model:[/] {effective_model}")
477
657
  console.print(f" [dim]Dir:[/] {working_dir}")
478
- console.print(f" [dim]Model:[/] {model}")
479
658
 
480
659
  try:
481
660
  session = manager.start_session(
482
661
  task=task,
483
662
  working_dir=working_dir,
484
- model=model,
663
+ model=effective_model,
485
664
  sandbox="workspace-write",
486
665
  source="user",
487
- adapter="codex",
488
666
  )
489
667
 
490
668
  console.print(f"\n[green]✓[/] Session: [cyan]{session.short_id}[/]")
@@ -613,13 +791,34 @@ def run_interactive(
613
791
  working_dir: Default working directory for sessions
614
792
  model: Default model for sessions
615
793
  """
616
- from zwarm.sessions import CodexSessionManager
794
+ from zwarm.sessions import get_session_manager
795
+ from zwarm.core.registry import get_adapter_for_model, list_adapters
796
+
797
+ # Initialize managers for all adapters
798
+ state_dir = working_dir / ".zwarm"
799
+ managers = {}
800
+ for adapter in list_adapters():
801
+ try:
802
+ managers[adapter] = get_session_manager(adapter, str(state_dir))
803
+ except Exception:
804
+ pass # Adapter not available
617
805
 
618
- manager = CodexSessionManager(working_dir / ".zwarm")
806
+ if not managers:
807
+ console.print("[red]No adapters available. Run 'zwarm init' first.[/]")
808
+ return
809
+
810
+ # Primary manager for listing (aggregates across all adapters)
811
+ primary_adapter = get_adapter_for_model(model) or "codex"
812
+ if primary_adapter not in managers:
813
+ primary_adapter = list(managers.keys())[0]
619
814
 
620
815
  # Setup prompt with autocomplete
621
816
  def get_sessions():
622
- return manager.list_sessions()
817
+ # Aggregate sessions from all managers
818
+ all_sessions = []
819
+ for mgr in managers.values():
820
+ all_sessions.extend(mgr.list_sessions())
821
+ return all_sessions
623
822
 
624
823
  completer = SessionCompleter(get_sessions)
625
824
  style = Style.from_dict({
@@ -637,7 +836,8 @@ def run_interactive(
637
836
  console.print("\n[bold cyan]zwarm interactive[/] - Session Manager\n")
638
837
  console.print(f" [dim]Dir:[/] {working_dir.absolute()}")
639
838
  console.print(f" [dim]Model:[/] {model}")
640
- console.print(f"\n Type [cyan]help[/] for commands, [cyan]quit[/] to exit.")
839
+ console.print(f" [dim]Adapters:[/] {', '.join(managers.keys())}")
840
+ console.print(f"\n Type [cyan]help[/] for commands, [cyan]models[/] to see available models.")
641
841
  console.print(f" [dim]Tab to autocomplete session IDs[/]\n")
642
842
 
643
843
  # REPL
@@ -647,6 +847,29 @@ def run_interactive(
647
847
  if not raw:
648
848
  continue
649
849
 
850
+ # Bang command: !cmd runs shell command
851
+ if raw.startswith("!"):
852
+ import subprocess
853
+ shell_cmd = raw[1:].strip()
854
+ if shell_cmd:
855
+ try:
856
+ result = subprocess.run(
857
+ shell_cmd,
858
+ shell=True,
859
+ cwd=working_dir,
860
+ capture_output=True,
861
+ text=True,
862
+ )
863
+ if result.stdout:
864
+ console.print(result.stdout.rstrip())
865
+ if result.stderr:
866
+ console.print(f"[red]{result.stderr.rstrip()}[/]")
867
+ if result.returncode != 0:
868
+ console.print(f"[dim](exit code: {result.returncode})[/]")
869
+ except Exception as e:
870
+ console.print(f"[red]Error:[/] {e}")
871
+ continue
872
+
650
873
  try:
651
874
  parts = shlex.split(raw)
652
875
  except ValueError:
@@ -655,6 +878,26 @@ def run_interactive(
655
878
  cmd = parts[0].lower()
656
879
  args = parts[1:]
657
880
 
881
+ # Helper to find session and return the correct manager for its adapter
882
+ def find_session(sid: str):
883
+ # First, find the session (any manager can load it)
884
+ session = None
885
+ for mgr in managers.values():
886
+ session = mgr.get_session(sid)
887
+ if session:
888
+ break
889
+
890
+ if not session:
891
+ return None, None
892
+
893
+ # Return the manager that matches the session's adapter
894
+ adapter = getattr(session, "adapter", "codex")
895
+ if adapter in managers:
896
+ return managers[adapter], session
897
+ else:
898
+ # Fallback to whichever manager found it
899
+ return mgr, session
900
+
658
901
  # Dispatch
659
902
  if cmd in ("q", "quit", "exit"):
660
903
  console.print("\n[dim]Goodbye![/]\n")
@@ -663,20 +906,43 @@ def run_interactive(
663
906
  elif cmd in ("h", "help"):
664
907
  cmd_help()
665
908
 
909
+ elif cmd == "models":
910
+ cmd_models()
911
+
666
912
  elif cmd in ("ls", "list"):
667
- cmd_ls(manager)
913
+ # Aggregate sessions from all managers
914
+ from zwarm.sessions import SessionStatus
915
+ from zwarm.core.costs import estimate_session_cost, format_cost
916
+
917
+ all_sessions = []
918
+ for mgr in managers.values():
919
+ all_sessions.extend(mgr.list_sessions())
920
+
921
+ if not all_sessions:
922
+ console.print(" [dim]No sessions. Use 'spawn \"task\"' to start one.[/]")
923
+ else:
924
+ # Use first manager's cmd_ls logic but with aggregated sessions
925
+ cmd_ls_multi(all_sessions, managers)
668
926
 
669
927
  elif cmd in ("?", "peek"):
670
928
  if not args:
671
929
  console.print(" [red]Usage:[/] peek ID")
672
930
  else:
673
- cmd_peek(manager, args[0])
931
+ mgr, _ = find_session(args[0])
932
+ if mgr:
933
+ cmd_peek(mgr, args[0])
934
+ else:
935
+ console.print(f" [red]Session not found:[/] {args[0]}")
674
936
 
675
937
  elif cmd == "show":
676
938
  if not args:
677
939
  console.print(" [red]Usage:[/] show ID")
678
940
  else:
679
- cmd_show(manager, args[0])
941
+ mgr, _ = find_session(args[0])
942
+ if mgr:
943
+ cmd_show(mgr, args[0])
944
+ else:
945
+ console.print(f" [red]Session not found:[/] {args[0]}")
680
946
 
681
947
  elif cmd in ("traj", "trajectory"):
682
948
  if not args:
@@ -684,22 +950,31 @@ def run_interactive(
684
950
  else:
685
951
  full = "--full" in args
686
952
  sid = [a for a in args if not a.startswith("-")][0]
687
- cmd_traj(manager, sid, full=full)
953
+ mgr, _ = find_session(sid)
954
+ if mgr:
955
+ cmd_traj(mgr, sid, full=full)
956
+ else:
957
+ console.print(f" [red]Session not found:[/] {sid}")
688
958
 
689
959
  elif cmd == "watch":
690
960
  if not args:
691
961
  console.print(" [red]Usage:[/] watch ID")
692
962
  else:
693
- cmd_watch(manager, args[0])
963
+ mgr, _ = find_session(args[0])
964
+ if mgr:
965
+ cmd_watch(mgr, args[0])
966
+ else:
967
+ console.print(f" [red]Session not found:[/] {args[0]}")
694
968
 
695
969
  elif cmd == "spawn":
696
970
  if not args:
697
- console.print(" [red]Usage:[/] spawn \"task\" [--dir PATH]")
971
+ console.print(" [red]Usage:[/] spawn \"task\" [--model M] [--adapter A]")
698
972
  else:
699
973
  # Parse spawn args
700
974
  task_parts = []
701
975
  spawn_dir = working_dir
702
976
  spawn_model = model
977
+ spawn_adapter = None
703
978
  i = 0
704
979
  while i < len(args):
705
980
  if args[i] in ("--dir", "-d") and i + 1 < len(args):
@@ -708,13 +983,16 @@ def run_interactive(
708
983
  elif args[i] in ("--model", "-m") and i + 1 < len(args):
709
984
  spawn_model = args[i + 1]
710
985
  i += 2
986
+ elif args[i] in ("--adapter", "-a") and i + 1 < len(args):
987
+ spawn_adapter = args[i + 1]
988
+ i += 2
711
989
  else:
712
990
  task_parts.append(args[i])
713
991
  i += 1
714
992
 
715
993
  task = " ".join(task_parts)
716
994
  if task:
717
- cmd_spawn(manager, task, spawn_dir, spawn_model)
995
+ cmd_spawn(managers, task, spawn_dir, spawn_model, spawn_adapter)
718
996
  else:
719
997
  console.print(" [red]Task required[/]")
720
998
 
@@ -722,19 +1000,57 @@ def run_interactive(
722
1000
  if len(args) < 2:
723
1001
  console.print(" [red]Usage:[/] c ID \"message\"")
724
1002
  else:
725
- cmd_continue(manager, args[0], " ".join(args[1:]))
1003
+ mgr, _ = find_session(args[0])
1004
+ if mgr:
1005
+ cmd_continue(mgr, args[0], " ".join(args[1:]))
1006
+ else:
1007
+ console.print(f" [red]Session not found:[/] {args[0]}")
726
1008
 
727
1009
  elif cmd == "kill":
728
1010
  if not args:
729
1011
  console.print(" [red]Usage:[/] kill ID | all")
1012
+ elif args[0].lower() == "all":
1013
+ # Kill all running across all managers
1014
+ killed = 0
1015
+ for mgr in managers.values():
1016
+ from zwarm.sessions import SessionStatus
1017
+ for s in mgr.list_sessions(status=SessionStatus.RUNNING):
1018
+ if mgr.kill_session(s.id):
1019
+ killed += 1
1020
+ console.print(f" [green]✓[/] Killed {s.short_id}")
1021
+ if killed:
1022
+ console.print(f"\n[green]Killed {killed} session(s)[/]")
1023
+ else:
1024
+ console.print(" [dim]No running sessions[/]")
730
1025
  else:
731
- cmd_kill(manager, args[0])
1026
+ mgr, _ = find_session(args[0])
1027
+ if mgr:
1028
+ cmd_kill(mgr, args[0])
1029
+ else:
1030
+ console.print(f" [red]Session not found:[/] {args[0]}")
732
1031
 
733
1032
  elif cmd in ("rm", "delete"):
734
1033
  if not args:
735
1034
  console.print(" [red]Usage:[/] rm ID | all")
1035
+ elif args[0].lower() == "all":
1036
+ # Delete all non-running across all managers
1037
+ deleted = 0
1038
+ for mgr in managers.values():
1039
+ from zwarm.sessions import SessionStatus
1040
+ for s in mgr.list_sessions():
1041
+ if s.status != SessionStatus.RUNNING:
1042
+ if mgr.delete_session(s.id):
1043
+ deleted += 1
1044
+ if deleted:
1045
+ console.print(f"[green]✓[/] Deleted {deleted} session(s)")
1046
+ else:
1047
+ console.print(" [dim]Nothing to delete[/]")
736
1048
  else:
737
- cmd_rm(manager, args[0])
1049
+ mgr, _ = find_session(args[0])
1050
+ if mgr:
1051
+ cmd_rm(mgr, args[0])
1052
+ else:
1053
+ console.print(f" [red]Session not found:[/] {args[0]}")
738
1054
 
739
1055
  else:
740
1056
  console.print(f" [yellow]Unknown command:[/] {cmd}")