ripperdoc 0.2.9__py3-none-any.whl → 0.2.10__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.
Files changed (45) hide show
  1. ripperdoc/__init__.py +1 -1
  2. ripperdoc/cli/cli.py +235 -14
  3. ripperdoc/cli/commands/__init__.py +2 -0
  4. ripperdoc/cli/commands/agents_cmd.py +132 -5
  5. ripperdoc/cli/commands/clear_cmd.py +8 -0
  6. ripperdoc/cli/commands/exit_cmd.py +1 -0
  7. ripperdoc/cli/commands/models_cmd.py +3 -3
  8. ripperdoc/cli/commands/resume_cmd.py +4 -0
  9. ripperdoc/cli/commands/stats_cmd.py +244 -0
  10. ripperdoc/cli/ui/panels.py +1 -0
  11. ripperdoc/cli/ui/rich_ui.py +295 -24
  12. ripperdoc/cli/ui/spinner.py +30 -18
  13. ripperdoc/cli/ui/thinking_spinner.py +1 -2
  14. ripperdoc/cli/ui/wizard.py +6 -8
  15. ripperdoc/core/agents.py +10 -3
  16. ripperdoc/core/config.py +3 -6
  17. ripperdoc/core/default_tools.py +90 -10
  18. ripperdoc/core/hooks/events.py +4 -0
  19. ripperdoc/core/hooks/llm_callback.py +59 -0
  20. ripperdoc/core/permissions.py +78 -4
  21. ripperdoc/core/providers/openai.py +29 -19
  22. ripperdoc/core/query.py +192 -31
  23. ripperdoc/core/tool.py +9 -4
  24. ripperdoc/sdk/client.py +77 -2
  25. ripperdoc/tools/background_shell.py +305 -134
  26. ripperdoc/tools/bash_tool.py +42 -13
  27. ripperdoc/tools/file_edit_tool.py +159 -50
  28. ripperdoc/tools/file_read_tool.py +20 -0
  29. ripperdoc/tools/file_write_tool.py +7 -8
  30. ripperdoc/tools/lsp_tool.py +615 -0
  31. ripperdoc/tools/task_tool.py +514 -65
  32. ripperdoc/utils/conversation_compaction.py +1 -1
  33. ripperdoc/utils/file_watch.py +206 -3
  34. ripperdoc/utils/lsp.py +806 -0
  35. ripperdoc/utils/message_formatting.py +5 -2
  36. ripperdoc/utils/messages.py +21 -1
  37. ripperdoc/utils/permissions/tool_permission_utils.py +174 -15
  38. ripperdoc/utils/session_heatmap.py +244 -0
  39. ripperdoc/utils/session_stats.py +293 -0
  40. {ripperdoc-0.2.9.dist-info → ripperdoc-0.2.10.dist-info}/METADATA +8 -2
  41. {ripperdoc-0.2.9.dist-info → ripperdoc-0.2.10.dist-info}/RECORD +45 -39
  42. {ripperdoc-0.2.9.dist-info → ripperdoc-0.2.10.dist-info}/WHEEL +0 -0
  43. {ripperdoc-0.2.9.dist-info → ripperdoc-0.2.10.dist-info}/entry_points.txt +0 -0
  44. {ripperdoc-0.2.9.dist-info → ripperdoc-0.2.10.dist-info}/licenses/LICENSE +0 -0
  45. {ripperdoc-0.2.9.dist-info → ripperdoc-0.2.10.dist-info}/top_level.txt +0 -0
ripperdoc/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """Ripperdoc - AI-powered coding agent."""
2
2
 
3
- __version__ = "0.2.9"
3
+ __version__ = "0.2.10"
ripperdoc/cli/cli.py CHANGED
@@ -6,6 +6,7 @@ This module provides the command-line interface for the Ripperdoc agent.
6
6
  import asyncio
7
7
  import click
8
8
  import sys
9
+ import time
9
10
  import uuid
10
11
  from pathlib import Path
11
12
  from typing import Any, Dict, List, Optional
@@ -16,19 +17,27 @@ from ripperdoc.core.config import (
16
17
  get_project_config,
17
18
  )
18
19
  from ripperdoc.cli.ui.wizard import check_onboarding
19
- from ripperdoc.core.default_tools import get_default_tools
20
+ from ripperdoc.core.default_tools import get_default_tools, BUILTIN_TOOL_NAMES
20
21
  from ripperdoc.core.query import query, QueryContext
21
22
  from ripperdoc.core.system_prompt import build_system_prompt
22
23
  from ripperdoc.core.skills import build_skill_summary, load_all_skills
23
24
  from ripperdoc.core.hooks.manager import hook_manager
25
+ from ripperdoc.core.hooks.llm_callback import build_hook_llm_callback
24
26
  from ripperdoc.utils.messages import create_user_message
25
27
  from ripperdoc.utils.memory import build_memory_instructions
26
28
  from ripperdoc.core.permissions import make_permission_checker
29
+ from ripperdoc.utils.session_history import (
30
+ SessionHistory,
31
+ list_session_summaries,
32
+ load_session_messages,
33
+ )
27
34
  from ripperdoc.utils.mcp import (
28
35
  load_mcp_servers_async,
29
36
  format_mcp_instructions,
30
37
  shutdown_mcp_runtime,
31
38
  )
39
+ from ripperdoc.utils.lsp import shutdown_lsp_manager
40
+ from ripperdoc.tools.background_shell import shutdown_background_shell
32
41
  from ripperdoc.tools.mcp_tools import load_dynamic_mcp_tools_async, merge_tools_with_dynamic
33
42
  from ripperdoc.utils.log import enable_session_file_logging, get_logger
34
43
 
@@ -42,12 +51,51 @@ console = Console()
42
51
  logger = get_logger()
43
52
 
44
53
 
54
+ def parse_tools_option(tools_arg: Optional[str]) -> Optional[List[str]]:
55
+ """Parse the --tools argument.
56
+
57
+ Args:
58
+ tools_arg: The raw tools argument from CLI.
59
+
60
+ Returns:
61
+ None for default (all tools), empty list for "" (no tools),
62
+ or a list of tool names for filtering.
63
+ """
64
+ if tools_arg is None:
65
+ return None # Use all default tools
66
+
67
+ tools_arg = tools_arg.strip()
68
+
69
+ if tools_arg == "":
70
+ return [] # Disable all tools
71
+
72
+ if tools_arg.lower() == "default":
73
+ return None # Use all default tools
74
+
75
+ # Parse comma-separated list
76
+ tool_names = [name.strip() for name in tools_arg.split(",") if name.strip()]
77
+
78
+ # Validate tool names
79
+ invalid_tools = [name for name in tool_names if name not in BUILTIN_TOOL_NAMES]
80
+ if invalid_tools:
81
+ logger.warning(
82
+ "[cli] Unknown tools specified: %s. Available tools: %s",
83
+ ", ".join(invalid_tools),
84
+ ", ".join(BUILTIN_TOOL_NAMES),
85
+ )
86
+
87
+ return tool_names if tool_names else None
88
+
89
+
45
90
  async def run_query(
46
91
  prompt: str,
47
92
  tools: list,
48
93
  yolo_mode: bool = False,
49
94
  verbose: bool = False,
50
95
  session_id: Optional[str] = None,
96
+ custom_system_prompt: Optional[str] = None,
97
+ append_system_prompt: Optional[str] = None,
98
+ model: Optional[str] = None,
51
99
  ) -> None:
52
100
  """Run a single query and print the response."""
53
101
 
@@ -58,6 +106,9 @@ async def run_query(
58
106
  "verbose": verbose,
59
107
  "session_id": session_id,
60
108
  "prompt_length": len(prompt),
109
+ "model": model,
110
+ "has_custom_system_prompt": custom_system_prompt is not None,
111
+ "has_append_system_prompt": append_system_prompt is not None,
61
112
  },
62
113
  )
63
114
  if prompt:
@@ -72,15 +123,32 @@ async def run_query(
72
123
  # Initialize hook manager
73
124
  hook_manager.set_project_dir(project_path)
74
125
  hook_manager.set_session_id(session_id)
126
+ hook_manager.set_llm_callback(build_hook_llm_callback())
127
+ session_history = SessionHistory(project_path, session_id or str(uuid.uuid4()))
128
+ hook_manager.set_transcript_path(str(session_history.path))
129
+
130
+ def _collect_hook_contexts(result: Any) -> List[str]:
131
+ contexts: List[str] = []
132
+ system_message = getattr(result, "system_message", None)
133
+ additional_context = getattr(result, "additional_context", None)
134
+ if system_message:
135
+ contexts.append(str(system_message))
136
+ if additional_context:
137
+ contexts.append(str(additional_context))
138
+ return contexts
75
139
 
76
140
  # Create initial user message
77
141
  from ripperdoc.utils.messages import UserMessage, AssistantMessage, ProgressMessage
78
142
 
79
143
  messages: List[UserMessage | AssistantMessage | ProgressMessage] = [create_user_message(prompt)]
144
+ session_history.append(messages[0])
80
145
 
81
146
  # Create query context
82
- query_context = QueryContext(tools=tools, yolo_mode=yolo_mode, verbose=verbose)
147
+ query_context = QueryContext(
148
+ tools=tools, yolo_mode=yolo_mode, verbose=verbose, model=model or "main"
149
+ )
83
150
 
151
+ session_start_time = time.time()
84
152
  try:
85
153
  context: Dict[str, Any] = {}
86
154
  # System prompt
@@ -103,13 +171,46 @@ async def run_query(
103
171
  memory_instructions = build_memory_instructions()
104
172
  if memory_instructions:
105
173
  additional_instructions.append(memory_instructions)
106
- system_prompt = build_system_prompt(
107
- tools,
108
- prompt,
109
- context,
110
- additional_instructions=additional_instructions or None,
111
- mcp_instructions=mcp_instructions,
112
- )
174
+
175
+ session_start_result = await hook_manager.run_session_start_async("startup")
176
+ session_hook_contexts = _collect_hook_contexts(session_start_result)
177
+ if session_hook_contexts:
178
+ additional_instructions.extend(session_hook_contexts)
179
+
180
+ prompt_hook_result = await hook_manager.run_user_prompt_submit_async(prompt)
181
+ if prompt_hook_result.should_block or not prompt_hook_result.should_continue:
182
+ reason = (
183
+ prompt_hook_result.block_reason
184
+ or prompt_hook_result.stop_reason
185
+ or "Prompt blocked by hook."
186
+ )
187
+ console.print(f"[red]{escape(str(reason))}[/red]")
188
+ return
189
+ prompt_hook_contexts = _collect_hook_contexts(prompt_hook_result)
190
+ if prompt_hook_contexts:
191
+ additional_instructions.extend(prompt_hook_contexts)
192
+
193
+ # Build system prompt based on options:
194
+ # - custom_system_prompt: replaces the default entirely
195
+ # - append_system_prompt: appends to the default system prompt
196
+ if custom_system_prompt:
197
+ # Complete replacement
198
+ system_prompt = custom_system_prompt
199
+ # Still append if both are provided
200
+ if append_system_prompt:
201
+ system_prompt = f"{system_prompt}\n\n{append_system_prompt}"
202
+ else:
203
+ # Build default with optional append
204
+ all_instructions = list(additional_instructions) if additional_instructions else []
205
+ if append_system_prompt:
206
+ all_instructions.append(append_system_prompt)
207
+ system_prompt = build_system_prompt(
208
+ tools,
209
+ prompt,
210
+ context,
211
+ additional_instructions=all_instructions or None,
212
+ mcp_instructions=mcp_instructions,
213
+ )
113
214
 
114
215
  # Run the query
115
216
  try:
@@ -158,6 +259,7 @@ async def run_query(
158
259
 
159
260
  # Add message to history
160
261
  messages.append(message) # type: ignore[arg-type]
262
+ session_history.append(message) # type: ignore[arg-type]
161
263
 
162
264
  except KeyboardInterrupt:
163
265
  console.print("\n[yellow]Interrupted by user[/yellow]")
@@ -180,7 +282,29 @@ async def run_query(
180
282
  extra={"session_id": session_id, "message_count": len(messages)},
181
283
  )
182
284
  finally:
285
+ duration = max(time.time() - session_start_time, 0.0)
286
+ try:
287
+ await hook_manager.run_session_end_async(
288
+ "other", duration_seconds=duration, message_count=len(messages)
289
+ )
290
+ except (OSError, RuntimeError, ConnectionError, ValueError, TypeError) as exc:
291
+ logger.warning(
292
+ "[cli] SessionEnd hook failed: %s: %s",
293
+ type(exc).__name__,
294
+ exc,
295
+ extra={"session_id": session_id},
296
+ )
183
297
  await shutdown_mcp_runtime()
298
+ await shutdown_lsp_manager()
299
+ # Shutdown background shell manager
300
+ try:
301
+ shutdown_background_shell(force=True)
302
+ except (OSError, RuntimeError) as exc:
303
+ logger.debug(
304
+ "[cli] Failed to shut down background shell: %s: %s",
305
+ type(exc).__name__,
306
+ exc,
307
+ )
184
308
  logger.debug("[cli] Shutdown MCP runtime", extra={"session_id": session_id})
185
309
 
186
310
 
@@ -194,11 +318,59 @@ async def run_query(
194
318
  help="YOLO mode: skip all permission prompts for tools",
195
319
  )
196
320
  @click.option("--verbose", is_flag=True, help="Verbose output")
197
- @click.option("--show-full-thinking", is_flag=True, help="Show full reasoning content instead of truncated preview")
321
+ @click.option(
322
+ "--show-full-thinking/--hide-full-thinking",
323
+ default=None,
324
+ help="Show full reasoning content instead of truncated preview",
325
+ )
198
326
  @click.option("-p", "--prompt", type=str, help="Direct prompt (non-interactive)")
327
+ @click.option(
328
+ "--tools",
329
+ type=str,
330
+ default=None,
331
+ help=(
332
+ 'Specify the list of available tools. Use "" to disable all tools, '
333
+ '"default" to use all tools, or specify tool names (e.g. "Bash,Edit,Read").'
334
+ ),
335
+ )
336
+ @click.option(
337
+ "--system-prompt",
338
+ type=str,
339
+ default=None,
340
+ help="System prompt to use for the session (replaces default).",
341
+ )
342
+ @click.option(
343
+ "--append-system-prompt",
344
+ type=str,
345
+ default=None,
346
+ help="Additional instructions to append to the system prompt.",
347
+ )
348
+ @click.option(
349
+ "--model",
350
+ type=str,
351
+ default=None,
352
+ help="Model profile for the current session.",
353
+ )
354
+ @click.option(
355
+ "-c",
356
+ "--continue",
357
+ "continue_session",
358
+ is_flag=True,
359
+ help="Continue the most recent conversation in the current directory.",
360
+ )
199
361
  @click.pass_context
200
362
  def cli(
201
- ctx: click.Context, cwd: Optional[str], yolo: bool, verbose: bool, show_full_thinking: bool, prompt: Optional[str]
363
+ ctx: click.Context,
364
+ cwd: Optional[str],
365
+ yolo: bool,
366
+ verbose: bool,
367
+ show_full_thinking: Optional[bool],
368
+ prompt: Optional[str],
369
+ tools: Optional[str],
370
+ system_prompt: Optional[str],
371
+ append_system_prompt: Optional[str],
372
+ model: Optional[str],
373
+ continue_session: bool,
202
374
  ) -> None:
203
375
  """Ripperdoc - AI-powered coding agent"""
204
376
  session_id = str(uuid.uuid4())
@@ -237,15 +409,59 @@ def cli(
237
409
  get_project_config(project_path)
238
410
 
239
411
  yolo_mode = yolo
412
+ # Parse --tools option
413
+ allowed_tools = parse_tools_option(tools)
414
+
415
+ # Handle --continue option: load the most recent session
416
+ resume_messages = None
417
+ if continue_session:
418
+ summaries = list_session_summaries(project_path)
419
+ if summaries:
420
+ most_recent = summaries[0]
421
+ session_id = most_recent.session_id
422
+ resume_messages = load_session_messages(project_path, session_id)
423
+ logger.info(
424
+ "[cli] Continuing session",
425
+ extra={
426
+ "session_id": session_id,
427
+ "message_count": len(resume_messages),
428
+ "last_prompt": most_recent.last_prompt,
429
+ },
430
+ )
431
+ console.print(f"[dim]Continuing session: {most_recent.last_prompt}[/dim]")
432
+ else:
433
+ logger.warning("[cli] No previous sessions found to continue")
434
+ console.print("[yellow]No previous sessions found in this directory.[/yellow]")
435
+
240
436
  logger.debug(
241
437
  "[cli] Configuration initialized",
242
- extra={"session_id": session_id, "yolo_mode": yolo_mode, "verbose": verbose},
438
+ extra={
439
+ "session_id": session_id,
440
+ "yolo_mode": yolo_mode,
441
+ "verbose": verbose,
442
+ "allowed_tools": allowed_tools,
443
+ "model": model,
444
+ "has_system_prompt": system_prompt is not None,
445
+ "has_append_system_prompt": append_system_prompt is not None,
446
+ "continue_session": continue_session,
447
+ },
243
448
  )
244
449
 
245
450
  # If prompt is provided, run directly
246
451
  if prompt:
247
- tools = get_default_tools()
248
- asyncio.run(run_query(prompt, tools, yolo_mode, verbose, session_id=session_id))
452
+ tool_list = get_default_tools(allowed_tools=allowed_tools)
453
+ asyncio.run(
454
+ run_query(
455
+ prompt,
456
+ tool_list,
457
+ yolo_mode,
458
+ verbose,
459
+ session_id=session_id,
460
+ custom_system_prompt=system_prompt,
461
+ append_system_prompt=append_system_prompt,
462
+ model=model,
463
+ )
464
+ )
249
465
  return
250
466
 
251
467
  # If no command specified, start interactive REPL with Rich interface
@@ -259,6 +475,11 @@ def cli(
259
475
  show_full_thinking=show_full_thinking,
260
476
  session_id=session_id,
261
477
  log_file_path=log_file,
478
+ allowed_tools=allowed_tools,
479
+ custom_system_prompt=system_prompt,
480
+ append_system_prompt=append_system_prompt,
481
+ model=model,
482
+ resume_messages=resume_messages,
262
483
  )
263
484
  return
264
485
 
@@ -21,6 +21,7 @@ from .mcp_cmd import command as mcp_command
21
21
  from .models_cmd import command as models_command
22
22
  from .permissions_cmd import command as permissions_command
23
23
  from .resume_cmd import command as resume_command
24
+ from .stats_cmd import command as stats_command
24
25
  from .tasks_cmd import command as tasks_command
25
26
  from .status_cmd import command as status_command
26
27
  from .todos_cmd import command as todos_command
@@ -51,6 +52,7 @@ ALL_COMMANDS: List[SlashCommand] = [
51
52
  models_command,
52
53
  exit_command,
53
54
  status_command,
55
+ stats_command,
54
56
  doctor_command,
55
57
  memory_command,
56
58
  permissions_command,
@@ -1,4 +1,7 @@
1
1
  from rich.markup import escape
2
+ from rich import box
3
+ from rich.panel import Panel
4
+ from rich.table import Table
2
5
 
3
6
  from ripperdoc.core.agents import (
4
7
  AGENT_DIR_NAME,
@@ -8,14 +11,38 @@ from ripperdoc.core.agents import (
8
11
  save_agent_definition,
9
12
  )
10
13
  from ripperdoc.core.config import get_global_config
14
+ from ripperdoc.tools.task_tool import (
15
+ list_agent_runs,
16
+ get_agent_run_snapshot,
17
+ cancel_agent_run,
18
+ )
11
19
  from ripperdoc.utils.log import get_logger
12
20
 
13
- from typing import Any
21
+ from typing import Any, Dict, Optional
14
22
  from .base import SlashCommand
15
23
 
16
24
  logger = get_logger()
17
25
 
18
26
 
27
+ def _format_duration(duration_ms: float | None) -> str:
28
+ if duration_ms is None:
29
+ return "-"
30
+ try:
31
+ duration = float(duration_ms)
32
+ except (TypeError, ValueError):
33
+ return "-"
34
+ if duration < 1000:
35
+ return f"{int(duration)} ms"
36
+ seconds = duration / 1000.0
37
+ if seconds < 60:
38
+ return f"{seconds:.1f}s"
39
+ minutes, secs = divmod(int(seconds), 60)
40
+ if minutes < 60:
41
+ return f"{minutes}m {secs}s"
42
+ hours, mins = divmod(minutes, 60)
43
+ return f"{hours}h {mins}m"
44
+
45
+
19
46
  def _handle(ui: Any, trimmed_arg: str) -> bool:
20
47
  console = ui.console
21
48
  tokens = trimmed_arg.split()
@@ -39,18 +66,118 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
39
66
  "[bold]/agents delete <name> [location][/bold] — "
40
67
  "delete agent (location: user|project, default user)"
41
68
  )
69
+ console.print("[bold]/agents runs[/bold] — list subagent runs")
70
+ console.print("[bold]/agents show <id>[/bold] — show subagent run details")
71
+ console.print("[bold]/agents cancel <id>[/bold] — cancel a background subagent run")
42
72
  console.print(
43
73
  f"[dim]Agent files live in ~/.ripperdoc/{AGENT_DIR_NAME} "
44
74
  f"or ./.ripperdoc/{AGENT_DIR_NAME}[/dim]"
45
75
  )
46
76
  console.print(
47
- "[dim]Model can be a profile name or pointer (task/main/etc). Defaults to 'task'.[/dim]"
77
+ "[dim]Model can be a profile name or pointer (main/quick). Defaults to 'main'.[/dim]"
48
78
  )
49
79
 
50
80
  if subcmd in ("help", "-h", "--help"):
51
81
  print_agents_usage()
52
82
  return True
53
83
 
84
+ if subcmd in ("runs", "run", "tasks", "status"):
85
+ console = ui.console
86
+ run_ids = list_agent_runs()
87
+ if not run_ids:
88
+ console.print(
89
+ Panel("No subagent runs recorded", title="Subagent runs", box=box.ROUNDED)
90
+ )
91
+ return True
92
+
93
+ table = Table(box=box.SIMPLE_HEAVY, expand=True)
94
+ table.add_column("ID", style="cyan", no_wrap=True)
95
+ table.add_column("Status", style="magenta", no_wrap=True)
96
+ table.add_column("Agent", style="white", no_wrap=True)
97
+ table.add_column("Duration", style="dim", no_wrap=True)
98
+ table.add_column("Background", style="dim", no_wrap=True)
99
+ table.add_column("Result", style="white")
100
+
101
+ for run_id in sorted(run_ids):
102
+ snapshot: Dict[Any, Any] = get_agent_run_snapshot(run_id) or {}
103
+ result_text = snapshot.get("result_text") or snapshot.get("error") or ""
104
+ result_preview = (
105
+ result_text if len(result_text) <= 80 else result_text[:77] + "..."
106
+ )
107
+ table.add_row(
108
+ escape(run_id),
109
+ escape(snapshot.get("status") or "unknown"),
110
+ escape(snapshot.get("agent_type") or "unknown"),
111
+ _format_duration(snapshot.get("duration_ms")),
112
+ "yes" if snapshot.get("is_background") else "no",
113
+ escape(result_preview),
114
+ )
115
+
116
+ console.print(
117
+ Panel(table, title="Subagent runs", box=box.ROUNDED, padding=(1, 2)),
118
+ markup=False,
119
+ )
120
+ console.print(
121
+ "[dim]Use /agents show <id> for details or /agents cancel <id> to stop a background run.[/dim]"
122
+ )
123
+ return True
124
+
125
+ if subcmd in ("show", "info", "details"):
126
+ if len(tokens) < 2:
127
+ console.print("[red]Usage: /agents show <id>[/red]")
128
+ return True
129
+ run_id = tokens[1]
130
+ snapshot = get_agent_run_snapshot(run_id) # type: ignore[assignment]
131
+ if not snapshot:
132
+ console.print(f"[red]No subagent run found with id '{escape(run_id)}'.[/red]")
133
+ return True
134
+ details = Table(box=box.SIMPLE_HEAVY, show_header=False)
135
+ details.add_row("ID", escape(run_id))
136
+ details.add_row("Status", escape(snapshot.get("status") or "unknown"))
137
+ details.add_row("Agent", escape(snapshot.get("agent_type") or "unknown"))
138
+ details.add_row("Duration", _format_duration(snapshot.get("duration_ms")))
139
+ details.add_row(
140
+ "Background", "yes" if snapshot.get("is_background") else "no"
141
+ )
142
+ if snapshot.get("model_used"):
143
+ details.add_row("Model", escape(str(snapshot.get("model_used"))))
144
+ if snapshot.get("tool_use_count"):
145
+ details.add_row("Tool uses", str(snapshot.get("tool_use_count")))
146
+ if snapshot.get("missing_tools"):
147
+ details.add_row("Missing tools", escape(", ".join(snapshot["missing_tools"])))
148
+ if snapshot.get("error"):
149
+ details.add_row("Error", escape(str(snapshot.get("error"))))
150
+ console.print(
151
+ Panel(details, title=f"Subagent {escape(run_id)}", box=box.ROUNDED, padding=(1, 2)),
152
+ markup=False,
153
+ )
154
+ result_text = snapshot.get("result_text")
155
+ if result_text:
156
+ console.print(Panel(escape(result_text), title="Result", box=box.SIMPLE))
157
+ return True
158
+
159
+ if subcmd in ("cancel", "kill", "stop"):
160
+ if len(tokens) < 2:
161
+ console.print("[red]Usage: /agents cancel <id>[/red]")
162
+ return True
163
+ run_id = tokens[1]
164
+ runner = getattr(ui, "run_async", None)
165
+ try:
166
+ if callable(runner):
167
+ cancelled = runner(cancel_agent_run(run_id))
168
+ else:
169
+ import asyncio
170
+
171
+ cancelled = asyncio.run(cancel_agent_run(run_id))
172
+ except (OSError, RuntimeError, ValueError) as exc:
173
+ console.print(f"[red]Failed to cancel '{escape(run_id)}': {escape(str(exc))}[/red]")
174
+ return True
175
+ if cancelled:
176
+ console.print(f"[green]Cancelled subagent {escape(run_id)}[/green]")
177
+ else:
178
+ console.print(f"[yellow]No running subagent found for '{escape(run_id)}'.[/yellow]")
179
+ return True
180
+
54
181
  if subcmd in ("create", "add"):
55
182
  agent_name = tokens[1] if len(tokens) > 1 else console.input("Agent name: ").strip()
56
183
  if not agent_name:
@@ -84,7 +211,7 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
84
211
 
85
212
  config = get_global_config()
86
213
  pointer_map = config.model_pointers.model_dump()
87
- default_model_value = model_arg or pointer_map.get("task", "task")
214
+ default_model_value = model_arg or pointer_map.get("main", "main")
88
215
  model_input = (
89
216
  console.input(f"Model profile or pointer [{default_model_value}]: ").strip()
90
217
  or default_model_value
@@ -205,7 +332,7 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
205
332
 
206
333
  config = get_global_config()
207
334
  pointer_map = config.model_pointers.model_dump()
208
- model_default = target_agent.model or pointer_map.get("task", "task")
335
+ model_default = target_agent.model or pointer_map.get("main", "main")
209
336
  model_input = (
210
337
  console.input(f"Model profile or pointer [{model_default}]: ").strip() or model_default
211
338
  )
@@ -245,7 +372,7 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
245
372
  console.print(f" • {escape(agent.agent_type)} ({escape(str(location))})", markup=False)
246
373
  console.print(f" {escape(agent.when_to_use)}", markup=False)
247
374
  console.print(f" tools: {escape(tools_str)}", markup=False)
248
- console.print(f" model: {escape(agent.model or 'task (default)')}", markup=False)
375
+ console.print(f" model: {escape(agent.model or 'main (default)')}", markup=False)
249
376
  if agents.failed_files:
250
377
  console.print("[yellow]Some agent files could not be loaded:[/yellow]")
251
378
  for path, error in agents.failed_files:
@@ -3,8 +3,16 @@ from .base import SlashCommand
3
3
 
4
4
 
5
5
  def _handle(ui: Any, _: str) -> bool:
6
+ try:
7
+ ui._run_session_end("clear")
8
+ except (AttributeError, RuntimeError, ValueError):
9
+ pass
6
10
  ui.conversation_messages = []
7
11
  ui.console.print("[green]✓ Conversation cleared[/green]")
12
+ try:
13
+ ui._run_session_start("clear")
14
+ except (AttributeError, RuntimeError, ValueError):
15
+ pass
8
16
  return True
9
17
 
10
18
 
@@ -4,6 +4,7 @@ from .base import SlashCommand
4
4
 
5
5
  def _handle(ui: Any, _: str) -> bool:
6
6
  ui.console.print("[yellow]Goodbye![/yellow]")
7
+ ui._exit_reason = "prompt_input_exit"
7
8
  ui._should_exit = True
8
9
  return True
9
10
 
@@ -36,7 +36,7 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
36
36
  console.print("[bold]/models delete <name>[/bold] — delete a model profile")
37
37
  console.print("[bold]/models use <name>[/bold] — set the main model pointer")
38
38
  console.print(
39
- "[bold]/models use <pointer> <name>[/bold] — set a specific pointer (main/task/reasoning/quick)"
39
+ "[bold]/models use <pointer> <name>[/bold] — set a specific pointer (main/quick)"
40
40
  )
41
41
 
42
42
  def parse_int(prompt_text: str, default_value: Optional[int]) -> Optional[int]:
@@ -331,7 +331,7 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
331
331
 
332
332
  if subcmd in ("use", "main", "set-main"):
333
333
  # Support both "/models use <profile>" and "/models use <pointer> <profile>"
334
- valid_pointers = {"main", "task", "reasoning", "quick"}
334
+ valid_pointers = {"main", "quick"}
335
335
 
336
336
  if len(tokens) >= 3:
337
337
  # /models use <pointer> <profile>
@@ -354,7 +354,7 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
354
354
  target = tokens[1]
355
355
  else:
356
356
  pointer = (
357
- console.input("Pointer (main/task/reasoning/quick) [main]: ").strip().lower()
357
+ console.input("Pointer (main/quick) [main]: ").strip().lower()
358
358
  or "main"
359
359
  )
360
360
  if pointer not in valid_pointers:
@@ -116,6 +116,10 @@ def _handle(ui: Any, arg: str) -> bool:
116
116
  ui.conversation_messages = messages
117
117
  ui._saved_conversation = None
118
118
  ui._set_session(summary.session_id)
119
+ try:
120
+ ui._run_session_start("resume")
121
+ except (AttributeError, RuntimeError, ValueError):
122
+ pass
119
123
  ui.replay_conversation(messages)
120
124
  ui.console.print(
121
125
  f"[green]✓ Resumed session {escape(summary.session_id)} "