ripperdoc 0.2.8__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.
- ripperdoc/__init__.py +1 -1
- ripperdoc/cli/cli.py +257 -123
- ripperdoc/cli/commands/__init__.py +2 -1
- ripperdoc/cli/commands/agents_cmd.py +138 -8
- ripperdoc/cli/commands/clear_cmd.py +9 -4
- ripperdoc/cli/commands/config_cmd.py +1 -1
- ripperdoc/cli/commands/context_cmd.py +3 -2
- ripperdoc/cli/commands/doctor_cmd.py +18 -4
- ripperdoc/cli/commands/exit_cmd.py +1 -0
- ripperdoc/cli/commands/hooks_cmd.py +27 -53
- ripperdoc/cli/commands/models_cmd.py +27 -10
- ripperdoc/cli/commands/permissions_cmd.py +27 -9
- ripperdoc/cli/commands/resume_cmd.py +9 -3
- ripperdoc/cli/commands/stats_cmd.py +244 -0
- ripperdoc/cli/commands/status_cmd.py +4 -4
- ripperdoc/cli/commands/tasks_cmd.py +8 -4
- ripperdoc/cli/ui/file_mention_completer.py +2 -1
- ripperdoc/cli/ui/interrupt_handler.py +2 -3
- ripperdoc/cli/ui/message_display.py +4 -2
- ripperdoc/cli/ui/panels.py +1 -0
- ripperdoc/cli/ui/provider_options.py +247 -0
- ripperdoc/cli/ui/rich_ui.py +403 -81
- ripperdoc/cli/ui/spinner.py +54 -18
- ripperdoc/cli/ui/thinking_spinner.py +1 -2
- ripperdoc/cli/ui/tool_renderers.py +8 -2
- ripperdoc/cli/ui/wizard.py +213 -0
- ripperdoc/core/agents.py +19 -6
- ripperdoc/core/config.py +51 -17
- ripperdoc/core/custom_commands.py +7 -6
- ripperdoc/core/default_tools.py +101 -12
- ripperdoc/core/hooks/config.py +1 -3
- ripperdoc/core/hooks/events.py +27 -28
- ripperdoc/core/hooks/executor.py +4 -6
- ripperdoc/core/hooks/integration.py +12 -21
- ripperdoc/core/hooks/llm_callback.py +59 -0
- ripperdoc/core/hooks/manager.py +40 -15
- ripperdoc/core/permissions.py +118 -12
- ripperdoc/core/providers/anthropic.py +109 -36
- ripperdoc/core/providers/gemini.py +70 -5
- ripperdoc/core/providers/openai.py +89 -24
- ripperdoc/core/query.py +273 -68
- ripperdoc/core/query_utils.py +2 -0
- ripperdoc/core/skills.py +9 -3
- ripperdoc/core/system_prompt.py +4 -2
- ripperdoc/core/tool.py +17 -8
- ripperdoc/sdk/client.py +79 -4
- ripperdoc/tools/ask_user_question_tool.py +5 -3
- ripperdoc/tools/background_shell.py +307 -135
- ripperdoc/tools/bash_output_tool.py +1 -1
- ripperdoc/tools/bash_tool.py +63 -24
- ripperdoc/tools/dynamic_mcp_tool.py +29 -8
- ripperdoc/tools/enter_plan_mode_tool.py +1 -1
- ripperdoc/tools/exit_plan_mode_tool.py +1 -1
- ripperdoc/tools/file_edit_tool.py +167 -54
- ripperdoc/tools/file_read_tool.py +28 -4
- ripperdoc/tools/file_write_tool.py +13 -10
- ripperdoc/tools/glob_tool.py +3 -2
- ripperdoc/tools/grep_tool.py +3 -2
- ripperdoc/tools/kill_bash_tool.py +1 -1
- ripperdoc/tools/ls_tool.py +1 -1
- ripperdoc/tools/lsp_tool.py +615 -0
- ripperdoc/tools/mcp_tools.py +13 -10
- ripperdoc/tools/multi_edit_tool.py +8 -7
- ripperdoc/tools/notebook_edit_tool.py +7 -4
- ripperdoc/tools/skill_tool.py +1 -1
- ripperdoc/tools/task_tool.py +519 -69
- ripperdoc/tools/todo_tool.py +2 -2
- ripperdoc/tools/tool_search_tool.py +3 -2
- ripperdoc/utils/conversation_compaction.py +9 -5
- ripperdoc/utils/file_watch.py +214 -5
- ripperdoc/utils/json_utils.py +2 -1
- ripperdoc/utils/lsp.py +806 -0
- ripperdoc/utils/mcp.py +11 -3
- ripperdoc/utils/memory.py +4 -2
- ripperdoc/utils/message_compaction.py +21 -7
- ripperdoc/utils/message_formatting.py +14 -7
- ripperdoc/utils/messages.py +126 -67
- ripperdoc/utils/path_ignore.py +35 -8
- ripperdoc/utils/permissions/path_validation_utils.py +2 -1
- ripperdoc/utils/permissions/shell_command_validation.py +427 -91
- ripperdoc/utils/permissions/tool_permission_utils.py +174 -15
- ripperdoc/utils/safe_get_cwd.py +2 -1
- ripperdoc/utils/session_heatmap.py +244 -0
- ripperdoc/utils/session_history.py +13 -6
- ripperdoc/utils/session_stats.py +293 -0
- ripperdoc/utils/todo.py +2 -1
- ripperdoc/utils/token_estimation.py +6 -1
- {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.10.dist-info}/METADATA +8 -2
- ripperdoc-0.2.10.dist-info/RECORD +129 -0
- ripperdoc-0.2.8.dist-info/RECORD +0 -121
- {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.10.dist-info}/WHEEL +0 -0
- {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.10.dist-info}/entry_points.txt +0 -0
- {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.10.dist-info}/licenses/LICENSE +0 -0
- {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.10.dist-info}/top_level.txt +0 -0
ripperdoc/cli/ui/rich_ui.py
CHANGED
|
@@ -6,6 +6,7 @@ This module provides a clean, minimal terminal UI using Rich for the Ripperdoc a
|
|
|
6
6
|
import asyncio
|
|
7
7
|
import json
|
|
8
8
|
import sys
|
|
9
|
+
import time
|
|
9
10
|
import uuid
|
|
10
11
|
from typing import List, Dict, Any, Optional, Union, Iterable
|
|
11
12
|
from pathlib import Path
|
|
@@ -25,6 +26,7 @@ from ripperdoc.core.query import query, QueryContext
|
|
|
25
26
|
from ripperdoc.core.system_prompt import build_system_prompt
|
|
26
27
|
from ripperdoc.core.skills import build_skill_summary, load_all_skills
|
|
27
28
|
from ripperdoc.core.hooks.manager import hook_manager
|
|
29
|
+
from ripperdoc.core.hooks.llm_callback import build_hook_llm_callback
|
|
28
30
|
from ripperdoc.cli.commands import (
|
|
29
31
|
get_slash_command,
|
|
30
32
|
get_custom_command,
|
|
@@ -64,6 +66,8 @@ from ripperdoc.utils.mcp import (
|
|
|
64
66
|
load_mcp_servers_async,
|
|
65
67
|
shutdown_mcp_runtime,
|
|
66
68
|
)
|
|
69
|
+
from ripperdoc.utils.lsp import shutdown_lsp_manager
|
|
70
|
+
from ripperdoc.tools.background_shell import shutdown_background_shell
|
|
67
71
|
from ripperdoc.tools.mcp_tools import load_dynamic_mcp_tools_async, merge_tools_with_dynamic
|
|
68
72
|
from ripperdoc.utils.session_history import SessionHistory
|
|
69
73
|
from ripperdoc.utils.memory import build_memory_instructions
|
|
@@ -86,31 +90,37 @@ console = Console()
|
|
|
86
90
|
logger = get_logger()
|
|
87
91
|
|
|
88
92
|
|
|
89
|
-
# Legacy aliases for backward compatibility with tests
|
|
90
|
-
_extract_tool_ids_from_message = extract_tool_ids_from_message
|
|
91
|
-
_get_complete_tool_pairs_tail = get_complete_tool_pairs_tail
|
|
92
|
-
|
|
93
|
-
|
|
94
93
|
class RichUI:
|
|
95
94
|
"""Rich-based UI for Ripperdoc."""
|
|
96
95
|
|
|
97
96
|
def __init__(
|
|
98
97
|
self,
|
|
99
|
-
|
|
98
|
+
yolo_mode: bool = False,
|
|
100
99
|
verbose: bool = False,
|
|
100
|
+
show_full_thinking: Optional[bool] = None,
|
|
101
101
|
session_id: Optional[str] = None,
|
|
102
102
|
log_file_path: Optional[Path] = None,
|
|
103
|
+
allowed_tools: Optional[List[str]] = None,
|
|
104
|
+
custom_system_prompt: Optional[str] = None,
|
|
105
|
+
append_system_prompt: Optional[str] = None,
|
|
106
|
+
model: Optional[str] = None,
|
|
107
|
+
resume_messages: Optional[List[Any]] = None,
|
|
103
108
|
):
|
|
104
109
|
self._loop = asyncio.new_event_loop()
|
|
105
110
|
asyncio.set_event_loop(self._loop)
|
|
106
111
|
self.console = console
|
|
107
|
-
self.
|
|
112
|
+
self.yolo_mode = yolo_mode
|
|
108
113
|
self.verbose = verbose
|
|
114
|
+
self.allowed_tools = allowed_tools
|
|
115
|
+
self.custom_system_prompt = custom_system_prompt
|
|
116
|
+
self.append_system_prompt = append_system_prompt
|
|
117
|
+
self.model = model or "main"
|
|
109
118
|
self.conversation_messages: List[ConversationMessage] = []
|
|
110
119
|
self._saved_conversation: Optional[List[ConversationMessage]] = None
|
|
111
120
|
self.query_context: Optional[QueryContext] = None
|
|
112
121
|
self._current_tool: Optional[str] = None
|
|
113
122
|
self._should_exit: bool = False
|
|
123
|
+
self._last_ctrl_c_time: float = 0.0 # Track Ctrl+C timing for double-press exit
|
|
114
124
|
self.command_list = list_slash_commands()
|
|
115
125
|
self._custom_command_list = list_custom_commands()
|
|
116
126
|
self._prompt_session: Optional[PromptSession] = None
|
|
@@ -128,16 +138,22 @@ class RichUI:
|
|
|
128
138
|
"session_id": self.session_id,
|
|
129
139
|
"project_path": str(self.project_path),
|
|
130
140
|
"log_file": str(self.log_file_path),
|
|
131
|
-
"
|
|
141
|
+
"yolo_mode": self.yolo_mode,
|
|
132
142
|
"verbose": self.verbose,
|
|
133
143
|
},
|
|
134
144
|
)
|
|
135
145
|
self._session_history = SessionHistory(self.project_path, self.session_id)
|
|
146
|
+
self._session_hook_contexts: List[str] = []
|
|
147
|
+
self._session_start_time = time.time()
|
|
148
|
+
self._session_end_sent = False
|
|
149
|
+
self._exit_reason: Optional[str] = None
|
|
150
|
+
hook_manager.set_transcript_path(str(self._session_history.path))
|
|
136
151
|
self._permission_checker = (
|
|
137
|
-
make_permission_checker(self.project_path,
|
|
152
|
+
None if yolo_mode else make_permission_checker(self.project_path, yolo_mode=False)
|
|
138
153
|
)
|
|
139
154
|
# Build ignore filter for file completion
|
|
140
155
|
from ripperdoc.utils.path_ignore import get_project_ignore_patterns
|
|
156
|
+
|
|
141
157
|
project_patterns = get_project_ignore_patterns()
|
|
142
158
|
self._ignore_filter = build_ignore_filter(
|
|
143
159
|
self.project_path,
|
|
@@ -146,8 +162,17 @@ class RichUI:
|
|
|
146
162
|
include_gitignore=True,
|
|
147
163
|
)
|
|
148
164
|
|
|
165
|
+
# Get global config for display preferences
|
|
166
|
+
config = get_global_config()
|
|
167
|
+
if show_full_thinking is None:
|
|
168
|
+
self.show_full_thinking = config.show_full_thinking
|
|
169
|
+
else:
|
|
170
|
+
self.show_full_thinking = show_full_thinking
|
|
171
|
+
|
|
149
172
|
# Initialize component handlers
|
|
150
|
-
self._message_display = MessageDisplay(
|
|
173
|
+
self._message_display = MessageDisplay(
|
|
174
|
+
self.console, self.verbose, self.show_full_thinking
|
|
175
|
+
)
|
|
151
176
|
self._interrupt_handler = InterruptHandler()
|
|
152
177
|
self._interrupt_handler.set_abort_callback(self._trigger_abort)
|
|
153
178
|
|
|
@@ -157,13 +182,15 @@ class RichUI:
|
|
|
157
182
|
except (OSError, RuntimeError, ConnectionError) as exc:
|
|
158
183
|
logger.warning(
|
|
159
184
|
"[ui] Failed to initialize MCP runtime at startup: %s: %s",
|
|
160
|
-
type(exc).__name__,
|
|
185
|
+
type(exc).__name__,
|
|
186
|
+
exc,
|
|
161
187
|
extra={"session_id": self.session_id},
|
|
162
188
|
)
|
|
163
189
|
|
|
164
190
|
# Initialize hook manager with project context
|
|
165
191
|
hook_manager.set_project_dir(self.project_path)
|
|
166
192
|
hook_manager.set_session_id(self.session_id)
|
|
193
|
+
hook_manager.set_llm_callback(build_hook_llm_callback())
|
|
167
194
|
logger.debug(
|
|
168
195
|
"[ui] Initialized hook manager",
|
|
169
196
|
extra={
|
|
@@ -171,6 +198,18 @@ class RichUI:
|
|
|
171
198
|
"project_path": str(self.project_path),
|
|
172
199
|
},
|
|
173
200
|
)
|
|
201
|
+
self._run_session_start("startup")
|
|
202
|
+
|
|
203
|
+
# Handle resume_messages if provided (for --continue)
|
|
204
|
+
if resume_messages:
|
|
205
|
+
self.conversation_messages = list(resume_messages)
|
|
206
|
+
logger.info(
|
|
207
|
+
"[ui] Resumed conversation with messages",
|
|
208
|
+
extra={
|
|
209
|
+
"session_id": self.session_id,
|
|
210
|
+
"message_count": len(resume_messages),
|
|
211
|
+
},
|
|
212
|
+
)
|
|
174
213
|
|
|
175
214
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
176
215
|
# Properties for backward compatibility with interrupt handler
|
|
@@ -206,6 +245,69 @@ class RichUI:
|
|
|
206
245
|
},
|
|
207
246
|
)
|
|
208
247
|
self._session_history = SessionHistory(self.project_path, session_id)
|
|
248
|
+
hook_manager.set_session_id(self.session_id)
|
|
249
|
+
hook_manager.set_transcript_path(str(self._session_history.path))
|
|
250
|
+
|
|
251
|
+
def _collect_hook_contexts(self, hook_result: Any) -> List[str]:
|
|
252
|
+
contexts: List[str] = []
|
|
253
|
+
system_message = getattr(hook_result, "system_message", None)
|
|
254
|
+
additional_context = getattr(hook_result, "additional_context", None)
|
|
255
|
+
if system_message:
|
|
256
|
+
contexts.append(str(system_message))
|
|
257
|
+
if additional_context:
|
|
258
|
+
contexts.append(str(additional_context))
|
|
259
|
+
return contexts
|
|
260
|
+
|
|
261
|
+
def _set_session_hook_contexts(self, hook_result: Any) -> None:
|
|
262
|
+
self._session_hook_contexts = self._collect_hook_contexts(hook_result)
|
|
263
|
+
self._session_start_time = time.time()
|
|
264
|
+
self._session_end_sent = False
|
|
265
|
+
|
|
266
|
+
def _run_session_start(self, source: str) -> None:
|
|
267
|
+
try:
|
|
268
|
+
result = self._run_async(hook_manager.run_session_start_async(source))
|
|
269
|
+
except (OSError, RuntimeError, ConnectionError, ValueError, TypeError) as exc:
|
|
270
|
+
logger.warning(
|
|
271
|
+
"[ui] SessionStart hook failed: %s: %s",
|
|
272
|
+
type(exc).__name__,
|
|
273
|
+
exc,
|
|
274
|
+
extra={"session_id": self.session_id, "source": source},
|
|
275
|
+
)
|
|
276
|
+
return
|
|
277
|
+
self._set_session_hook_contexts(result)
|
|
278
|
+
|
|
279
|
+
async def _run_session_start_async(self, source: str) -> None:
|
|
280
|
+
try:
|
|
281
|
+
result = await hook_manager.run_session_start_async(source)
|
|
282
|
+
except (OSError, RuntimeError, ConnectionError, ValueError, TypeError) as exc:
|
|
283
|
+
logger.warning(
|
|
284
|
+
"[ui] SessionStart hook failed: %s: %s",
|
|
285
|
+
type(exc).__name__,
|
|
286
|
+
exc,
|
|
287
|
+
extra={"session_id": self.session_id, "source": source},
|
|
288
|
+
)
|
|
289
|
+
return
|
|
290
|
+
self._set_session_hook_contexts(result)
|
|
291
|
+
|
|
292
|
+
def _run_session_end(self, reason: str) -> None:
|
|
293
|
+
if self._session_end_sent:
|
|
294
|
+
return
|
|
295
|
+
duration = max(time.time() - self._session_start_time, 0.0)
|
|
296
|
+
message_count = len(self.conversation_messages)
|
|
297
|
+
try:
|
|
298
|
+
self._run_async(
|
|
299
|
+
hook_manager.run_session_end_async(
|
|
300
|
+
reason, duration_seconds=duration, message_count=message_count
|
|
301
|
+
)
|
|
302
|
+
)
|
|
303
|
+
except (OSError, RuntimeError, ConnectionError, ValueError, TypeError) as exc:
|
|
304
|
+
logger.warning(
|
|
305
|
+
"[ui] SessionEnd hook failed: %s: %s",
|
|
306
|
+
type(exc).__name__,
|
|
307
|
+
exc,
|
|
308
|
+
extra={"session_id": self.session_id, "reason": reason},
|
|
309
|
+
)
|
|
310
|
+
self._session_end_sent = True
|
|
209
311
|
|
|
210
312
|
def _log_message(self, message: Any) -> None:
|
|
211
313
|
"""Best-effort persistence of a message to the session log."""
|
|
@@ -215,7 +317,8 @@ class RichUI:
|
|
|
215
317
|
# Logging failures should never interrupt the UI flow
|
|
216
318
|
logger.warning(
|
|
217
319
|
"[ui] Failed to append message to session history: %s: %s",
|
|
218
|
-
type(exc).__name__,
|
|
320
|
+
type(exc).__name__,
|
|
321
|
+
exc,
|
|
219
322
|
extra={"session_id": self.session_id},
|
|
220
323
|
)
|
|
221
324
|
|
|
@@ -229,7 +332,8 @@ class RichUI:
|
|
|
229
332
|
except (AttributeError, TypeError, ValueError) as exc:
|
|
230
333
|
logger.warning(
|
|
231
334
|
"[ui] Failed to append prompt history: %s: %s",
|
|
232
|
-
type(exc).__name__,
|
|
335
|
+
type(exc).__name__,
|
|
336
|
+
exc,
|
|
233
337
|
extra={"session_id": self.session_id},
|
|
234
338
|
)
|
|
235
339
|
|
|
@@ -264,8 +368,8 @@ class RichUI:
|
|
|
264
368
|
self.display_message("Ripperdoc", text)
|
|
265
369
|
|
|
266
370
|
def get_default_tools(self) -> list:
|
|
267
|
-
"""Get the default set of tools."""
|
|
268
|
-
return get_default_tools()
|
|
371
|
+
"""Get the default set of tools, filtered by allowed_tools if specified."""
|
|
372
|
+
return get_default_tools(allowed_tools=self.allowed_tools)
|
|
269
373
|
|
|
270
374
|
def display_message(
|
|
271
375
|
self,
|
|
@@ -320,7 +424,9 @@ class RichUI:
|
|
|
320
424
|
def _print_reasoning(self, reasoning: Any) -> None:
|
|
321
425
|
self._message_display.print_reasoning(reasoning)
|
|
322
426
|
|
|
323
|
-
async def _prepare_query_context(
|
|
427
|
+
async def _prepare_query_context(
|
|
428
|
+
self, user_input: str, hook_instructions: Optional[List[str]] = None
|
|
429
|
+
) -> tuple[str, Dict[str, str]]:
|
|
324
430
|
"""Load MCP servers, skills, and build system prompt.
|
|
325
431
|
|
|
326
432
|
Returns:
|
|
@@ -366,14 +472,32 @@ class RichUI:
|
|
|
366
472
|
memory_instructions = build_memory_instructions()
|
|
367
473
|
if memory_instructions:
|
|
368
474
|
additional_instructions.append(memory_instructions)
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
475
|
+
if self._session_hook_contexts:
|
|
476
|
+
additional_instructions.extend(self._session_hook_contexts)
|
|
477
|
+
if hook_instructions:
|
|
478
|
+
additional_instructions.extend([text for text in hook_instructions if text])
|
|
479
|
+
|
|
480
|
+
# Build system prompt based on options:
|
|
481
|
+
# - custom_system_prompt: replaces the default entirely
|
|
482
|
+
# - append_system_prompt: appends to the default system prompt
|
|
483
|
+
if self.custom_system_prompt:
|
|
484
|
+
# Complete replacement
|
|
485
|
+
system_prompt = self.custom_system_prompt
|
|
486
|
+
# Still append if both are provided
|
|
487
|
+
if self.append_system_prompt:
|
|
488
|
+
system_prompt = f"{system_prompt}\n\n{self.append_system_prompt}"
|
|
489
|
+
else:
|
|
490
|
+
# Build default with optional append
|
|
491
|
+
all_instructions = list(additional_instructions) if additional_instructions else []
|
|
492
|
+
if self.append_system_prompt:
|
|
493
|
+
all_instructions.append(self.append_system_prompt)
|
|
494
|
+
system_prompt = build_system_prompt(
|
|
495
|
+
self.query_context.tools if self.query_context else [],
|
|
496
|
+
user_input,
|
|
497
|
+
context,
|
|
498
|
+
additional_instructions=all_instructions or None,
|
|
499
|
+
mcp_instructions=mcp_instructions,
|
|
500
|
+
)
|
|
377
501
|
|
|
378
502
|
return system_prompt, context
|
|
379
503
|
|
|
@@ -438,10 +562,33 @@ class RichUI:
|
|
|
438
562
|
if usage_status.should_auto_compact:
|
|
439
563
|
original_messages = list(messages)
|
|
440
564
|
spinner = Spinner(self.console, "Summarizing conversation...", spinner="dots")
|
|
565
|
+
hook_instructions = ""
|
|
566
|
+
try:
|
|
567
|
+
hook_result = await hook_manager.run_pre_compact_async(
|
|
568
|
+
trigger="auto", custom_instructions=""
|
|
569
|
+
)
|
|
570
|
+
if hook_result.should_block or not hook_result.should_continue:
|
|
571
|
+
reason = (
|
|
572
|
+
hook_result.block_reason
|
|
573
|
+
or hook_result.stop_reason
|
|
574
|
+
or "Compaction blocked by hook."
|
|
575
|
+
)
|
|
576
|
+
console.print(f"[yellow]{escape(str(reason))}[/yellow]")
|
|
577
|
+
return messages
|
|
578
|
+
hook_contexts = self._collect_hook_contexts(hook_result)
|
|
579
|
+
if hook_contexts:
|
|
580
|
+
hook_instructions = "\n\n".join(hook_contexts)
|
|
581
|
+
except (OSError, RuntimeError, ConnectionError, ValueError, TypeError) as exc:
|
|
582
|
+
logger.warning(
|
|
583
|
+
"[ui] PreCompact hook failed: %s: %s",
|
|
584
|
+
type(exc).__name__,
|
|
585
|
+
exc,
|
|
586
|
+
extra={"session_id": self.session_id},
|
|
587
|
+
)
|
|
441
588
|
try:
|
|
442
589
|
spinner.start()
|
|
443
590
|
result = await compact_conversation(
|
|
444
|
-
messages, custom_instructions=
|
|
591
|
+
messages, custom_instructions=hook_instructions, protocol=protocol
|
|
445
592
|
)
|
|
446
593
|
finally:
|
|
447
594
|
spinner.stop()
|
|
@@ -462,6 +609,7 @@ class RichUI:
|
|
|
462
609
|
"tokens_saved": result.tokens_saved,
|
|
463
610
|
},
|
|
464
611
|
)
|
|
612
|
+
await self._run_session_start_async("compact")
|
|
465
613
|
return result.messages
|
|
466
614
|
elif isinstance(result, CompactionError):
|
|
467
615
|
logger.warning(
|
|
@@ -476,29 +624,36 @@ class RichUI:
|
|
|
476
624
|
self,
|
|
477
625
|
message: AssistantMessage,
|
|
478
626
|
tool_registry: Dict[str, Dict[str, Any]],
|
|
627
|
+
spinner: Optional[ThinkingSpinner] = None,
|
|
479
628
|
) -> Optional[str]:
|
|
480
629
|
"""Handle an assistant message from the query stream.
|
|
481
630
|
|
|
482
631
|
Returns:
|
|
483
632
|
The last tool name if a tool_use block was processed, None otherwise.
|
|
484
633
|
"""
|
|
634
|
+
# Factory to create pause context - spinner.paused() if spinner exists, else no-op
|
|
635
|
+
from contextlib import nullcontext
|
|
636
|
+
|
|
637
|
+
pause = lambda: spinner.paused() if spinner else nullcontext() # noqa: E731
|
|
638
|
+
|
|
485
639
|
meta = getattr(getattr(message, "message", None), "metadata", {}) or {}
|
|
486
640
|
reasoning_payload = (
|
|
487
|
-
meta.get("reasoning_content")
|
|
488
|
-
or meta.get("reasoning")
|
|
489
|
-
or meta.get("reasoning_details")
|
|
641
|
+
meta.get("reasoning_content") or meta.get("reasoning") or meta.get("reasoning_details")
|
|
490
642
|
)
|
|
491
643
|
if reasoning_payload:
|
|
492
|
-
|
|
644
|
+
with pause():
|
|
645
|
+
self._print_reasoning(reasoning_payload)
|
|
493
646
|
|
|
494
647
|
last_tool_name: Optional[str] = None
|
|
495
648
|
|
|
496
649
|
if isinstance(message.message.content, str):
|
|
497
|
-
|
|
650
|
+
with pause():
|
|
651
|
+
self.display_message("Ripperdoc", message.message.content)
|
|
498
652
|
elif isinstance(message.message.content, list):
|
|
499
653
|
for block in message.message.content:
|
|
500
654
|
if hasattr(block, "type") and block.type == "text" and block.text:
|
|
501
|
-
|
|
655
|
+
with pause():
|
|
656
|
+
self.display_message("Ripperdoc", block.text)
|
|
502
657
|
elif hasattr(block, "type") and block.type == "tool_use":
|
|
503
658
|
tool_name = getattr(block, "name", "unknown tool")
|
|
504
659
|
tool_args = getattr(block, "input", {})
|
|
@@ -512,9 +667,10 @@ class RichUI:
|
|
|
512
667
|
}
|
|
513
668
|
|
|
514
669
|
if tool_name == "Task":
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
670
|
+
with pause():
|
|
671
|
+
self.display_message(
|
|
672
|
+
tool_name, "", is_tool=True, tool_type="call", tool_args=tool_args
|
|
673
|
+
)
|
|
518
674
|
if tool_use_id:
|
|
519
675
|
tool_registry[tool_use_id]["printed"] = True
|
|
520
676
|
|
|
@@ -527,11 +683,17 @@ class RichUI:
|
|
|
527
683
|
message: UserMessage,
|
|
528
684
|
tool_registry: Dict[str, Dict[str, Any]],
|
|
529
685
|
last_tool_name: Optional[str],
|
|
686
|
+
spinner: Optional[ThinkingSpinner] = None,
|
|
530
687
|
) -> None:
|
|
531
688
|
"""Handle a user message containing tool results."""
|
|
532
689
|
if not isinstance(message.message.content, list):
|
|
533
690
|
return
|
|
534
691
|
|
|
692
|
+
# Factory to create pause context - spinner.paused() if spinner exists, else no-op
|
|
693
|
+
from contextlib import nullcontext
|
|
694
|
+
|
|
695
|
+
pause = lambda: spinner.paused() if spinner else nullcontext() # noqa: E731
|
|
696
|
+
|
|
535
697
|
for block in message.message.content:
|
|
536
698
|
if not (hasattr(block, "type") and block.type == "tool_result" and block.text):
|
|
537
699
|
continue
|
|
@@ -545,25 +707,27 @@ class RichUI:
|
|
|
545
707
|
if entry:
|
|
546
708
|
tool_name = entry.get("name", tool_name)
|
|
547
709
|
if not entry.get("printed"):
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
710
|
+
with pause():
|
|
711
|
+
self.display_message(
|
|
712
|
+
tool_name,
|
|
713
|
+
"",
|
|
714
|
+
is_tool=True,
|
|
715
|
+
tool_type="call",
|
|
716
|
+
tool_args=entry.get("args", {}),
|
|
717
|
+
)
|
|
555
718
|
entry["printed"] = True
|
|
556
719
|
elif last_tool_name:
|
|
557
720
|
tool_name = last_tool_name
|
|
558
721
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
722
|
+
with pause():
|
|
723
|
+
self.display_message(
|
|
724
|
+
tool_name,
|
|
725
|
+
block.text,
|
|
726
|
+
is_tool=True,
|
|
727
|
+
tool_type="result",
|
|
728
|
+
tool_data=tool_data,
|
|
729
|
+
tool_error=is_error,
|
|
730
|
+
)
|
|
567
731
|
|
|
568
732
|
def _handle_progress_message(
|
|
569
733
|
self,
|
|
@@ -577,14 +741,17 @@ class RichUI:
|
|
|
577
741
|
Updated output token estimate.
|
|
578
742
|
"""
|
|
579
743
|
if self.verbose:
|
|
580
|
-
|
|
744
|
+
with spinner.paused():
|
|
745
|
+
self.display_message("System", f"Progress: {message.content}", is_tool=True)
|
|
581
746
|
elif message.content and isinstance(message.content, str):
|
|
582
747
|
if message.content.startswith("Subagent: "):
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
748
|
+
with spinner.paused():
|
|
749
|
+
self.display_message(
|
|
750
|
+
"Subagent", message.content[len("Subagent: ") :], is_tool=True
|
|
751
|
+
)
|
|
586
752
|
elif message.content.startswith("Subagent"):
|
|
587
|
-
|
|
753
|
+
with spinner.paused():
|
|
754
|
+
self.display_message("Subagent", message.content, is_tool=True)
|
|
588
755
|
|
|
589
756
|
if message.tool_use_id == "stream":
|
|
590
757
|
delta_tokens = estimate_tokens(message.content)
|
|
@@ -600,12 +767,16 @@ class RichUI:
|
|
|
600
767
|
# Initialize or reset query context
|
|
601
768
|
if not self.query_context:
|
|
602
769
|
self.query_context = QueryContext(
|
|
603
|
-
tools=self.get_default_tools(),
|
|
770
|
+
tools=self.get_default_tools(),
|
|
771
|
+
yolo_mode=self.yolo_mode,
|
|
772
|
+
verbose=self.verbose,
|
|
773
|
+
model=self.model,
|
|
604
774
|
)
|
|
605
775
|
else:
|
|
606
776
|
abort_controller = getattr(self.query_context, "abort_controller", None)
|
|
607
777
|
if abort_controller is not None:
|
|
608
778
|
abort_controller.clear()
|
|
779
|
+
self.query_context.stop_hook_active = False
|
|
609
780
|
|
|
610
781
|
logger.info(
|
|
611
782
|
"[ui] Starting query processing",
|
|
@@ -617,8 +788,21 @@ class RichUI:
|
|
|
617
788
|
)
|
|
618
789
|
|
|
619
790
|
try:
|
|
791
|
+
hook_result = await hook_manager.run_user_prompt_submit_async(user_input)
|
|
792
|
+
if hook_result.should_block or not hook_result.should_continue:
|
|
793
|
+
reason = (
|
|
794
|
+
hook_result.block_reason
|
|
795
|
+
or hook_result.stop_reason
|
|
796
|
+
or "Prompt blocked by hook."
|
|
797
|
+
)
|
|
798
|
+
self.console.print(f"[red]{escape(str(reason))}[/red]")
|
|
799
|
+
return
|
|
800
|
+
hook_instructions = self._collect_hook_contexts(hook_result)
|
|
801
|
+
|
|
620
802
|
# Prepare context and system prompt
|
|
621
|
-
system_prompt, context = await self._prepare_query_context(
|
|
803
|
+
system_prompt, context = await self._prepare_query_context(
|
|
804
|
+
user_input, hook_instructions
|
|
805
|
+
)
|
|
622
806
|
|
|
623
807
|
# Create and log user message
|
|
624
808
|
user_message = create_user_message(user_input)
|
|
@@ -684,7 +868,8 @@ class RichUI:
|
|
|
684
868
|
except (RuntimeError, ValueError, OSError) as exc:
|
|
685
869
|
logger.debug(
|
|
686
870
|
"[ui] Failed to restart spinner after permission check: %s: %s",
|
|
687
|
-
type(exc).__name__,
|
|
871
|
+
type(exc).__name__,
|
|
872
|
+
exc,
|
|
688
873
|
)
|
|
689
874
|
|
|
690
875
|
# Process query stream
|
|
@@ -702,12 +887,14 @@ class RichUI:
|
|
|
702
887
|
permission_checker, # type: ignore[arg-type]
|
|
703
888
|
):
|
|
704
889
|
if message.type == "assistant" and isinstance(message, AssistantMessage):
|
|
705
|
-
result = self._handle_assistant_message(message, tool_registry)
|
|
890
|
+
result = self._handle_assistant_message(message, tool_registry, spinner)
|
|
706
891
|
if result:
|
|
707
892
|
last_tool_name = result
|
|
708
893
|
|
|
709
894
|
elif message.type == "user" and isinstance(message, UserMessage):
|
|
710
|
-
self._handle_tool_result_message(
|
|
895
|
+
self._handle_tool_result_message(
|
|
896
|
+
message, tool_registry, last_tool_name, spinner
|
|
897
|
+
)
|
|
711
898
|
|
|
712
899
|
elif message.type == "progress" and isinstance(message, ProgressMessage):
|
|
713
900
|
output_token_est = self._handle_progress_message(
|
|
@@ -723,7 +910,8 @@ class RichUI:
|
|
|
723
910
|
except (OSError, ConnectionError, RuntimeError, ValueError, KeyError, TypeError) as e:
|
|
724
911
|
logger.warning(
|
|
725
912
|
"[ui] Error while processing streamed query response: %s: %s",
|
|
726
|
-
type(e).__name__,
|
|
913
|
+
type(e).__name__,
|
|
914
|
+
e,
|
|
727
915
|
extra={"session_id": self.session_id},
|
|
728
916
|
)
|
|
729
917
|
self.display_message("System", f"Error: {str(e)}", is_tool=True)
|
|
@@ -733,7 +921,8 @@ class RichUI:
|
|
|
733
921
|
except (RuntimeError, ValueError, OSError) as exc:
|
|
734
922
|
logger.warning(
|
|
735
923
|
"[ui] Failed to stop spinner: %s: %s",
|
|
736
|
-
type(exc).__name__,
|
|
924
|
+
type(exc).__name__,
|
|
925
|
+
exc,
|
|
737
926
|
extra={"session_id": self.session_id},
|
|
738
927
|
)
|
|
739
928
|
|
|
@@ -753,7 +942,8 @@ class RichUI:
|
|
|
753
942
|
except (OSError, ConnectionError, RuntimeError, ValueError, KeyError, TypeError) as exc:
|
|
754
943
|
logger.warning(
|
|
755
944
|
"[ui] Error during query processing: %s: %s",
|
|
756
|
-
type(exc).__name__,
|
|
945
|
+
type(exc).__name__,
|
|
946
|
+
exc,
|
|
757
947
|
extra={"session_id": self.session_id},
|
|
758
948
|
)
|
|
759
949
|
self.display_message("System", f"Error: {str(exc)}", is_tool=True)
|
|
@@ -823,14 +1013,10 @@ class RichUI:
|
|
|
823
1013
|
custom_cmd = get_custom_command(command_name, self.project_path)
|
|
824
1014
|
if custom_cmd is not None:
|
|
825
1015
|
# Expand the custom command content
|
|
826
|
-
expanded_content = expand_command_content(
|
|
827
|
-
custom_cmd, trimmed_arg, self.project_path
|
|
828
|
-
)
|
|
1016
|
+
expanded_content = expand_command_content(custom_cmd, trimmed_arg, self.project_path)
|
|
829
1017
|
|
|
830
1018
|
# Show a hint that this is from a custom command
|
|
831
|
-
self.console.print(
|
|
832
|
-
f"[dim]Running custom command: /{command_name}[/dim]"
|
|
833
|
-
)
|
|
1019
|
+
self.console.print(f"[dim]Running custom command: /{command_name}[/dim]")
|
|
834
1020
|
if custom_cmd.argument_hint and trimmed_arg:
|
|
835
1021
|
self.console.print(f"[dim]Arguments: {trimmed_arg}[/dim]")
|
|
836
1022
|
|
|
@@ -851,7 +1037,7 @@ class RichUI:
|
|
|
851
1037
|
def __init__(self, project_path: Path):
|
|
852
1038
|
self.project_path = project_path
|
|
853
1039
|
|
|
854
|
-
def get_completions(self, document: Any,
|
|
1040
|
+
def get_completions(self, document: Any, _complete_event: Any) -> Iterable[Completion]:
|
|
855
1041
|
text = document.text_before_cursor
|
|
856
1042
|
if not text.startswith("/"):
|
|
857
1043
|
return
|
|
@@ -902,12 +1088,57 @@ class RichUI:
|
|
|
902
1088
|
else:
|
|
903
1089
|
buf.start_completion(select_first=True)
|
|
904
1090
|
|
|
1091
|
+
@key_bindings.add("escape", "enter")
|
|
1092
|
+
def _(event: Any) -> None:
|
|
1093
|
+
"""Insert newline on Alt+Enter."""
|
|
1094
|
+
event.current_buffer.insert_text("\n")
|
|
1095
|
+
|
|
1096
|
+
# Capture self for use in Ctrl+C handler closure
|
|
1097
|
+
ui_instance = self
|
|
1098
|
+
|
|
1099
|
+
@key_bindings.add("c-c")
|
|
1100
|
+
def _(event: Any) -> None:
|
|
1101
|
+
"""Handle Ctrl+C: first press clears input, second press exits."""
|
|
1102
|
+
import time as time_module
|
|
1103
|
+
|
|
1104
|
+
buf = event.current_buffer
|
|
1105
|
+
current_text = buf.text
|
|
1106
|
+
current_time = time_module.time()
|
|
1107
|
+
|
|
1108
|
+
# Check if this is a double Ctrl+C (within 1.5 seconds)
|
|
1109
|
+
if current_time - ui_instance._last_ctrl_c_time < 1.5:
|
|
1110
|
+
# Double Ctrl+C - exit
|
|
1111
|
+
buf.reset()
|
|
1112
|
+
raise KeyboardInterrupt()
|
|
1113
|
+
|
|
1114
|
+
# First Ctrl+C - save to history and clear
|
|
1115
|
+
ui_instance._last_ctrl_c_time = current_time
|
|
1116
|
+
|
|
1117
|
+
if current_text.strip():
|
|
1118
|
+
# Save current input to history before clearing
|
|
1119
|
+
try:
|
|
1120
|
+
event.app.current_buffer.history.append_string(current_text)
|
|
1121
|
+
except (AttributeError, TypeError, ValueError):
|
|
1122
|
+
pass
|
|
1123
|
+
|
|
1124
|
+
# Print hint message in clean terminal context, then clear buffer
|
|
1125
|
+
from prompt_toolkit.application import run_in_terminal
|
|
1126
|
+
|
|
1127
|
+
def _print_hint() -> None:
|
|
1128
|
+
print("\n\033[2mPress Ctrl+C again to exit, or continue typing.\033[0m")
|
|
1129
|
+
|
|
1130
|
+
run_in_terminal(_print_hint)
|
|
1131
|
+
|
|
1132
|
+
# Clear the buffer after printing
|
|
1133
|
+
buf.reset()
|
|
1134
|
+
|
|
905
1135
|
self._prompt_session = PromptSession(
|
|
906
1136
|
completer=combined_completer,
|
|
907
1137
|
complete_style=CompleteStyle.COLUMN,
|
|
908
1138
|
complete_while_typing=True,
|
|
909
1139
|
history=InMemoryHistory(),
|
|
910
1140
|
key_bindings=key_bindings,
|
|
1141
|
+
multiline=True,
|
|
911
1142
|
)
|
|
912
1143
|
return self._prompt_session
|
|
913
1144
|
|
|
@@ -921,7 +1152,10 @@ class RichUI:
|
|
|
921
1152
|
# Display status
|
|
922
1153
|
console.print(create_status_bar())
|
|
923
1154
|
console.print()
|
|
924
|
-
console.print(
|
|
1155
|
+
console.print(
|
|
1156
|
+
"[dim]Tip: type '/' then press Tab to see available commands. Type '@' to mention files. "
|
|
1157
|
+
"Press Alt+Enter for newline. Press ESC to interrupt. Press Ctrl+C twice to exit.[/dim]\n"
|
|
1158
|
+
)
|
|
925
1159
|
|
|
926
1160
|
session = self.get_prompt_session()
|
|
927
1161
|
logger.info(
|
|
@@ -929,6 +1163,7 @@ class RichUI:
|
|
|
929
1163
|
extra={"session_id": self.session_id, "log_file": str(self.log_file_path)},
|
|
930
1164
|
)
|
|
931
1165
|
|
|
1166
|
+
exit_reason = "other"
|
|
932
1167
|
try:
|
|
933
1168
|
while not self._should_exit:
|
|
934
1169
|
try:
|
|
@@ -951,6 +1186,7 @@ class RichUI:
|
|
|
951
1186
|
)
|
|
952
1187
|
handled = self.handle_slash_command(user_input)
|
|
953
1188
|
if self._should_exit:
|
|
1189
|
+
exit_reason = self._exit_reason or "other"
|
|
954
1190
|
break
|
|
955
1191
|
# If handled is a string, it's expanded custom command content
|
|
956
1192
|
if isinstance(handled, str):
|
|
@@ -972,7 +1208,9 @@ class RichUI:
|
|
|
972
1208
|
interrupted = self._run_async_with_esc_interrupt(self.process_query(user_input))
|
|
973
1209
|
|
|
974
1210
|
if interrupted:
|
|
975
|
-
console.print(
|
|
1211
|
+
console.print(
|
|
1212
|
+
"\n[red]■ Conversation interrupted[/red] · [dim]Tell the model what to do differently.[/dim]"
|
|
1213
|
+
)
|
|
976
1214
|
logger.info(
|
|
977
1215
|
"[ui] Query interrupted by ESC key",
|
|
978
1216
|
extra={"session_id": self.session_id},
|
|
@@ -981,21 +1219,45 @@ class RichUI:
|
|
|
981
1219
|
console.print() # Add spacing between interactions
|
|
982
1220
|
|
|
983
1221
|
except KeyboardInterrupt:
|
|
1222
|
+
# Handle Ctrl+C: first press during query aborts it,
|
|
1223
|
+
# double press exits the CLI
|
|
1224
|
+
current_time = time.time()
|
|
1225
|
+
|
|
984
1226
|
# Signal abort to cancel running queries
|
|
985
1227
|
if self.query_context:
|
|
986
1228
|
abort_controller = getattr(self.query_context, "abort_controller", None)
|
|
987
1229
|
if abort_controller is not None:
|
|
988
1230
|
abort_controller.set()
|
|
989
|
-
|
|
990
|
-
|
|
1231
|
+
|
|
1232
|
+
# Check if this is a double Ctrl+C (within 1.5 seconds)
|
|
1233
|
+
if current_time - self._last_ctrl_c_time < 1.5:
|
|
1234
|
+
console.print("\n[yellow]Goodbye![/yellow]")
|
|
1235
|
+
exit_reason = "prompt_input_exit"
|
|
1236
|
+
break
|
|
1237
|
+
|
|
1238
|
+
# First Ctrl+C - just abort the query and continue
|
|
1239
|
+
self._last_ctrl_c_time = current_time
|
|
1240
|
+
console.print(
|
|
1241
|
+
"\n[dim]Query interrupted. Press Ctrl+C again to exit.[/dim]"
|
|
1242
|
+
)
|
|
1243
|
+
continue
|
|
991
1244
|
except EOFError:
|
|
992
1245
|
console.print("\n[yellow]Goodbye![/yellow]")
|
|
1246
|
+
exit_reason = "prompt_input_exit"
|
|
993
1247
|
break
|
|
994
|
-
except (
|
|
1248
|
+
except (
|
|
1249
|
+
OSError,
|
|
1250
|
+
ConnectionError,
|
|
1251
|
+
RuntimeError,
|
|
1252
|
+
ValueError,
|
|
1253
|
+
KeyError,
|
|
1254
|
+
TypeError,
|
|
1255
|
+
) as e:
|
|
995
1256
|
console.print(f"[red]Error: {escape(str(e))}[/]")
|
|
996
1257
|
logger.warning(
|
|
997
1258
|
"[ui] Error in interactive loop: %s: %s",
|
|
998
|
-
type(e).__name__,
|
|
1259
|
+
type(e).__name__,
|
|
1260
|
+
e,
|
|
999
1261
|
extra={"session_id": self.session_id},
|
|
1000
1262
|
)
|
|
1001
1263
|
if self.verbose:
|
|
@@ -1009,6 +1271,8 @@ class RichUI:
|
|
|
1009
1271
|
if abort_controller is not None:
|
|
1010
1272
|
abort_controller.set()
|
|
1011
1273
|
|
|
1274
|
+
self._run_session_end(exit_reason)
|
|
1275
|
+
|
|
1012
1276
|
# Suppress async generator cleanup errors during shutdown
|
|
1013
1277
|
original_hook = sys.unraisablehook
|
|
1014
1278
|
|
|
@@ -1025,11 +1289,24 @@ class RichUI:
|
|
|
1025
1289
|
try:
|
|
1026
1290
|
try:
|
|
1027
1291
|
self._run_async(shutdown_mcp_runtime())
|
|
1292
|
+
self._run_async(shutdown_lsp_manager())
|
|
1028
1293
|
except (OSError, RuntimeError, ConnectionError, asyncio.CancelledError) as exc:
|
|
1029
1294
|
# pragma: no cover - defensive shutdown
|
|
1030
1295
|
logger.warning(
|
|
1031
1296
|
"[ui] Failed to shut down MCP runtime cleanly: %s: %s",
|
|
1032
|
-
type(exc).__name__,
|
|
1297
|
+
type(exc).__name__,
|
|
1298
|
+
exc,
|
|
1299
|
+
extra={"session_id": self.session_id},
|
|
1300
|
+
)
|
|
1301
|
+
|
|
1302
|
+
# Shutdown background shell manager to clean up any background tasks
|
|
1303
|
+
try:
|
|
1304
|
+
shutdown_background_shell(force=True)
|
|
1305
|
+
except (OSError, RuntimeError) as exc:
|
|
1306
|
+
logger.debug(
|
|
1307
|
+
"[ui] Failed to shut down background shell cleanly: %s: %s",
|
|
1308
|
+
type(exc).__name__,
|
|
1309
|
+
exc,
|
|
1033
1310
|
extra={"session_id": self.session_id},
|
|
1034
1311
|
)
|
|
1035
1312
|
finally:
|
|
@@ -1073,15 +1350,47 @@ class RichUI:
|
|
|
1073
1350
|
original_messages = list(self.conversation_messages)
|
|
1074
1351
|
spinner = Spinner(self.console, "Summarizing conversation...", spinner="dots")
|
|
1075
1352
|
|
|
1353
|
+
hook_instructions = ""
|
|
1354
|
+
try:
|
|
1355
|
+
hook_result = await hook_manager.run_pre_compact_async(
|
|
1356
|
+
trigger="manual", custom_instructions=custom_instructions
|
|
1357
|
+
)
|
|
1358
|
+
if hook_result.should_block or not hook_result.should_continue:
|
|
1359
|
+
reason = (
|
|
1360
|
+
hook_result.block_reason
|
|
1361
|
+
or hook_result.stop_reason
|
|
1362
|
+
or "Compaction blocked by hook."
|
|
1363
|
+
)
|
|
1364
|
+
self.console.print(f"[yellow]{escape(str(reason))}[/yellow]")
|
|
1365
|
+
return
|
|
1366
|
+
hook_contexts = self._collect_hook_contexts(hook_result)
|
|
1367
|
+
if hook_contexts:
|
|
1368
|
+
hook_instructions = "\n\n".join(hook_contexts)
|
|
1369
|
+
except (OSError, RuntimeError, ConnectionError, ValueError, TypeError) as exc:
|
|
1370
|
+
logger.warning(
|
|
1371
|
+
"[ui] PreCompact hook failed: %s: %s",
|
|
1372
|
+
type(exc).__name__,
|
|
1373
|
+
exc,
|
|
1374
|
+
extra={"session_id": self.session_id},
|
|
1375
|
+
)
|
|
1376
|
+
|
|
1377
|
+
merged_instructions = custom_instructions.strip()
|
|
1378
|
+
if hook_instructions:
|
|
1379
|
+
merged_instructions = (
|
|
1380
|
+
f"{merged_instructions}\n\n{hook_instructions}".strip()
|
|
1381
|
+
if merged_instructions
|
|
1382
|
+
else hook_instructions
|
|
1383
|
+
)
|
|
1076
1384
|
try:
|
|
1077
1385
|
spinner.start()
|
|
1078
1386
|
result = await compact_conversation(
|
|
1079
1387
|
self.conversation_messages,
|
|
1080
|
-
|
|
1388
|
+
merged_instructions,
|
|
1081
1389
|
protocol=protocol,
|
|
1082
1390
|
)
|
|
1083
1391
|
except Exception as exc:
|
|
1084
1392
|
import traceback
|
|
1393
|
+
|
|
1085
1394
|
self.console.print(f"[red]Error during compaction: {escape(str(exc))}[/red]")
|
|
1086
1395
|
self.console.print(f"[dim red]{traceback.format_exc()}[/dim red]")
|
|
1087
1396
|
return
|
|
@@ -1095,6 +1404,7 @@ class RichUI:
|
|
|
1095
1404
|
f"[green]✓ Conversation compacted[/green] "
|
|
1096
1405
|
f"(saved ~{result.tokens_saved} tokens). Use /resume to restore full history."
|
|
1097
1406
|
)
|
|
1407
|
+
await self._run_session_start_async("compact")
|
|
1098
1408
|
elif isinstance(result, CompactionError):
|
|
1099
1409
|
self.console.print(f"[red]{escape(result.message)}[/red]")
|
|
1100
1410
|
|
|
@@ -1110,17 +1420,23 @@ def check_onboarding_rich() -> bool:
|
|
|
1110
1420
|
if config.has_completed_onboarding:
|
|
1111
1421
|
return True
|
|
1112
1422
|
|
|
1113
|
-
# Use
|
|
1114
|
-
from ripperdoc.cli.
|
|
1423
|
+
# Use the wizard onboarding
|
|
1424
|
+
from ripperdoc.cli.ui.wizard import check_onboarding
|
|
1115
1425
|
|
|
1116
1426
|
return check_onboarding()
|
|
1117
1427
|
|
|
1118
1428
|
|
|
1119
1429
|
def main_rich(
|
|
1120
|
-
|
|
1430
|
+
yolo_mode: bool = False,
|
|
1121
1431
|
verbose: bool = False,
|
|
1432
|
+
show_full_thinking: Optional[bool] = None,
|
|
1122
1433
|
session_id: Optional[str] = None,
|
|
1123
1434
|
log_file_path: Optional[Path] = None,
|
|
1435
|
+
allowed_tools: Optional[List[str]] = None,
|
|
1436
|
+
custom_system_prompt: Optional[str] = None,
|
|
1437
|
+
append_system_prompt: Optional[str] = None,
|
|
1438
|
+
model: Optional[str] = None,
|
|
1439
|
+
resume_messages: Optional[List[Any]] = None,
|
|
1124
1440
|
) -> None:
|
|
1125
1441
|
"""Main entry point for Rich interface."""
|
|
1126
1442
|
|
|
@@ -1130,10 +1446,16 @@ def main_rich(
|
|
|
1130
1446
|
|
|
1131
1447
|
# Run the Rich UI
|
|
1132
1448
|
ui = RichUI(
|
|
1133
|
-
|
|
1449
|
+
yolo_mode=yolo_mode,
|
|
1134
1450
|
verbose=verbose,
|
|
1451
|
+
show_full_thinking=show_full_thinking,
|
|
1135
1452
|
session_id=session_id,
|
|
1136
1453
|
log_file_path=log_file_path,
|
|
1454
|
+
allowed_tools=allowed_tools,
|
|
1455
|
+
custom_system_prompt=custom_system_prompt,
|
|
1456
|
+
append_system_prompt=append_system_prompt,
|
|
1457
|
+
model=model,
|
|
1458
|
+
resume_messages=resume_messages,
|
|
1137
1459
|
)
|
|
1138
1460
|
ui.run()
|
|
1139
1461
|
|