ripperdoc 0.1.0__py3-none-any.whl → 0.2.2__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 +75 -15
- ripperdoc/cli/commands/__init__.py +4 -0
- ripperdoc/cli/commands/agents_cmd.py +23 -1
- ripperdoc/cli/commands/context_cmd.py +13 -3
- ripperdoc/cli/commands/cost_cmd.py +1 -1
- ripperdoc/cli/commands/doctor_cmd.py +200 -0
- ripperdoc/cli/commands/memory_cmd.py +209 -0
- ripperdoc/cli/commands/models_cmd.py +25 -0
- ripperdoc/cli/commands/resume_cmd.py +3 -3
- ripperdoc/cli/commands/status_cmd.py +5 -5
- ripperdoc/cli/commands/tasks_cmd.py +32 -5
- ripperdoc/cli/ui/context_display.py +4 -3
- ripperdoc/cli/ui/rich_ui.py +205 -43
- ripperdoc/cli/ui/spinner.py +3 -4
- ripperdoc/core/agents.py +10 -6
- ripperdoc/core/config.py +48 -3
- ripperdoc/core/default_tools.py +26 -6
- ripperdoc/core/permissions.py +19 -0
- ripperdoc/core/query.py +238 -302
- ripperdoc/core/query_utils.py +537 -0
- ripperdoc/core/system_prompt.py +2 -1
- ripperdoc/core/tool.py +14 -1
- ripperdoc/sdk/client.py +1 -1
- ripperdoc/tools/background_shell.py +9 -3
- ripperdoc/tools/bash_tool.py +19 -4
- ripperdoc/tools/file_edit_tool.py +9 -2
- ripperdoc/tools/file_read_tool.py +9 -2
- ripperdoc/tools/file_write_tool.py +15 -2
- ripperdoc/tools/glob_tool.py +57 -17
- ripperdoc/tools/grep_tool.py +9 -2
- ripperdoc/tools/ls_tool.py +244 -75
- ripperdoc/tools/mcp_tools.py +47 -19
- ripperdoc/tools/multi_edit_tool.py +13 -2
- ripperdoc/tools/notebook_edit_tool.py +9 -6
- ripperdoc/tools/task_tool.py +20 -5
- ripperdoc/tools/todo_tool.py +163 -29
- ripperdoc/tools/tool_search_tool.py +15 -4
- ripperdoc/utils/git_utils.py +276 -0
- ripperdoc/utils/json_utils.py +28 -0
- ripperdoc/utils/log.py +130 -29
- ripperdoc/utils/mcp.py +83 -10
- ripperdoc/utils/memory.py +14 -1
- ripperdoc/utils/message_compaction.py +51 -14
- ripperdoc/utils/messages.py +63 -4
- ripperdoc/utils/output_utils.py +36 -9
- ripperdoc/utils/permissions/path_validation_utils.py +6 -0
- ripperdoc/utils/safe_get_cwd.py +4 -0
- ripperdoc/utils/session_history.py +27 -9
- ripperdoc/utils/todo.py +2 -2
- {ripperdoc-0.1.0.dist-info → ripperdoc-0.2.2.dist-info}/METADATA +4 -2
- ripperdoc-0.2.2.dist-info/RECORD +86 -0
- ripperdoc-0.1.0.dist-info/RECORD +0 -81
- {ripperdoc-0.1.0.dist-info → ripperdoc-0.2.2.dist-info}/WHEEL +0 -0
- {ripperdoc-0.1.0.dist-info → ripperdoc-0.2.2.dist-info}/entry_points.txt +0 -0
- {ripperdoc-0.1.0.dist-info → ripperdoc-0.2.2.dist-info}/licenses/LICENSE +0 -0
- {ripperdoc-0.1.0.dist-info → ripperdoc-0.2.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""Slash command to view and edit AGENTS memory files."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import shlex
|
|
7
|
+
import shutil
|
|
8
|
+
import subprocess
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, List, Optional
|
|
11
|
+
|
|
12
|
+
from rich.markup import escape
|
|
13
|
+
from rich.panel import Panel
|
|
14
|
+
from rich.table import Table
|
|
15
|
+
|
|
16
|
+
from ripperdoc.utils.memory import (
|
|
17
|
+
LOCAL_MEMORY_FILE_NAME,
|
|
18
|
+
MEMORY_FILE_NAME,
|
|
19
|
+
collect_all_memory_files,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
from .base import SlashCommand
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _shorten_path(path: Path, project_path: Path) -> str:
|
|
26
|
+
"""Return a short, user-friendly path."""
|
|
27
|
+
try:
|
|
28
|
+
return str(path.resolve().relative_to(project_path.resolve()))
|
|
29
|
+
except Exception:
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
home = Path.home()
|
|
33
|
+
try:
|
|
34
|
+
rel_home = path.resolve().relative_to(home)
|
|
35
|
+
return f"~/{rel_home}"
|
|
36
|
+
except Exception:
|
|
37
|
+
return str(path)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _preferred_user_memory_path() -> Path:
|
|
41
|
+
"""Pick the user-level memory path, preferring ~/.ripperdoc/AGENTS.md."""
|
|
42
|
+
home = Path.home()
|
|
43
|
+
preferred_dir = home / ".ripperdoc"
|
|
44
|
+
preferred_path = preferred_dir / MEMORY_FILE_NAME
|
|
45
|
+
fallback_path = home / MEMORY_FILE_NAME
|
|
46
|
+
|
|
47
|
+
if preferred_path.exists():
|
|
48
|
+
return preferred_path
|
|
49
|
+
if fallback_path.exists():
|
|
50
|
+
return fallback_path
|
|
51
|
+
|
|
52
|
+
preferred_dir.mkdir(parents=True, exist_ok=True)
|
|
53
|
+
return preferred_path
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _ensure_file(path: Path) -> bool:
|
|
57
|
+
"""Ensure the target file exists. Returns True if created."""
|
|
58
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
59
|
+
if path.exists():
|
|
60
|
+
return False
|
|
61
|
+
path.write_text("", encoding="utf-8")
|
|
62
|
+
return True
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _ensure_gitignore_entry(project_path: Path, entry: str) -> bool:
|
|
66
|
+
"""Ensure an entry exists in .gitignore. Returns True if added."""
|
|
67
|
+
gitignore_path = project_path / ".gitignore"
|
|
68
|
+
try:
|
|
69
|
+
text = ""
|
|
70
|
+
if gitignore_path.exists():
|
|
71
|
+
text = gitignore_path.read_text(encoding="utf-8", errors="ignore")
|
|
72
|
+
existing_lines = text.splitlines()
|
|
73
|
+
if entry in existing_lines:
|
|
74
|
+
return False
|
|
75
|
+
with gitignore_path.open("a", encoding="utf-8") as f:
|
|
76
|
+
if text and not text.endswith("\n"):
|
|
77
|
+
f.write("\n")
|
|
78
|
+
f.write(f"{entry}\n")
|
|
79
|
+
return True
|
|
80
|
+
except Exception:
|
|
81
|
+
return False
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _determine_editor_command() -> Optional[List[str]]:
|
|
85
|
+
"""Resolve the editor command from environment or common defaults."""
|
|
86
|
+
for env_var in ("VISUAL", "EDITOR"):
|
|
87
|
+
value = os.environ.get(env_var)
|
|
88
|
+
if value:
|
|
89
|
+
return shlex.split(value)
|
|
90
|
+
|
|
91
|
+
candidates = ["code", "nano", "vim", "vi"]
|
|
92
|
+
if os.name == "nt":
|
|
93
|
+
candidates.insert(0, "notepad")
|
|
94
|
+
|
|
95
|
+
for candidate in candidates:
|
|
96
|
+
if shutil.which(candidate):
|
|
97
|
+
return [candidate]
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _open_in_editor(path: Path, console: Any) -> bool:
|
|
102
|
+
"""Open the file in a text editor; returns True if an editor was launched."""
|
|
103
|
+
editor_cmd = _determine_editor_command()
|
|
104
|
+
if not editor_cmd:
|
|
105
|
+
console.print(
|
|
106
|
+
f"[yellow]No editor configured. Set $EDITOR or $VISUAL, or manually edit: {escape(str(path))}[/yellow]"
|
|
107
|
+
)
|
|
108
|
+
return False
|
|
109
|
+
|
|
110
|
+
cmd = [*editor_cmd, str(path)]
|
|
111
|
+
try:
|
|
112
|
+
console.print(f"[dim]Opening with: {' '.join(editor_cmd)}[/dim]")
|
|
113
|
+
subprocess.run(cmd, check=False)
|
|
114
|
+
return True
|
|
115
|
+
except FileNotFoundError:
|
|
116
|
+
console.print(f"[red]Editor command not found: {escape(editor_cmd[0])}[/red]")
|
|
117
|
+
return False
|
|
118
|
+
except Exception as exc: # pragma: no cover - best-effort logging
|
|
119
|
+
console.print(f"[red]Failed to launch editor: {escape(str(exc))}[/red]")
|
|
120
|
+
return False
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _render_memory_table(console: Any, project_path: Path) -> None:
|
|
124
|
+
files = collect_all_memory_files()
|
|
125
|
+
table = Table(title="Memory files", show_header=True, header_style="bold cyan")
|
|
126
|
+
table.add_column("Type", style="bold")
|
|
127
|
+
table.add_column("Location")
|
|
128
|
+
table.add_column("Nested", justify="center")
|
|
129
|
+
|
|
130
|
+
for memory_file in files:
|
|
131
|
+
display_path = _shorten_path(Path(memory_file.path), project_path)
|
|
132
|
+
nested = "yes" if getattr(memory_file, "is_nested", False) else ""
|
|
133
|
+
table.add_row(memory_file.type, escape(display_path), nested)
|
|
134
|
+
|
|
135
|
+
if files:
|
|
136
|
+
console.print(table)
|
|
137
|
+
else:
|
|
138
|
+
console.print("[yellow]No memory files found yet.[/yellow]")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _handle(ui: Any, trimmed_arg: str) -> bool:
|
|
142
|
+
project_path = getattr(ui, "project_path", Path.cwd())
|
|
143
|
+
scope = trimmed_arg.strip().lower()
|
|
144
|
+
|
|
145
|
+
if scope:
|
|
146
|
+
scope_aliases = {
|
|
147
|
+
"project": "project",
|
|
148
|
+
"workspace": "project",
|
|
149
|
+
"local": "local",
|
|
150
|
+
"private": "local",
|
|
151
|
+
"user": "user",
|
|
152
|
+
"global": "user",
|
|
153
|
+
}
|
|
154
|
+
if scope not in scope_aliases:
|
|
155
|
+
ui.console.print(
|
|
156
|
+
"[red]Unknown scope. Use one of: project, local, user.[/red]"
|
|
157
|
+
)
|
|
158
|
+
return True
|
|
159
|
+
|
|
160
|
+
resolved_scope = scope_aliases[scope]
|
|
161
|
+
if resolved_scope == "project":
|
|
162
|
+
target_path = project_path / MEMORY_FILE_NAME
|
|
163
|
+
heading = "Project memory (checked in)"
|
|
164
|
+
elif resolved_scope == "local":
|
|
165
|
+
target_path = project_path / LOCAL_MEMORY_FILE_NAME
|
|
166
|
+
heading = "Local memory (not checked in)"
|
|
167
|
+
else:
|
|
168
|
+
target_path = _preferred_user_memory_path()
|
|
169
|
+
heading = "User memory (home directory)"
|
|
170
|
+
|
|
171
|
+
created = _ensure_file(target_path)
|
|
172
|
+
gitignore_added = False
|
|
173
|
+
if resolved_scope == "local":
|
|
174
|
+
gitignore_added = _ensure_gitignore_entry(project_path, LOCAL_MEMORY_FILE_NAME)
|
|
175
|
+
|
|
176
|
+
_open_in_editor(target_path, ui.console)
|
|
177
|
+
|
|
178
|
+
messages: List[str] = [
|
|
179
|
+
f"{heading}: {escape(_shorten_path(target_path, project_path))}"
|
|
180
|
+
]
|
|
181
|
+
if created:
|
|
182
|
+
messages.append("Created new memory file.")
|
|
183
|
+
if gitignore_added:
|
|
184
|
+
messages.append("Added AGENTS.local.md to .gitignore.")
|
|
185
|
+
if not created:
|
|
186
|
+
messages.append("Opened existing memory file.")
|
|
187
|
+
|
|
188
|
+
ui.console.print(Panel("\n".join(messages), title="/memory"))
|
|
189
|
+
return True
|
|
190
|
+
|
|
191
|
+
_render_memory_table(ui.console, project_path)
|
|
192
|
+
ui.console.print(
|
|
193
|
+
"[dim]Usage: /memory project | /memory local | /memory user[/dim]"
|
|
194
|
+
)
|
|
195
|
+
ui.console.print(
|
|
196
|
+
"[dim]Project and user memories feed directly into the system prompt.[/dim]"
|
|
197
|
+
)
|
|
198
|
+
return True
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
command = SlashCommand(
|
|
202
|
+
name="memory",
|
|
203
|
+
description="List and edit AGENTS memory files",
|
|
204
|
+
handler=_handle,
|
|
205
|
+
aliases=("mem",),
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
__all__ = ["command"]
|
|
@@ -13,15 +13,22 @@ from ripperdoc.core.config import (
|
|
|
13
13
|
get_global_config,
|
|
14
14
|
set_model_pointer,
|
|
15
15
|
)
|
|
16
|
+
from ripperdoc.utils.log import get_logger
|
|
16
17
|
|
|
17
18
|
from .base import SlashCommand
|
|
18
19
|
|
|
20
|
+
logger = get_logger()
|
|
21
|
+
|
|
19
22
|
|
|
20
23
|
def _handle(ui: Any, trimmed_arg: str) -> bool:
|
|
21
24
|
console = ui.console
|
|
22
25
|
tokens = trimmed_arg.split()
|
|
23
26
|
subcmd = tokens[0].lower() if tokens else ""
|
|
24
27
|
config = get_global_config()
|
|
28
|
+
logger.info(
|
|
29
|
+
"[models_cmd] Handling /models command",
|
|
30
|
+
extra={"subcommand": subcmd or "list", "session_id": getattr(ui, "session_id", None)},
|
|
31
|
+
)
|
|
25
32
|
|
|
26
33
|
def print_models_usage() -> None:
|
|
27
34
|
console.print("[bold]/models[/bold] — list configured models")
|
|
@@ -167,6 +174,10 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
|
|
|
167
174
|
)
|
|
168
175
|
except Exception as exc:
|
|
169
176
|
console.print(f"[red]Failed to save model: {escape(str(exc))}[/red]")
|
|
177
|
+
logger.exception(
|
|
178
|
+
"[models_cmd] Failed to save model profile",
|
|
179
|
+
extra={"profile": profile_name, "session_id": getattr(ui, "session_id", None)},
|
|
180
|
+
)
|
|
170
181
|
return True
|
|
171
182
|
|
|
172
183
|
marker = " (main)" if set_as_main else ""
|
|
@@ -255,6 +266,10 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
|
|
|
255
266
|
)
|
|
256
267
|
except Exception as exc:
|
|
257
268
|
console.print(f"[red]Failed to update model: {escape(str(exc))}[/red]")
|
|
269
|
+
logger.exception(
|
|
270
|
+
"[models_cmd] Failed to update model profile",
|
|
271
|
+
extra={"profile": profile_name, "session_id": getattr(ui, "session_id", None)},
|
|
272
|
+
)
|
|
258
273
|
return True
|
|
259
274
|
|
|
260
275
|
console.print(f"[green]✓ Model '{escape(profile_name)}' updated[/green]")
|
|
@@ -274,6 +289,10 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
|
|
|
274
289
|
except Exception as exc:
|
|
275
290
|
console.print(f"[red]Failed to delete model: {escape(str(exc))}[/red]")
|
|
276
291
|
print_models_usage()
|
|
292
|
+
logger.exception(
|
|
293
|
+
"[models_cmd] Failed to delete model profile",
|
|
294
|
+
extra={"profile": target, "session_id": getattr(ui, "session_id", None)},
|
|
295
|
+
)
|
|
277
296
|
return True
|
|
278
297
|
|
|
279
298
|
if subcmd in ("use", "main", "set-main"):
|
|
@@ -288,6 +307,10 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
|
|
|
288
307
|
except Exception as exc:
|
|
289
308
|
console.print(f"[red]{escape(str(exc))}[/red]")
|
|
290
309
|
print_models_usage()
|
|
310
|
+
logger.exception(
|
|
311
|
+
"[models_cmd] Failed to set main model pointer",
|
|
312
|
+
extra={"profile": target, "session_id": getattr(ui, "session_id", None)},
|
|
313
|
+
)
|
|
291
314
|
return True
|
|
292
315
|
|
|
293
316
|
print_models_usage()
|
|
@@ -312,6 +335,8 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
|
|
|
312
335
|
markup=False,
|
|
313
336
|
)
|
|
314
337
|
console.print(f" api_key: {'***' if profile.api_key else 'Not set'}", markup=False)
|
|
338
|
+
if profile.openai_tool_mode:
|
|
339
|
+
console.print(f" openai_tool_mode: {profile.openai_tool_mode}", markup=False)
|
|
315
340
|
pointer_labels = ", ".join(f"{p}->{v or '-'}" for p, v in pointer_map.items())
|
|
316
341
|
console.print(f"[dim]Pointers: {escape(pointer_labels)}[/dim]")
|
|
317
342
|
return True
|
|
@@ -17,7 +17,7 @@ def _format_time(dt: datetime) -> str:
|
|
|
17
17
|
return dt.strftime("%Y-%m-%d %H:%M")
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
def _choose_session(ui, arg: str) -> Optional[SessionSummary]:
|
|
20
|
+
def _choose_session(ui: Any, arg: str) -> Optional[SessionSummary]:
|
|
21
21
|
sessions = list_session_summaries(ui.project_path)
|
|
22
22
|
if not sessions:
|
|
23
23
|
ui.console.print("[yellow]No saved sessions found for this project.[/yellow]")
|
|
@@ -30,7 +30,7 @@ def _choose_session(ui, arg: str) -> Optional[SessionSummary]:
|
|
|
30
30
|
if 0 <= idx < len(sessions):
|
|
31
31
|
return sessions[idx]
|
|
32
32
|
ui.console.print(
|
|
33
|
-
f"[red]Invalid session index {escape(str(idx))}. Choose 0-{len(sessions)-1}.[/red]"
|
|
33
|
+
f"[red]Invalid session index {escape(str(idx))}. Choose 0-{len(sessions) - 1}.[/red]"
|
|
34
34
|
)
|
|
35
35
|
else:
|
|
36
36
|
# Treat arg as session id if it matches.
|
|
@@ -60,7 +60,7 @@ def _choose_session(ui, arg: str) -> Optional[SessionSummary]:
|
|
|
60
60
|
idx = int(choice_text)
|
|
61
61
|
if idx < 0 or idx >= len(sessions):
|
|
62
62
|
ui.console.print(
|
|
63
|
-
f"[red]Invalid session index {escape(str(idx))}. Choose 0-{len(sessions)-1}.[/red]"
|
|
63
|
+
f"[red]Invalid session index {escape(str(idx))}. Choose 0-{len(sessions) - 1}.[/red]"
|
|
64
64
|
)
|
|
65
65
|
return None
|
|
66
66
|
return sessions[idx]
|
|
@@ -22,8 +22,8 @@ def _auth_token_display(profile: Optional[ModelProfile]) -> Tuple[str, Optional[
|
|
|
22
22
|
if not profile:
|
|
23
23
|
return ("Not configured", None)
|
|
24
24
|
|
|
25
|
-
provider_value =
|
|
26
|
-
profile.provider
|
|
25
|
+
provider_value = (
|
|
26
|
+
profile.provider.value if hasattr(profile.provider, "value") else str(profile.provider)
|
|
27
27
|
)
|
|
28
28
|
|
|
29
29
|
env_candidates = api_key_env_candidates(profile.provider)
|
|
@@ -50,8 +50,8 @@ def _api_base_display(profile: Optional[ModelProfile]) -> str:
|
|
|
50
50
|
ProviderType.GEMINI: "Gemini base URL",
|
|
51
51
|
}
|
|
52
52
|
label = label_map.get(profile.provider, "API base URL")
|
|
53
|
-
provider_value =
|
|
54
|
-
profile.provider
|
|
53
|
+
provider_value = (
|
|
54
|
+
profile.provider.value if hasattr(profile.provider, "value") else str(profile.provider)
|
|
55
55
|
)
|
|
56
56
|
|
|
57
57
|
env_candidates = api_base_env_candidates(profile.provider)
|
|
@@ -81,7 +81,7 @@ def _memory_status_lines(memory_files: List[MemoryFile]) -> List[str]:
|
|
|
81
81
|
|
|
82
82
|
|
|
83
83
|
def _setting_sources_summary(
|
|
84
|
-
config,
|
|
84
|
+
config: Any,
|
|
85
85
|
profile: Optional[ModelProfile],
|
|
86
86
|
memory_files: List[MemoryFile],
|
|
87
87
|
auth_env_var: Optional[str],
|
|
@@ -13,12 +13,16 @@ from ripperdoc.tools.background_shell import (
|
|
|
13
13
|
kill_background_task,
|
|
14
14
|
list_background_tasks,
|
|
15
15
|
)
|
|
16
|
+
from ripperdoc.utils.log import get_logger
|
|
16
17
|
|
|
17
|
-
from typing import Any
|
|
18
|
+
from typing import Any, Optional
|
|
18
19
|
from .base import SlashCommand
|
|
19
20
|
|
|
20
21
|
|
|
21
|
-
|
|
22
|
+
logger = get_logger()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _format_duration(duration_ms: Optional[float]) -> str:
|
|
22
26
|
"""Render milliseconds into a short human-readable duration."""
|
|
23
27
|
if duration_ms is None:
|
|
24
28
|
return "-"
|
|
@@ -82,7 +86,7 @@ def _tail_lines(text: str, max_lines: int = 20, max_chars: int = 4000) -> str:
|
|
|
82
86
|
return content
|
|
83
87
|
|
|
84
88
|
|
|
85
|
-
def _list_tasks(ui) -> bool:
|
|
89
|
+
def _list_tasks(ui: Any) -> bool:
|
|
86
90
|
console = ui.console
|
|
87
91
|
task_ids = list_background_tasks()
|
|
88
92
|
|
|
@@ -103,6 +107,10 @@ def _list_tasks(ui) -> bool:
|
|
|
103
107
|
status = get_background_status(task_id, consume=False)
|
|
104
108
|
except Exception as exc:
|
|
105
109
|
table.add_row(escape(task_id), "[red]error[/]", escape(str(exc)), "-")
|
|
110
|
+
logger.exception(
|
|
111
|
+
"[tasks_cmd] Failed to read background task status",
|
|
112
|
+
extra={"task_id": task_id, "session_id": getattr(ui, "session_id", None)},
|
|
113
|
+
)
|
|
106
114
|
continue
|
|
107
115
|
|
|
108
116
|
command = status.get("command") or ""
|
|
@@ -128,7 +136,7 @@ def _list_tasks(ui) -> bool:
|
|
|
128
136
|
return True
|
|
129
137
|
|
|
130
138
|
|
|
131
|
-
def _kill_task(ui, task_id: str) -> bool:
|
|
139
|
+
def _kill_task(ui: Any, task_id: str) -> bool:
|
|
132
140
|
console = ui.console
|
|
133
141
|
try:
|
|
134
142
|
status = get_background_status(task_id, consume=False)
|
|
@@ -137,6 +145,10 @@ def _kill_task(ui, task_id: str) -> bool:
|
|
|
137
145
|
return True
|
|
138
146
|
except Exception as exc:
|
|
139
147
|
console.print(f"[red]Failed to read task '{escape(task_id)}': {escape(str(exc))}[/red]")
|
|
148
|
+
logger.exception(
|
|
149
|
+
"[tasks_cmd] Failed to read task before kill",
|
|
150
|
+
extra={"task_id": task_id, "session_id": getattr(ui, "session_id", None)},
|
|
151
|
+
)
|
|
140
152
|
return True
|
|
141
153
|
|
|
142
154
|
if status.get("status") != "running":
|
|
@@ -149,6 +161,10 @@ def _kill_task(ui, task_id: str) -> bool:
|
|
|
149
161
|
killed = asyncio.run(kill_background_task(task_id))
|
|
150
162
|
except Exception as exc:
|
|
151
163
|
console.print(f"[red]Error stopping task {escape(task_id)}: {escape(str(exc))}[/red]")
|
|
164
|
+
logger.exception(
|
|
165
|
+
"[tasks_cmd] Error stopping background task",
|
|
166
|
+
extra={"task_id": task_id, "session_id": getattr(ui, "session_id", None)},
|
|
167
|
+
)
|
|
152
168
|
return True
|
|
153
169
|
|
|
154
170
|
if killed:
|
|
@@ -160,7 +176,7 @@ def _kill_task(ui, task_id: str) -> bool:
|
|
|
160
176
|
return True
|
|
161
177
|
|
|
162
178
|
|
|
163
|
-
def _show_task(ui, task_id: str) -> bool:
|
|
179
|
+
def _show_task(ui: Any, task_id: str) -> bool:
|
|
164
180
|
console = ui.console
|
|
165
181
|
try:
|
|
166
182
|
status = get_background_status(task_id, consume=False)
|
|
@@ -169,6 +185,10 @@ def _show_task(ui, task_id: str) -> bool:
|
|
|
169
185
|
return True
|
|
170
186
|
except Exception as exc:
|
|
171
187
|
console.print(f"[red]Failed to read task '{escape(task_id)}': {escape(str(exc))}[/red]")
|
|
188
|
+
logger.exception(
|
|
189
|
+
"[tasks_cmd] Failed to read task for detail view",
|
|
190
|
+
extra={"task_id": task_id, "session_id": getattr(ui, "session_id", None)},
|
|
191
|
+
)
|
|
172
192
|
return True
|
|
173
193
|
|
|
174
194
|
details = Table(box=box.SIMPLE_HEAVY, show_header=False)
|
|
@@ -208,6 +228,13 @@ def _show_task(ui, task_id: str) -> bool:
|
|
|
208
228
|
|
|
209
229
|
def _handle(ui: Any, args: str) -> bool:
|
|
210
230
|
parts = args.split()
|
|
231
|
+
logger.info(
|
|
232
|
+
"[tasks_cmd] Handling /tasks command",
|
|
233
|
+
extra={
|
|
234
|
+
"session_id": getattr(ui, "session_id", None),
|
|
235
|
+
"raw_args": args,
|
|
236
|
+
},
|
|
237
|
+
)
|
|
211
238
|
if not parts:
|
|
212
239
|
return _list_tasks(ui)
|
|
213
240
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import re
|
|
6
|
-
from typing import Any, Dict, List, Optional
|
|
6
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
7
7
|
|
|
8
8
|
from ripperdoc.utils.message_compaction import ContextBreakdown
|
|
9
9
|
|
|
@@ -205,7 +205,8 @@ def make_segment_grid(
|
|
|
205
205
|
|
|
206
206
|
rows: List[str] = []
|
|
207
207
|
for start in range(0, total_slots, per_row):
|
|
208
|
-
|
|
208
|
+
row_icons = [icon for icon in icons[start : start + per_row] if icon is not None]
|
|
209
|
+
rows.append(" ".join(row_icons))
|
|
209
210
|
return rows
|
|
210
211
|
|
|
211
212
|
|
|
@@ -229,7 +230,7 @@ def context_usage_lines(
|
|
|
229
230
|
grid_lines.append(f" {row}")
|
|
230
231
|
|
|
231
232
|
# Textual stats (without additional mini bars).
|
|
232
|
-
stats = [
|
|
233
|
+
stats: List[Tuple[str, Optional[int], Optional[float]]] = [
|
|
233
234
|
(
|
|
234
235
|
f"{styled_symbol('⛁', 'grey58')} System prompt",
|
|
235
236
|
breakdown.system_prompt_tokens,
|