ripperdoc 0.2.9__py3-none-any.whl → 0.3.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.
- ripperdoc/__init__.py +1 -1
- ripperdoc/cli/cli.py +379 -51
- ripperdoc/cli/commands/__init__.py +6 -0
- ripperdoc/cli/commands/agents_cmd.py +128 -5
- ripperdoc/cli/commands/clear_cmd.py +8 -0
- ripperdoc/cli/commands/doctor_cmd.py +29 -0
- ripperdoc/cli/commands/exit_cmd.py +1 -0
- ripperdoc/cli/commands/memory_cmd.py +2 -1
- ripperdoc/cli/commands/models_cmd.py +63 -7
- ripperdoc/cli/commands/resume_cmd.py +5 -0
- ripperdoc/cli/commands/skills_cmd.py +103 -0
- ripperdoc/cli/commands/stats_cmd.py +244 -0
- ripperdoc/cli/commands/status_cmd.py +10 -0
- ripperdoc/cli/commands/tasks_cmd.py +6 -3
- ripperdoc/cli/commands/themes_cmd.py +139 -0
- ripperdoc/cli/ui/file_mention_completer.py +63 -13
- ripperdoc/cli/ui/helpers.py +6 -3
- ripperdoc/cli/ui/interrupt_handler.py +34 -0
- ripperdoc/cli/ui/panels.py +14 -8
- ripperdoc/cli/ui/rich_ui.py +737 -47
- ripperdoc/cli/ui/spinner.py +93 -18
- ripperdoc/cli/ui/thinking_spinner.py +1 -2
- ripperdoc/cli/ui/tool_renderers.py +10 -9
- ripperdoc/cli/ui/wizard.py +24 -19
- ripperdoc/core/agents.py +14 -3
- ripperdoc/core/config.py +238 -6
- ripperdoc/core/default_tools.py +91 -10
- ripperdoc/core/hooks/events.py +4 -0
- ripperdoc/core/hooks/llm_callback.py +58 -0
- ripperdoc/core/hooks/manager.py +6 -0
- ripperdoc/core/permissions.py +160 -9
- ripperdoc/core/providers/openai.py +84 -28
- ripperdoc/core/query.py +489 -87
- ripperdoc/core/query_utils.py +17 -14
- ripperdoc/core/skills.py +1 -0
- ripperdoc/core/theme.py +298 -0
- ripperdoc/core/tool.py +15 -5
- ripperdoc/protocol/__init__.py +14 -0
- ripperdoc/protocol/models.py +300 -0
- ripperdoc/protocol/stdio.py +1453 -0
- ripperdoc/tools/background_shell.py +354 -139
- ripperdoc/tools/bash_tool.py +117 -22
- ripperdoc/tools/file_edit_tool.py +228 -50
- ripperdoc/tools/file_read_tool.py +154 -3
- ripperdoc/tools/file_write_tool.py +53 -11
- ripperdoc/tools/grep_tool.py +98 -8
- ripperdoc/tools/lsp_tool.py +609 -0
- ripperdoc/tools/multi_edit_tool.py +26 -3
- ripperdoc/tools/skill_tool.py +52 -1
- ripperdoc/tools/task_tool.py +539 -65
- ripperdoc/utils/conversation_compaction.py +1 -1
- ripperdoc/utils/file_watch.py +216 -7
- ripperdoc/utils/image_utils.py +125 -0
- ripperdoc/utils/log.py +30 -3
- ripperdoc/utils/lsp.py +812 -0
- ripperdoc/utils/mcp.py +80 -18
- ripperdoc/utils/message_formatting.py +7 -4
- ripperdoc/utils/messages.py +198 -33
- ripperdoc/utils/pending_messages.py +50 -0
- ripperdoc/utils/permissions/shell_command_validation.py +3 -3
- ripperdoc/utils/permissions/tool_permission_utils.py +180 -15
- ripperdoc/utils/platform.py +198 -0
- ripperdoc/utils/session_heatmap.py +242 -0
- ripperdoc/utils/session_history.py +2 -2
- ripperdoc/utils/session_stats.py +294 -0
- ripperdoc/utils/shell_utils.py +8 -5
- ripperdoc/utils/todo.py +0 -6
- {ripperdoc-0.2.9.dist-info → ripperdoc-0.3.0.dist-info}/METADATA +55 -17
- ripperdoc-0.3.0.dist-info/RECORD +136 -0
- {ripperdoc-0.2.9.dist-info → ripperdoc-0.3.0.dist-info}/WHEEL +1 -1
- ripperdoc/sdk/__init__.py +0 -9
- ripperdoc/sdk/client.py +0 -333
- ripperdoc-0.2.9.dist-info/RECORD +0 -123
- {ripperdoc-0.2.9.dist-info → ripperdoc-0.3.0.dist-info}/entry_points.txt +0 -0
- {ripperdoc-0.2.9.dist-info → ripperdoc-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {ripperdoc-0.2.9.dist-info → ripperdoc-0.3.0.dist-info}/top_level.txt +0 -0
|
@@ -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
|
|
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,114 @@ 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 (
|
|
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 = result_text if len(result_text) <= 80 else result_text[:77] + "..."
|
|
105
|
+
table.add_row(
|
|
106
|
+
escape(run_id),
|
|
107
|
+
escape(snapshot.get("status") or "unknown"),
|
|
108
|
+
escape(snapshot.get("agent_type") or "unknown"),
|
|
109
|
+
_format_duration(snapshot.get("duration_ms")),
|
|
110
|
+
"yes" if snapshot.get("is_background") else "no",
|
|
111
|
+
escape(result_preview),
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
console.print(
|
|
115
|
+
Panel(table, title="Subagent runs", box=box.ROUNDED, padding=(1, 2)),
|
|
116
|
+
markup=False,
|
|
117
|
+
)
|
|
118
|
+
console.print(
|
|
119
|
+
"[dim]Use /agents show <id> for details or /agents cancel <id> to stop a background run.[/dim]"
|
|
120
|
+
)
|
|
121
|
+
return True
|
|
122
|
+
|
|
123
|
+
if subcmd in ("show", "info", "details"):
|
|
124
|
+
if len(tokens) < 2:
|
|
125
|
+
console.print("[red]Usage: /agents show <id>[/red]")
|
|
126
|
+
return True
|
|
127
|
+
run_id = tokens[1]
|
|
128
|
+
snapshot = get_agent_run_snapshot(run_id) # type: ignore[assignment]
|
|
129
|
+
if not snapshot:
|
|
130
|
+
console.print(f"[red]No subagent run found with id '{escape(run_id)}'.[/red]")
|
|
131
|
+
return True
|
|
132
|
+
details = Table(box=box.SIMPLE_HEAVY, show_header=False)
|
|
133
|
+
details.add_row("ID", escape(run_id))
|
|
134
|
+
details.add_row("Status", escape(snapshot.get("status") or "unknown"))
|
|
135
|
+
details.add_row("Agent", escape(snapshot.get("agent_type") or "unknown"))
|
|
136
|
+
details.add_row("Duration", _format_duration(snapshot.get("duration_ms")))
|
|
137
|
+
details.add_row("Background", "yes" if snapshot.get("is_background") else "no")
|
|
138
|
+
if snapshot.get("model_used"):
|
|
139
|
+
details.add_row("Model", escape(str(snapshot.get("model_used"))))
|
|
140
|
+
if snapshot.get("tool_use_count"):
|
|
141
|
+
details.add_row("Tool uses", str(snapshot.get("tool_use_count")))
|
|
142
|
+
if snapshot.get("missing_tools"):
|
|
143
|
+
details.add_row("Missing tools", escape(", ".join(snapshot["missing_tools"])))
|
|
144
|
+
if snapshot.get("error"):
|
|
145
|
+
details.add_row("Error", escape(str(snapshot.get("error"))))
|
|
146
|
+
console.print(
|
|
147
|
+
Panel(details, title=f"Subagent {escape(run_id)}", box=box.ROUNDED, padding=(1, 2)),
|
|
148
|
+
markup=False,
|
|
149
|
+
)
|
|
150
|
+
result_text = snapshot.get("result_text")
|
|
151
|
+
if result_text:
|
|
152
|
+
console.print(Panel(escape(result_text), title="Result", box=box.SIMPLE))
|
|
153
|
+
return True
|
|
154
|
+
|
|
155
|
+
if subcmd in ("cancel", "kill", "stop"):
|
|
156
|
+
if len(tokens) < 2:
|
|
157
|
+
console.print("[red]Usage: /agents cancel <id>[/red]")
|
|
158
|
+
return True
|
|
159
|
+
run_id = tokens[1]
|
|
160
|
+
runner = getattr(ui, "run_async", None)
|
|
161
|
+
try:
|
|
162
|
+
if callable(runner):
|
|
163
|
+
cancelled = runner(cancel_agent_run(run_id))
|
|
164
|
+
else:
|
|
165
|
+
import asyncio
|
|
166
|
+
|
|
167
|
+
cancelled = asyncio.run(cancel_agent_run(run_id))
|
|
168
|
+
except (OSError, RuntimeError, ValueError) as exc:
|
|
169
|
+
console.print(f"[red]Failed to cancel '{escape(run_id)}': {escape(str(exc))}[/red]")
|
|
170
|
+
return True
|
|
171
|
+
if cancelled:
|
|
172
|
+
console.print(f"[green]Cancelled subagent {escape(run_id)}[/green]")
|
|
173
|
+
else:
|
|
174
|
+
console.print(f"[yellow]No running subagent found for '{escape(run_id)}'.[/yellow]")
|
|
175
|
+
return True
|
|
176
|
+
|
|
54
177
|
if subcmd in ("create", "add"):
|
|
55
178
|
agent_name = tokens[1] if len(tokens) > 1 else console.input("Agent name: ").strip()
|
|
56
179
|
if not agent_name:
|
|
@@ -84,7 +207,7 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
|
|
|
84
207
|
|
|
85
208
|
config = get_global_config()
|
|
86
209
|
pointer_map = config.model_pointers.model_dump()
|
|
87
|
-
default_model_value = model_arg or pointer_map.get("
|
|
210
|
+
default_model_value = model_arg or pointer_map.get("main", "main")
|
|
88
211
|
model_input = (
|
|
89
212
|
console.input(f"Model profile or pointer [{default_model_value}]: ").strip()
|
|
90
213
|
or default_model_value
|
|
@@ -205,7 +328,7 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
|
|
|
205
328
|
|
|
206
329
|
config = get_global_config()
|
|
207
330
|
pointer_map = config.model_pointers.model_dump()
|
|
208
|
-
model_default = target_agent.model or pointer_map.get("
|
|
331
|
+
model_default = target_agent.model or pointer_map.get("main", "main")
|
|
209
332
|
model_input = (
|
|
210
333
|
console.input(f"Model profile or pointer [{model_default}]: ").strip() or model_default
|
|
211
334
|
)
|
|
@@ -245,7 +368,7 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
|
|
|
245
368
|
console.print(f" • {escape(agent.agent_type)} ({escape(str(location))})", markup=False)
|
|
246
369
|
console.print(f" {escape(agent.when_to_use)}", markup=False)
|
|
247
370
|
console.print(f" tools: {escape(tools_str)}", markup=False)
|
|
248
|
-
console.print(f" model: {escape(agent.model or '
|
|
371
|
+
console.print(f" model: {escape(agent.model or 'main (default)')}", markup=False)
|
|
249
372
|
if agents.failed_files:
|
|
250
373
|
console.print("[yellow]Some agent files could not be loaded:[/yellow]")
|
|
251
374
|
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
|
|
|
@@ -15,6 +15,8 @@ from ripperdoc.core.config import (
|
|
|
15
15
|
api_key_env_candidates,
|
|
16
16
|
get_global_config,
|
|
17
17
|
get_project_config,
|
|
18
|
+
get_ripperdoc_env_status,
|
|
19
|
+
has_ripperdoc_env_overrides,
|
|
18
20
|
)
|
|
19
21
|
from ripperdoc.cli.ui.helpers import get_profile_for_pointer
|
|
20
22
|
from ripperdoc.utils.log import get_logger
|
|
@@ -41,6 +43,12 @@ def _api_key_status(provider: ProviderType, profile_key: Optional[str]) -> Tuple
|
|
|
41
43
|
"""Check API key presence and source."""
|
|
42
44
|
import os
|
|
43
45
|
|
|
46
|
+
# 首先检查全局 RIPPERDOC_API_KEY
|
|
47
|
+
if ripperdoc_api_key := os.getenv("RIPPERDOC_API_KEY"):
|
|
48
|
+
masked = ripperdoc_api_key[:4] + "…" if len(ripperdoc_api_key) > 4 else "set"
|
|
49
|
+
return ("ok", f"Found in $RIPPERDOC_API_KEY ({masked})")
|
|
50
|
+
|
|
51
|
+
# 然后检查 provider 特定的环境变量
|
|
44
52
|
for env_var in api_key_env_candidates(provider):
|
|
45
53
|
if os.environ.get(env_var):
|
|
46
54
|
masked = os.environ[env_var]
|
|
@@ -186,6 +194,23 @@ def _project_status(project_path: Path) -> Tuple[str, str, str]:
|
|
|
186
194
|
)
|
|
187
195
|
|
|
188
196
|
|
|
197
|
+
def _ripperdoc_env_status() -> List[Tuple[str, str, str]]:
|
|
198
|
+
"""Check RIPPERDOC_* environment variable overrides."""
|
|
199
|
+
rows: List[Tuple[str, str, str]] = []
|
|
200
|
+
|
|
201
|
+
if not has_ripperdoc_env_overrides():
|
|
202
|
+
rows.append(_status_row("Env overrides", "ok", "No RIPPERDOC_* overrides active"))
|
|
203
|
+
return rows
|
|
204
|
+
|
|
205
|
+
rows.append(_status_row("Env overrides", "ok", "RIPPERDOC_* variables detected"))
|
|
206
|
+
|
|
207
|
+
status = get_ripperdoc_env_status()
|
|
208
|
+
for key, value in status.items():
|
|
209
|
+
rows.append(_status_row("", "ok", f" {key}: {value}"))
|
|
210
|
+
|
|
211
|
+
return rows
|
|
212
|
+
|
|
213
|
+
|
|
189
214
|
def _render_table(console: Any, rows: List[Tuple[str, str, str]]) -> None:
|
|
190
215
|
table = Table(show_header=True, header_style="bold cyan")
|
|
191
216
|
table.add_column("Check")
|
|
@@ -202,6 +227,10 @@ def _handle(ui: Any, _: str) -> bool:
|
|
|
202
227
|
|
|
203
228
|
results.append(_onboarding_status())
|
|
204
229
|
results.extend(_model_status(project_path))
|
|
230
|
+
|
|
231
|
+
# 检查 RIPPERDOC_* 环境变量
|
|
232
|
+
results.extend(_ripperdoc_env_status())
|
|
233
|
+
|
|
205
234
|
project_row = _project_status(project_path)
|
|
206
235
|
results.append(project_row)
|
|
207
236
|
|
|
@@ -18,6 +18,7 @@ from ripperdoc.utils.memory import (
|
|
|
18
18
|
MEMORY_FILE_NAME,
|
|
19
19
|
collect_all_memory_files,
|
|
20
20
|
)
|
|
21
|
+
from ripperdoc.utils.platform import is_windows
|
|
21
22
|
|
|
22
23
|
from .base import SlashCommand
|
|
23
24
|
|
|
@@ -89,7 +90,7 @@ def _determine_editor_command() -> Optional[List[str]]:
|
|
|
89
90
|
return shlex.split(value)
|
|
90
91
|
|
|
91
92
|
candidates = ["code", "nano", "vim", "vi"]
|
|
92
|
-
if
|
|
93
|
+
if is_windows():
|
|
93
94
|
candidates.insert(0, "notepad")
|
|
94
95
|
|
|
95
96
|
for candidate in candidates:
|
|
@@ -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/
|
|
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]:
|
|
@@ -155,12 +155,34 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
|
|
|
155
155
|
context_prompt += "): "
|
|
156
156
|
context_window = parse_int(context_prompt, context_window_default)
|
|
157
157
|
|
|
158
|
+
# Vision support prompt
|
|
159
|
+
supports_vision_default = existing_profile.supports_vision if existing_profile else None
|
|
160
|
+
supports_vision = None
|
|
161
|
+
vision_default_display = (
|
|
162
|
+
"auto"
|
|
163
|
+
if supports_vision_default is None
|
|
164
|
+
else ("yes" if supports_vision_default else "no")
|
|
165
|
+
)
|
|
166
|
+
supports_vision_input = (
|
|
167
|
+
console.input(f"Supports vision (images)? [{vision_default_display}] (Y/n/auto): ")
|
|
168
|
+
.strip()
|
|
169
|
+
.lower()
|
|
170
|
+
)
|
|
171
|
+
if supports_vision_input in ("y", "yes"):
|
|
172
|
+
supports_vision = True
|
|
173
|
+
elif supports_vision_input in ("n", "no"):
|
|
174
|
+
supports_vision = False
|
|
175
|
+
elif supports_vision_input in ("auto", ""):
|
|
176
|
+
supports_vision = None
|
|
177
|
+
else:
|
|
178
|
+
supports_vision = supports_vision_default
|
|
179
|
+
|
|
158
180
|
default_set_main = (
|
|
159
181
|
not config.model_profiles
|
|
160
182
|
or getattr(config.model_pointers, "main", "") not in config.model_profiles
|
|
161
183
|
)
|
|
162
184
|
set_main_input = (
|
|
163
|
-
console.input(f"Set as main model?
|
|
185
|
+
console.input(f"Set as main model? ({'Y' if default_set_main else 'y'}/N): ")
|
|
164
186
|
.strip()
|
|
165
187
|
.lower()
|
|
166
188
|
)
|
|
@@ -175,6 +197,7 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
|
|
|
175
197
|
temperature=temperature,
|
|
176
198
|
context_window=context_window,
|
|
177
199
|
auth_token=auth_token,
|
|
200
|
+
supports_vision=supports_vision,
|
|
178
201
|
)
|
|
179
202
|
|
|
180
203
|
try:
|
|
@@ -276,6 +299,31 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
|
|
|
276
299
|
existing_profile.context_window,
|
|
277
300
|
)
|
|
278
301
|
|
|
302
|
+
# Vision support prompt
|
|
303
|
+
vision_default_display = (
|
|
304
|
+
"auto"
|
|
305
|
+
if existing_profile.supports_vision is None
|
|
306
|
+
else ("yes" if existing_profile.supports_vision else "no")
|
|
307
|
+
)
|
|
308
|
+
supports_vision_input = (
|
|
309
|
+
console.input(
|
|
310
|
+
f"Supports vision (images)? [{vision_default_display}] (Y/n/auto/C=clear): "
|
|
311
|
+
)
|
|
312
|
+
.strip()
|
|
313
|
+
.lower()
|
|
314
|
+
)
|
|
315
|
+
supports_vision = None
|
|
316
|
+
if supports_vision_input in ("y", "yes"):
|
|
317
|
+
supports_vision = True
|
|
318
|
+
elif supports_vision_input in ("n", "no"):
|
|
319
|
+
supports_vision = False
|
|
320
|
+
elif supports_vision_input in ("c", "clear", "-"):
|
|
321
|
+
supports_vision = None
|
|
322
|
+
elif supports_vision_input in ("auto", ""):
|
|
323
|
+
supports_vision = existing_profile.supports_vision
|
|
324
|
+
else:
|
|
325
|
+
supports_vision = existing_profile.supports_vision
|
|
326
|
+
|
|
279
327
|
updated_profile = ModelProfile(
|
|
280
328
|
provider=provider,
|
|
281
329
|
model=model_name,
|
|
@@ -285,6 +333,7 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
|
|
|
285
333
|
temperature=temperature,
|
|
286
334
|
context_window=context_window,
|
|
287
335
|
auth_token=auth_token,
|
|
336
|
+
supports_vision=supports_vision,
|
|
288
337
|
)
|
|
289
338
|
|
|
290
339
|
try:
|
|
@@ -331,7 +380,7 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
|
|
|
331
380
|
|
|
332
381
|
if subcmd in ("use", "main", "set-main"):
|
|
333
382
|
# Support both "/models use <profile>" and "/models use <pointer> <profile>"
|
|
334
|
-
valid_pointers = {"main", "
|
|
383
|
+
valid_pointers = {"main", "quick"}
|
|
335
384
|
|
|
336
385
|
if len(tokens) >= 3:
|
|
337
386
|
# /models use <pointer> <profile>
|
|
@@ -353,10 +402,7 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
|
|
|
353
402
|
pointer = "main"
|
|
354
403
|
target = tokens[1]
|
|
355
404
|
else:
|
|
356
|
-
pointer = (
|
|
357
|
-
console.input("Pointer (main/task/reasoning/quick) [main]: ").strip().lower()
|
|
358
|
-
or "main"
|
|
359
|
-
)
|
|
405
|
+
pointer = console.input("Pointer (main/quick) [main]: ").strip().lower() or "main"
|
|
360
406
|
if pointer not in valid_pointers:
|
|
361
407
|
console.print(
|
|
362
408
|
f"[red]Invalid pointer '{escape(pointer)}'. Valid pointers: {', '.join(valid_pointers)}[/red]"
|
|
@@ -415,6 +461,16 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
|
|
|
415
461
|
)
|
|
416
462
|
if profile.openai_tool_mode:
|
|
417
463
|
console.print(f" openai_tool_mode: {profile.openai_tool_mode}", markup=False)
|
|
464
|
+
if profile.thinking_mode:
|
|
465
|
+
console.print(f" thinking_mode: {profile.thinking_mode}", markup=False)
|
|
466
|
+
# Display vision support
|
|
467
|
+
if profile.supports_vision is None:
|
|
468
|
+
vision_display = "auto-detect"
|
|
469
|
+
elif profile.supports_vision:
|
|
470
|
+
vision_display = "yes"
|
|
471
|
+
else:
|
|
472
|
+
vision_display = "no"
|
|
473
|
+
console.print(f" supports_vision: {vision_display}", markup=False)
|
|
418
474
|
pointer_labels = ", ".join(f"{p}->{v or '-'}" for p, v in pointer_map.items())
|
|
419
475
|
console.print(f"[dim]Pointers: {escape(pointer_labels)}[/dim]")
|
|
420
476
|
return True
|
|
@@ -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)} "
|
|
@@ -128,6 +132,7 @@ command = SlashCommand(
|
|
|
128
132
|
name="resume",
|
|
129
133
|
description="Resume a previous session conversation",
|
|
130
134
|
handler=_handle,
|
|
135
|
+
aliases=("sessions",),
|
|
131
136
|
)
|
|
132
137
|
|
|
133
138
|
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
from rich.markup import escape
|
|
2
|
+
|
|
3
|
+
from ripperdoc.core.skills import (
|
|
4
|
+
SkillDefinition,
|
|
5
|
+
SkillLoadResult,
|
|
6
|
+
SkillLocation,
|
|
7
|
+
load_all_skills,
|
|
8
|
+
skill_directories,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
from typing import Any
|
|
12
|
+
from .base import SlashCommand
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _handle(ui: Any, _: str) -> bool:
|
|
16
|
+
console = ui.console
|
|
17
|
+
project_path = getattr(ui, "project_path", None)
|
|
18
|
+
|
|
19
|
+
result: SkillLoadResult = load_all_skills(project_path=project_path)
|
|
20
|
+
|
|
21
|
+
if not result.skills:
|
|
22
|
+
dirs = skill_directories(project_path=project_path)
|
|
23
|
+
dir_paths = [f"'{d}'" for d, _ in dirs]
|
|
24
|
+
console.print("[yellow]No skills found.[/yellow]")
|
|
25
|
+
console.print(
|
|
26
|
+
f"\n[bold]Create skills in:[/bold]\n"
|
|
27
|
+
f" • User: {dir_paths[0]}\n"
|
|
28
|
+
f" • Project: {dir_paths[1]}\n"
|
|
29
|
+
f"\n[dim]Each skill needs a SKILL.md file with YAML frontmatter:\n"
|
|
30
|
+
f"---\n"
|
|
31
|
+
f"name: my-skill\n"
|
|
32
|
+
f"description: A helpful skill\n"
|
|
33
|
+
f"---\n\n"
|
|
34
|
+
f"Skill content goes here...[/dim]"
|
|
35
|
+
)
|
|
36
|
+
if result.errors:
|
|
37
|
+
console.print("\n[bold red]Errors encountered while loading skills:[/bold red]")
|
|
38
|
+
for error in result.errors:
|
|
39
|
+
console.print(f" • {error.path}: {escape(error.reason)}", markup=False)
|
|
40
|
+
return True
|
|
41
|
+
|
|
42
|
+
console.print("\n[bold]Skills[/bold]")
|
|
43
|
+
|
|
44
|
+
# Group skills by location for better organization
|
|
45
|
+
user_skills = [s for s in result.skills if s.location == SkillLocation.USER]
|
|
46
|
+
project_skills = [s for s in result.skills if s.location == SkillLocation.PROJECT]
|
|
47
|
+
other_skills = [s for s in result.skills if s.location == SkillLocation.OTHER]
|
|
48
|
+
|
|
49
|
+
def print_skill(skill: SkillDefinition) -> None:
|
|
50
|
+
location_tag = f"[dim]({skill.location.value})[/dim]" if skill.location else ""
|
|
51
|
+
console.print(f"\n[bold cyan]{escape(skill.name)}[/bold cyan] {location_tag}")
|
|
52
|
+
console.print(f" {escape(skill.description)}")
|
|
53
|
+
|
|
54
|
+
if skill.allowed_tools:
|
|
55
|
+
tools_str = ", ".join(escape(t) for t in skill.allowed_tools)
|
|
56
|
+
console.print(f" Tools: {tools_str}")
|
|
57
|
+
|
|
58
|
+
if skill.model:
|
|
59
|
+
console.print(f" Model: {escape(skill.model)}")
|
|
60
|
+
|
|
61
|
+
if skill.max_thinking_tokens:
|
|
62
|
+
console.print(f" Max thinking tokens: {skill.max_thinking_tokens}")
|
|
63
|
+
|
|
64
|
+
if skill.skill_type != "prompt":
|
|
65
|
+
console.print(f" Type: {escape(skill.skill_type)}")
|
|
66
|
+
|
|
67
|
+
if skill.disable_model_invocation:
|
|
68
|
+
console.print(" [yellow]Model invocation disabled[/yellow]")
|
|
69
|
+
|
|
70
|
+
console.print(f" Path: {escape(str(skill.path))}", markup=False)
|
|
71
|
+
|
|
72
|
+
# Print project skills first (they have priority), then user skills
|
|
73
|
+
if project_skills:
|
|
74
|
+
console.print("\n[bold]Project skills:[/bold]")
|
|
75
|
+
for skill in project_skills:
|
|
76
|
+
print_skill(skill)
|
|
77
|
+
|
|
78
|
+
if user_skills:
|
|
79
|
+
console.print("\n[bold]User skills:[/bold]")
|
|
80
|
+
for skill in user_skills:
|
|
81
|
+
print_skill(skill)
|
|
82
|
+
|
|
83
|
+
if other_skills:
|
|
84
|
+
console.print("\n[bold]Other skills:[/bold]")
|
|
85
|
+
for skill in other_skills:
|
|
86
|
+
print_skill(skill)
|
|
87
|
+
|
|
88
|
+
if result.errors:
|
|
89
|
+
console.print("\n[bold red]Errors encountered while loading skills:[/bold red]")
|
|
90
|
+
for error in result.errors:
|
|
91
|
+
console.print(f" • {error.path}: {escape(error.reason)}", markup=False)
|
|
92
|
+
|
|
93
|
+
return True
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
command = SlashCommand(
|
|
97
|
+
name="skills",
|
|
98
|
+
description="List available skills",
|
|
99
|
+
handler=_handle,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
__all__ = ["command"]
|