code-puppy 0.0.302__py3-none-any.whl → 0.0.335__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.
- code_puppy/agents/base_agent.py +343 -35
- code_puppy/chatgpt_codex_client.py +283 -0
- code_puppy/cli_runner.py +898 -0
- code_puppy/command_line/add_model_menu.py +23 -1
- code_puppy/command_line/autosave_menu.py +271 -35
- code_puppy/command_line/colors_menu.py +520 -0
- code_puppy/command_line/command_handler.py +8 -2
- code_puppy/command_line/config_commands.py +82 -10
- code_puppy/command_line/core_commands.py +70 -7
- code_puppy/command_line/diff_menu.py +5 -0
- code_puppy/command_line/mcp/custom_server_form.py +4 -0
- code_puppy/command_line/mcp/edit_command.py +3 -1
- code_puppy/command_line/mcp/handler.py +7 -2
- code_puppy/command_line/mcp/install_command.py +8 -3
- code_puppy/command_line/mcp/install_menu.py +5 -1
- code_puppy/command_line/mcp/logs_command.py +173 -64
- code_puppy/command_line/mcp/restart_command.py +7 -2
- code_puppy/command_line/mcp/search_command.py +10 -4
- code_puppy/command_line/mcp/start_all_command.py +16 -6
- code_puppy/command_line/mcp/start_command.py +3 -1
- code_puppy/command_line/mcp/status_command.py +2 -1
- code_puppy/command_line/mcp/stop_all_command.py +5 -1
- code_puppy/command_line/mcp/stop_command.py +3 -1
- code_puppy/command_line/mcp/wizard_utils.py +10 -4
- code_puppy/command_line/model_settings_menu.py +58 -7
- code_puppy/command_line/motd.py +13 -7
- code_puppy/command_line/onboarding_slides.py +180 -0
- code_puppy/command_line/onboarding_wizard.py +340 -0
- code_puppy/command_line/prompt_toolkit_completion.py +16 -2
- code_puppy/command_line/session_commands.py +11 -4
- code_puppy/config.py +106 -17
- code_puppy/http_utils.py +155 -196
- code_puppy/keymap.py +8 -0
- code_puppy/main.py +5 -828
- code_puppy/mcp_/__init__.py +17 -0
- code_puppy/mcp_/blocking_startup.py +61 -32
- code_puppy/mcp_/config_wizard.py +5 -1
- code_puppy/mcp_/managed_server.py +23 -3
- code_puppy/mcp_/manager.py +65 -0
- code_puppy/mcp_/mcp_logs.py +224 -0
- code_puppy/messaging/__init__.py +20 -4
- code_puppy/messaging/bus.py +64 -0
- code_puppy/messaging/markdown_patches.py +57 -0
- code_puppy/messaging/messages.py +16 -0
- code_puppy/messaging/renderers.py +21 -9
- code_puppy/messaging/rich_renderer.py +113 -67
- code_puppy/messaging/spinner/console_spinner.py +34 -0
- code_puppy/model_factory.py +271 -45
- code_puppy/model_utils.py +57 -48
- code_puppy/models.json +21 -7
- code_puppy/plugins/__init__.py +12 -0
- code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
- code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
- code_puppy/plugins/antigravity_oauth/antigravity_model.py +612 -0
- code_puppy/plugins/antigravity_oauth/config.py +42 -0
- code_puppy/plugins/antigravity_oauth/constants.py +136 -0
- code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
- code_puppy/plugins/antigravity_oauth/register_callbacks.py +406 -0
- code_puppy/plugins/antigravity_oauth/storage.py +271 -0
- code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
- code_puppy/plugins/antigravity_oauth/token.py +167 -0
- code_puppy/plugins/antigravity_oauth/transport.py +595 -0
- code_puppy/plugins/antigravity_oauth/utils.py +169 -0
- code_puppy/plugins/chatgpt_oauth/config.py +5 -1
- code_puppy/plugins/chatgpt_oauth/oauth_flow.py +5 -6
- code_puppy/plugins/chatgpt_oauth/register_callbacks.py +5 -3
- code_puppy/plugins/chatgpt_oauth/test_plugin.py +26 -11
- code_puppy/plugins/chatgpt_oauth/utils.py +180 -65
- code_puppy/plugins/claude_code_oauth/register_callbacks.py +30 -0
- code_puppy/plugins/claude_code_oauth/utils.py +1 -0
- code_puppy/plugins/shell_safety/agent_shell_safety.py +1 -118
- code_puppy/plugins/shell_safety/register_callbacks.py +44 -3
- code_puppy/prompts/codex_system_prompt.md +310 -0
- code_puppy/pydantic_patches.py +131 -0
- code_puppy/reopenable_async_client.py +8 -8
- code_puppy/terminal_utils.py +291 -0
- code_puppy/tools/agent_tools.py +34 -9
- code_puppy/tools/command_runner.py +344 -27
- code_puppy/tools/file_operations.py +33 -45
- code_puppy/uvx_detection.py +242 -0
- {code_puppy-0.0.302.data → code_puppy-0.0.335.data}/data/code_puppy/models.json +21 -7
- {code_puppy-0.0.302.dist-info → code_puppy-0.0.335.dist-info}/METADATA +30 -1
- {code_puppy-0.0.302.dist-info → code_puppy-0.0.335.dist-info}/RECORD +87 -64
- {code_puppy-0.0.302.data → code_puppy-0.0.335.data}/data/code_puppy/models_dev_api.json +0 -0
- {code_puppy-0.0.302.dist-info → code_puppy-0.0.335.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.302.dist-info → code_puppy-0.0.335.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.302.dist-info → code_puppy-0.0.335.dist-info}/licenses/LICENSE +0 -0
code_puppy/messaging/bus.py
CHANGED
|
@@ -86,6 +86,9 @@ class MessageBus:
|
|
|
86
86
|
# Request/Response correlation: prompt_id → Future (for async usage)
|
|
87
87
|
self._pending_requests: Dict[str, asyncio.Future[Any]] = {}
|
|
88
88
|
|
|
89
|
+
# Session context for multi-agent tracking
|
|
90
|
+
self._current_session_id: Optional[str] = None
|
|
91
|
+
|
|
89
92
|
# =========================================================================
|
|
90
93
|
# Outgoing Messages (Agent → UI)
|
|
91
94
|
# =========================================================================
|
|
@@ -95,11 +98,16 @@ class MessageBus:
|
|
|
95
98
|
|
|
96
99
|
Thread-safe. Can be called from sync or async context.
|
|
97
100
|
If no renderer is active, messages are buffered for later.
|
|
101
|
+
Auto-tags message with current session_id if not already set.
|
|
98
102
|
|
|
99
103
|
Args:
|
|
100
104
|
message: The message to emit.
|
|
101
105
|
"""
|
|
106
|
+
# Auto-tag message with current session if not already set
|
|
102
107
|
with self._lock:
|
|
108
|
+
if message.session_id is None and self._current_session_id is not None:
|
|
109
|
+
message.session_id = self._current_session_id
|
|
110
|
+
|
|
103
111
|
if not self._has_active_renderer:
|
|
104
112
|
self._startup_buffer.append(message)
|
|
105
113
|
return
|
|
@@ -151,6 +159,43 @@ class MessageBus:
|
|
|
151
159
|
"""Emit a DEBUG level text message."""
|
|
152
160
|
self.emit_text(MessageLevel.DEBUG, text)
|
|
153
161
|
|
|
162
|
+
def emit_shell_line(self, line: str, stream: str = "stdout") -> None:
|
|
163
|
+
"""Emit a shell output line with ANSI preservation.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
line: The output line (may contain ANSI codes).
|
|
167
|
+
stream: Which stream this came from ("stdout" or "stderr").
|
|
168
|
+
"""
|
|
169
|
+
from .messages import ShellLineMessage
|
|
170
|
+
|
|
171
|
+
message = ShellLineMessage(line=line, stream=stream) # type: ignore[arg-type]
|
|
172
|
+
self.emit(message)
|
|
173
|
+
|
|
174
|
+
# =========================================================================
|
|
175
|
+
# Session Context (Multi-Agent Tracking)
|
|
176
|
+
# =========================================================================
|
|
177
|
+
|
|
178
|
+
def set_session_context(self, session_id: Optional[str]) -> None:
|
|
179
|
+
"""Set the current session context for auto-tagging messages.
|
|
180
|
+
|
|
181
|
+
When set, all messages emitted via emit() will be automatically tagged
|
|
182
|
+
with this session_id unless they already have one set.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
session_id: The session ID to tag messages with, or None to clear.
|
|
186
|
+
"""
|
|
187
|
+
with self._lock:
|
|
188
|
+
self._current_session_id = session_id
|
|
189
|
+
|
|
190
|
+
def get_session_context(self) -> Optional[str]:
|
|
191
|
+
"""Get the current session context.
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
The current session_id, or None if not set.
|
|
195
|
+
"""
|
|
196
|
+
with self._lock:
|
|
197
|
+
return self._current_session_id
|
|
198
|
+
|
|
154
199
|
# =========================================================================
|
|
155
200
|
# User Input Requests (Agent waits for UI response)
|
|
156
201
|
# =========================================================================
|
|
@@ -526,6 +571,21 @@ def emit_debug(text: str) -> None:
|
|
|
526
571
|
get_message_bus().emit_debug(text)
|
|
527
572
|
|
|
528
573
|
|
|
574
|
+
def emit_shell_line(line: str, stream: str = "stdout") -> None:
|
|
575
|
+
"""Emit a shell output line with ANSI preservation."""
|
|
576
|
+
get_message_bus().emit_shell_line(line, stream)
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def set_session_context(session_id: Optional[str]) -> None:
|
|
580
|
+
"""Set the session context on the global bus."""
|
|
581
|
+
get_message_bus().set_session_context(session_id)
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
def get_session_context() -> Optional[str]:
|
|
585
|
+
"""Get the session context from the global bus."""
|
|
586
|
+
return get_message_bus().get_session_context()
|
|
587
|
+
|
|
588
|
+
|
|
529
589
|
# =============================================================================
|
|
530
590
|
# Export all public symbols
|
|
531
591
|
# =============================================================================
|
|
@@ -543,4 +603,8 @@ __all__ = [
|
|
|
543
603
|
"emit_error",
|
|
544
604
|
"emit_success",
|
|
545
605
|
"emit_debug",
|
|
606
|
+
"emit_shell_line",
|
|
607
|
+
# Session context
|
|
608
|
+
"set_session_context",
|
|
609
|
+
"get_session_context",
|
|
546
610
|
]
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Patches for Rich's Markdown rendering.
|
|
2
|
+
|
|
3
|
+
This module provides customizations to Rich's default Markdown rendering,
|
|
4
|
+
particularly for header justification which is hardcoded to center in Rich.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from rich import box
|
|
8
|
+
from rich.markdown import Heading, Markdown
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
from rich.text import Text
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class LeftJustifiedHeading(Heading):
|
|
14
|
+
"""A heading that left-justifies text instead of centering.
|
|
15
|
+
|
|
16
|
+
Rich's default Heading class hardcodes `text.justify = 'center'`,
|
|
17
|
+
which can look odd in a CLI context. This subclass overrides that
|
|
18
|
+
to use left justification instead.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __rich_console__(self, console, options):
|
|
22
|
+
"""Render the heading with left justification."""
|
|
23
|
+
text = self.text
|
|
24
|
+
text.justify = "left" # Override Rich's default 'center'
|
|
25
|
+
|
|
26
|
+
if self.tag == "h1":
|
|
27
|
+
# Draw a border around h1s (same as Rich default)
|
|
28
|
+
yield Panel(
|
|
29
|
+
text,
|
|
30
|
+
box=box.HEAVY,
|
|
31
|
+
style="markdown.h1.border",
|
|
32
|
+
)
|
|
33
|
+
else:
|
|
34
|
+
# Styled text for h2 and beyond (same as Rich default)
|
|
35
|
+
if self.tag == "h2":
|
|
36
|
+
yield Text("")
|
|
37
|
+
yield text
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
_patched = False
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def patch_markdown_headings():
|
|
44
|
+
"""Patch Rich's Markdown to use left-justified headings.
|
|
45
|
+
|
|
46
|
+
This function is idempotent - calling it multiple times has no effect
|
|
47
|
+
after the first call.
|
|
48
|
+
"""
|
|
49
|
+
global _patched
|
|
50
|
+
if _patched:
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
Markdown.elements["heading_open"] = LeftJustifiedHeading
|
|
54
|
+
_patched = True
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
__all__ = ["patch_markdown_headings", "LeftJustifiedHeading"]
|
code_puppy/messaging/messages.py
CHANGED
|
@@ -56,6 +56,10 @@ class BaseMessage(BaseModel):
|
|
|
56
56
|
category: MessageCategory = Field(
|
|
57
57
|
description="Category for routing and rendering decisions"
|
|
58
58
|
)
|
|
59
|
+
session_id: Optional[str] = Field(
|
|
60
|
+
default=None,
|
|
61
|
+
description="Session ID of the agent that emitted this message (for multi-agent tracking)",
|
|
62
|
+
)
|
|
59
63
|
|
|
60
64
|
model_config = {"frozen": False, "extra": "forbid"}
|
|
61
65
|
|
|
@@ -207,6 +211,16 @@ class ShellStartMessage(BaseMessage):
|
|
|
207
211
|
timeout: int = Field(default=60, description="Timeout in seconds")
|
|
208
212
|
|
|
209
213
|
|
|
214
|
+
class ShellLineMessage(BaseMessage):
|
|
215
|
+
"""A single line of shell command output with ANSI preservation."""
|
|
216
|
+
|
|
217
|
+
category: MessageCategory = MessageCategory.TOOL_OUTPUT
|
|
218
|
+
line: str = Field(description="The output line (may contain ANSI codes)")
|
|
219
|
+
stream: Literal["stdout", "stderr"] = Field(
|
|
220
|
+
default="stdout", description="Which output stream this line came from"
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
|
|
210
224
|
class ShellOutputMessage(BaseMessage):
|
|
211
225
|
"""Output from a shell command execution with stdout, stderr, and timing."""
|
|
212
226
|
|
|
@@ -394,6 +408,7 @@ AnyMessage = Union[
|
|
|
394
408
|
GrepResultMessage,
|
|
395
409
|
DiffMessage,
|
|
396
410
|
ShellStartMessage,
|
|
411
|
+
ShellLineMessage,
|
|
397
412
|
ShellOutputMessage,
|
|
398
413
|
AgentReasoningMessage,
|
|
399
414
|
AgentResponseMessage,
|
|
@@ -433,6 +448,7 @@ __all__ = [
|
|
|
433
448
|
"DiffMessage",
|
|
434
449
|
# Shell
|
|
435
450
|
"ShellStartMessage",
|
|
451
|
+
"ShellLineMessage",
|
|
436
452
|
"ShellOutputMessage",
|
|
437
453
|
# Agent
|
|
438
454
|
"AgentReasoningMessage",
|
|
@@ -12,6 +12,7 @@ from typing import Optional
|
|
|
12
12
|
|
|
13
13
|
from rich.console import Console
|
|
14
14
|
from rich.markdown import Markdown
|
|
15
|
+
from rich.markup import escape as escape_rich_markup
|
|
15
16
|
|
|
16
17
|
from .message_queue import MessageQueue, MessageType, UIMessage
|
|
17
18
|
|
|
@@ -126,11 +127,15 @@ class InteractiveRenderer(MessageRenderer):
|
|
|
126
127
|
self.console.print(markdown)
|
|
127
128
|
except Exception:
|
|
128
129
|
# Fallback to plain text if markdown parsing fails
|
|
129
|
-
|
|
130
|
+
safe_content = escape_rich_markup(message.content)
|
|
131
|
+
self.console.print(safe_content)
|
|
130
132
|
elif style:
|
|
131
|
-
|
|
133
|
+
# Escape Rich markup to prevent crashes from malformed tags
|
|
134
|
+
safe_content = escape_rich_markup(message.content)
|
|
135
|
+
self.console.print(safe_content, style=style)
|
|
132
136
|
else:
|
|
133
|
-
|
|
137
|
+
safe_content = escape_rich_markup(message.content)
|
|
138
|
+
self.console.print(safe_content)
|
|
134
139
|
else:
|
|
135
140
|
# For complex Rich objects (Tables, Markdown, Text, etc.)
|
|
136
141
|
self.console.print(message.content)
|
|
@@ -145,7 +150,8 @@ class InteractiveRenderer(MessageRenderer):
|
|
|
145
150
|
# This renderer is not currently used in practice, but if it were:
|
|
146
151
|
# We would need async input handling here
|
|
147
152
|
# For now, just render as a system message
|
|
148
|
-
|
|
153
|
+
safe_content = escape_rich_markup(str(message.content))
|
|
154
|
+
self.console.print(f"[bold cyan]INPUT REQUESTED:[/bold cyan] {safe_content}")
|
|
149
155
|
if hasattr(self.console.file, "flush"):
|
|
150
156
|
self.console.file.flush()
|
|
151
157
|
|
|
@@ -253,11 +259,16 @@ class SynchronousInteractiveRenderer:
|
|
|
253
259
|
self.console.print(markdown)
|
|
254
260
|
except Exception:
|
|
255
261
|
# Fallback to plain text if markdown parsing fails
|
|
256
|
-
|
|
262
|
+
safe_content = escape_rich_markup(message.content)
|
|
263
|
+
self.console.print(safe_content)
|
|
257
264
|
elif style:
|
|
258
|
-
|
|
265
|
+
# Escape Rich markup to prevent crashes from malformed tags
|
|
266
|
+
# in shell output or other user-provided content
|
|
267
|
+
safe_content = escape_rich_markup(message.content)
|
|
268
|
+
self.console.print(safe_content, style=style)
|
|
259
269
|
else:
|
|
260
|
-
|
|
270
|
+
safe_content = escape_rich_markup(message.content)
|
|
271
|
+
self.console.print(safe_content)
|
|
261
272
|
else:
|
|
262
273
|
# For complex Rich objects (Tables, Markdown, Text, etc.)
|
|
263
274
|
self.console.print(message.content)
|
|
@@ -276,8 +287,9 @@ class SynchronousInteractiveRenderer:
|
|
|
276
287
|
)
|
|
277
288
|
return
|
|
278
289
|
|
|
279
|
-
# Display the prompt
|
|
280
|
-
|
|
290
|
+
# Display the prompt - escape to prevent markup injection
|
|
291
|
+
safe_content = escape_rich_markup(str(message.content))
|
|
292
|
+
self.console.print(f"[bold cyan]{safe_content}[/bold cyan]")
|
|
281
293
|
if hasattr(self.console.file, "flush"):
|
|
282
294
|
self.console.file.flush()
|
|
283
295
|
|
|
@@ -11,6 +11,7 @@ from typing import Dict, Optional, Protocol, runtime_checkable
|
|
|
11
11
|
|
|
12
12
|
from rich.console import Console
|
|
13
13
|
from rich.markdown import Markdown
|
|
14
|
+
from rich.markup import escape as escape_rich_markup
|
|
14
15
|
from rich.panel import Panel
|
|
15
16
|
from rich.rule import Rule
|
|
16
17
|
|
|
@@ -37,6 +38,7 @@ from .messages import (
|
|
|
37
38
|
GrepResultMessage,
|
|
38
39
|
MessageLevel,
|
|
39
40
|
SelectionRequest,
|
|
41
|
+
ShellLineMessage,
|
|
40
42
|
ShellOutputMessage,
|
|
41
43
|
ShellStartMessage,
|
|
42
44
|
SpinnerControl,
|
|
@@ -131,6 +133,32 @@ class RichConsoleRenderer:
|
|
|
131
133
|
"""Get the Rich console."""
|
|
132
134
|
return self._console
|
|
133
135
|
|
|
136
|
+
def _get_banner_color(self, banner_name: str) -> str:
|
|
137
|
+
"""Get the configured color for a banner.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
banner_name: The banner identifier (e.g., 'thinking', 'shell_command')
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
Rich color name for the banner background
|
|
144
|
+
"""
|
|
145
|
+
from code_puppy.config import get_banner_color
|
|
146
|
+
|
|
147
|
+
return get_banner_color(banner_name)
|
|
148
|
+
|
|
149
|
+
def _format_banner(self, banner_name: str, text: str) -> str:
|
|
150
|
+
"""Format a banner with its configured color.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
banner_name: The banner identifier
|
|
154
|
+
text: The banner text
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
Rich markup string for the banner
|
|
158
|
+
"""
|
|
159
|
+
color = self._get_banner_color(banner_name)
|
|
160
|
+
return f"[bold white on {color}] {text} [/bold white on {color}]"
|
|
161
|
+
|
|
134
162
|
# =========================================================================
|
|
135
163
|
# Lifecycle (Synchronous - for compatibility with main.py)
|
|
136
164
|
# =========================================================================
|
|
@@ -187,7 +215,9 @@ class RichConsoleRenderer:
|
|
|
187
215
|
self._do_render(message)
|
|
188
216
|
except Exception as e:
|
|
189
217
|
# Don't let rendering errors crash the loop
|
|
190
|
-
|
|
218
|
+
# Escape the error message to prevent nested markup errors
|
|
219
|
+
safe_error = escape_rich_markup(str(e))
|
|
220
|
+
self._console.print(f"[dim red]Render error: {safe_error}[/dim red]")
|
|
191
221
|
|
|
192
222
|
# =========================================================================
|
|
193
223
|
# Async Lifecycle (for future async-first usage)
|
|
@@ -233,12 +263,15 @@ class RichConsoleRenderer:
|
|
|
233
263
|
self._render_diff(message)
|
|
234
264
|
elif isinstance(message, ShellStartMessage):
|
|
235
265
|
self._render_shell_start(message)
|
|
266
|
+
elif isinstance(message, ShellLineMessage):
|
|
267
|
+
self._render_shell_line(message)
|
|
236
268
|
elif isinstance(message, ShellOutputMessage):
|
|
237
269
|
self._render_shell_output(message)
|
|
238
270
|
elif isinstance(message, AgentReasoningMessage):
|
|
239
271
|
self._render_agent_reasoning(message)
|
|
240
272
|
elif isinstance(message, AgentResponseMessage):
|
|
241
|
-
|
|
273
|
+
# Skip rendering - we now stream agent responses via event_stream_handler
|
|
274
|
+
pass
|
|
242
275
|
elif isinstance(message, SubAgentInvocationMessage):
|
|
243
276
|
self._render_subagent_invocation(message)
|
|
244
277
|
elif isinstance(message, SubAgentResponseMessage):
|
|
@@ -282,7 +315,12 @@ class RichConsoleRenderer:
|
|
|
282
315
|
# =========================================================================
|
|
283
316
|
|
|
284
317
|
def _render_text(self, msg: TextMessage) -> None:
|
|
285
|
-
"""Render a text message with appropriate styling.
|
|
318
|
+
"""Render a text message with appropriate styling.
|
|
319
|
+
|
|
320
|
+
Text is escaped to prevent Rich markup injection which could crash
|
|
321
|
+
the renderer if malformed tags are present in shell output or other
|
|
322
|
+
user-provided content.
|
|
323
|
+
"""
|
|
286
324
|
style = self._styles.get(msg.level, "white")
|
|
287
325
|
|
|
288
326
|
# Make version messages dim
|
|
@@ -290,7 +328,9 @@ class RichConsoleRenderer:
|
|
|
290
328
|
style = "dim"
|
|
291
329
|
|
|
292
330
|
prefix = self._get_level_prefix(msg.level)
|
|
293
|
-
|
|
331
|
+
# Escape Rich markup to prevent crashes from malformed tags
|
|
332
|
+
safe_text = escape_rich_markup(msg.text)
|
|
333
|
+
self._console.print(f"{prefix}{safe_text}", style=style)
|
|
294
334
|
|
|
295
335
|
def _get_level_prefix(self, level: MessageLevel) -> str:
|
|
296
336
|
"""Get a prefix icon for the message level."""
|
|
@@ -311,8 +351,9 @@ class RichConsoleRenderer:
|
|
|
311
351
|
"""Render a directory listing matching the old Rich-formatted output."""
|
|
312
352
|
# Header on single line
|
|
313
353
|
rec_flag = f"(recursive={msg.recursive})"
|
|
354
|
+
banner = self._format_banner("directory_listing", "DIRECTORY LISTING")
|
|
314
355
|
self._console.print(
|
|
315
|
-
f"\n
|
|
356
|
+
f"\n{banner} "
|
|
316
357
|
f"📂 [bold cyan]{msg.directory}[/bold cyan] [dim]{rec_flag}[/dim]\n"
|
|
317
358
|
)
|
|
318
359
|
|
|
@@ -362,26 +403,25 @@ class RichConsoleRenderer:
|
|
|
362
403
|
line_info = f" [dim](lines {msg.start_line}-{end_line})[/dim]"
|
|
363
404
|
|
|
364
405
|
# Just print the header - content is for LLM only
|
|
406
|
+
banner = self._format_banner("read_file", "READ FILE")
|
|
365
407
|
self._console.print(
|
|
366
|
-
f"\n[bold
|
|
367
|
-
f"📂 [bold cyan]{msg.path}[/bold cyan]{line_info}"
|
|
408
|
+
f"\n{banner} 📂 [bold cyan]{msg.path}[/bold cyan]{line_info}"
|
|
368
409
|
)
|
|
369
410
|
|
|
370
411
|
def _render_grep_result(self, msg: GrepResultMessage) -> None:
|
|
371
412
|
"""Render grep results grouped by file matching old format."""
|
|
372
413
|
import re
|
|
373
414
|
|
|
374
|
-
# Header
|
|
415
|
+
# Header
|
|
416
|
+
banner = self._format_banner("grep", "GREP")
|
|
375
417
|
self._console.print(
|
|
376
|
-
f"\n
|
|
377
|
-
f"📂 [bold cyan]{msg.directory}[/bold cyan] "
|
|
378
|
-
f"[dim]for '{msg.search_term}'[/dim]"
|
|
418
|
+
f"\n{banner} 📂 [dim]{msg.directory} for '{msg.search_term}'[/dim]"
|
|
379
419
|
)
|
|
380
420
|
|
|
381
421
|
if not msg.matches:
|
|
382
422
|
self._console.print(
|
|
383
|
-
f"[
|
|
384
|
-
f"in {msg.directory}[/
|
|
423
|
+
f"[dim]No matches found for '{msg.search_term}' "
|
|
424
|
+
f"in {msg.directory}[/dim]"
|
|
385
425
|
)
|
|
386
426
|
return
|
|
387
427
|
|
|
@@ -397,8 +437,7 @@ class RichConsoleRenderer:
|
|
|
397
437
|
file_matches = by_file[file_path]
|
|
398
438
|
match_word = "match" if len(file_matches) == 1 else "matches"
|
|
399
439
|
self._console.print(
|
|
400
|
-
f"\n[
|
|
401
|
-
f"[dim]({len(file_matches)} {match_word})[/dim]"
|
|
440
|
+
f"\n[dim]📄 {file_path} ({len(file_matches)} {match_word})[/dim]"
|
|
402
441
|
)
|
|
403
442
|
|
|
404
443
|
# Show each match with line number and content
|
|
@@ -414,7 +453,7 @@ class RichConsoleRenderer:
|
|
|
414
453
|
if search_term and not search_term.startswith("-"):
|
|
415
454
|
highlighted_line = re.sub(
|
|
416
455
|
f"({re.escape(search_term)})",
|
|
417
|
-
r"[bold yellow
|
|
456
|
+
r"[bold yellow]\1[/bold yellow]",
|
|
418
457
|
line,
|
|
419
458
|
flags=re.IGNORECASE,
|
|
420
459
|
)
|
|
@@ -422,9 +461,7 @@ class RichConsoleRenderer:
|
|
|
422
461
|
highlighted_line = line
|
|
423
462
|
|
|
424
463
|
ln = match.line_number
|
|
425
|
-
self._console.print(
|
|
426
|
-
f" [bold cyan]{ln:4d}[/bold cyan] │ {highlighted_line}"
|
|
427
|
-
)
|
|
464
|
+
self._console.print(f" [dim]{ln:4d}[/dim] │ {highlighted_line}")
|
|
428
465
|
else:
|
|
429
466
|
# Concise mode (default): Show only file summaries
|
|
430
467
|
self._console.print("")
|
|
@@ -435,15 +472,18 @@ class RichConsoleRenderer:
|
|
|
435
472
|
f"[dim]📄 {file_path} ({len(file_matches)} {match_word})[/dim]"
|
|
436
473
|
)
|
|
437
474
|
|
|
438
|
-
# Summary
|
|
475
|
+
# Summary - subtle
|
|
439
476
|
match_word = "match" if msg.total_matches == 1 else "matches"
|
|
440
477
|
file_word = "file" if len(by_file) == 1 else "files"
|
|
441
478
|
num_files = len(by_file)
|
|
442
479
|
self._console.print(
|
|
443
|
-
f"[
|
|
444
|
-
f"across
|
|
480
|
+
f"[dim]Found {msg.total_matches} {match_word} "
|
|
481
|
+
f"across {num_files} {file_word}[/dim]"
|
|
445
482
|
)
|
|
446
483
|
|
|
484
|
+
# Trailing newline for spinner separation
|
|
485
|
+
self._console.print()
|
|
486
|
+
|
|
447
487
|
# =========================================================================
|
|
448
488
|
# Diff
|
|
449
489
|
# =========================================================================
|
|
@@ -457,8 +497,9 @@ class RichConsoleRenderer:
|
|
|
457
497
|
op_color = op_colors.get(msg.operation, "white")
|
|
458
498
|
|
|
459
499
|
# Header on single line
|
|
500
|
+
banner = self._format_banner("edit_file", "EDIT FILE")
|
|
460
501
|
self._console.print(
|
|
461
|
-
f"\n
|
|
502
|
+
f"\n{banner} "
|
|
462
503
|
f"{icon} [{op_color}]{msg.operation.upper()}[/{op_color}] "
|
|
463
504
|
f"[bold cyan]{msg.path}[/bold cyan]"
|
|
464
505
|
)
|
|
@@ -474,7 +515,12 @@ class RichConsoleRenderer:
|
|
|
474
515
|
elif line.type == "remove":
|
|
475
516
|
diff_text_lines.append(f"-{line.content}")
|
|
476
517
|
else: # context
|
|
477
|
-
|
|
518
|
+
# Don't add space prefix to diff headers - they need to be preserved
|
|
519
|
+
# exactly for syntax highlighting to detect the file extension
|
|
520
|
+
if line.content.startswith(("---", "+++", "@@", "diff ", "index ")):
|
|
521
|
+
diff_text_lines.append(line.content)
|
|
522
|
+
else:
|
|
523
|
+
diff_text_lines.append(f" {line.content}")
|
|
478
524
|
|
|
479
525
|
diff_text = "\n".join(diff_text_lines)
|
|
480
526
|
|
|
@@ -488,43 +534,39 @@ class RichConsoleRenderer:
|
|
|
488
534
|
|
|
489
535
|
def _render_shell_start(self, msg: ShellStartMessage) -> None:
|
|
490
536
|
"""Render shell command start notification."""
|
|
537
|
+
# Escape command to prevent Rich markup injection
|
|
538
|
+
safe_command = escape_rich_markup(msg.command)
|
|
491
539
|
# Header showing command is starting
|
|
492
|
-
self.
|
|
493
|
-
|
|
494
|
-
f"🚀 [bold green]$ {msg.command}[/bold green]"
|
|
495
|
-
)
|
|
540
|
+
banner = self._format_banner("shell_command", "SHELL COMMAND")
|
|
541
|
+
self._console.print(f"\n{banner} 🚀 [dim]$ {safe_command}[/dim]")
|
|
496
542
|
|
|
497
543
|
# Show working directory if specified
|
|
498
544
|
if msg.cwd:
|
|
499
|
-
|
|
545
|
+
safe_cwd = escape_rich_markup(msg.cwd)
|
|
546
|
+
self._console.print(f"[dim]📂 Working directory: {safe_cwd}[/dim]")
|
|
500
547
|
|
|
501
548
|
# Show timeout
|
|
502
549
|
self._console.print(f"[dim]⏱ Timeout: {msg.timeout}s[/dim]")
|
|
503
550
|
|
|
504
|
-
def
|
|
505
|
-
"""Render shell
|
|
506
|
-
|
|
507
|
-
self._console.print(
|
|
508
|
-
f"\n[bold white on blue] SHELL COMMAND [/bold white on blue] "
|
|
509
|
-
f"📂 [bold green]$ {msg.command}[/bold green]"
|
|
510
|
-
)
|
|
551
|
+
def _render_shell_line(self, msg: ShellLineMessage) -> None:
|
|
552
|
+
"""Render shell output line preserving ANSI codes."""
|
|
553
|
+
from rich.text import Text
|
|
511
554
|
|
|
512
|
-
#
|
|
513
|
-
|
|
514
|
-
|
|
555
|
+
# Use Text.from_ansi() to parse ANSI codes into Rich styling
|
|
556
|
+
# This preserves colors while still being safe
|
|
557
|
+
text = Text.from_ansi(msg.line)
|
|
515
558
|
|
|
516
|
-
#
|
|
517
|
-
|
|
518
|
-
self._console.print(f"[red]{msg.stderr}[/red]")
|
|
559
|
+
# Make all shell output dim to reduce visual noise
|
|
560
|
+
self._console.print(text, style="dim")
|
|
519
561
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
562
|
+
def _render_shell_output(self, msg: ShellOutputMessage) -> None:
|
|
563
|
+
"""Render shell command output - just a trailing newline for spinner separation.
|
|
564
|
+
|
|
565
|
+
Shell command results are already returned to the LLM via tool responses,
|
|
566
|
+
so we don't need to clutter the UI with redundant output.
|
|
567
|
+
"""
|
|
568
|
+
# Just print trailing newline for spinner separation
|
|
569
|
+
self._console.print()
|
|
528
570
|
|
|
529
571
|
# =========================================================================
|
|
530
572
|
# Agent Messages
|
|
@@ -533,9 +575,8 @@ class RichConsoleRenderer:
|
|
|
533
575
|
def _render_agent_reasoning(self, msg: AgentReasoningMessage) -> None:
|
|
534
576
|
"""Render agent reasoning matching old format."""
|
|
535
577
|
# Header matching old format
|
|
536
|
-
self.
|
|
537
|
-
|
|
538
|
-
)
|
|
578
|
+
banner = self._format_banner("agent_reasoning", "AGENT REASONING")
|
|
579
|
+
self._console.print(f"\n{banner}")
|
|
539
580
|
|
|
540
581
|
# Current reasoning
|
|
541
582
|
self._console.print("[bold cyan]Current reasoning:[/bold cyan]")
|
|
@@ -549,12 +590,14 @@ class RichConsoleRenderer:
|
|
|
549
590
|
md_steps = Markdown(msg.next_steps)
|
|
550
591
|
self._console.print(md_steps)
|
|
551
592
|
|
|
593
|
+
# Trailing newline for spinner separation
|
|
594
|
+
self._console.print()
|
|
595
|
+
|
|
552
596
|
def _render_agent_response(self, msg: AgentResponseMessage) -> None:
|
|
553
597
|
"""Render agent response with header and markdown formatting."""
|
|
554
598
|
# Header
|
|
555
|
-
self.
|
|
556
|
-
|
|
557
|
-
)
|
|
599
|
+
banner = self._format_banner("agent_response", "AGENT RESPONSE")
|
|
600
|
+
self._console.print(f"\n{banner}\n")
|
|
558
601
|
|
|
559
602
|
# Content (markdown or plain)
|
|
560
603
|
if msg.is_markdown:
|
|
@@ -571,8 +614,9 @@ class RichConsoleRenderer:
|
|
|
571
614
|
if msg.is_new_session
|
|
572
615
|
else f"Continuing ({msg.message_count} messages)"
|
|
573
616
|
)
|
|
617
|
+
banner = self._format_banner("invoke_agent", "🤖 INVOKE AGENT")
|
|
574
618
|
self._console.print(
|
|
575
|
-
f"\n
|
|
619
|
+
f"\n{banner} "
|
|
576
620
|
f"[bold cyan]{msg.agent_name}[/bold cyan] "
|
|
577
621
|
f"[dim]({session_type})[/dim]"
|
|
578
622
|
)
|
|
@@ -591,10 +635,8 @@ class RichConsoleRenderer:
|
|
|
591
635
|
def _render_subagent_response(self, msg: SubAgentResponseMessage) -> None:
|
|
592
636
|
"""Render sub-agent response with markdown formatting."""
|
|
593
637
|
# Response header
|
|
594
|
-
self.
|
|
595
|
-
|
|
596
|
-
f"[bold cyan]{msg.agent_name}[/bold cyan]"
|
|
597
|
-
)
|
|
638
|
+
banner = self._format_banner("subagent_response", "✓ AGENT RESPONSE")
|
|
639
|
+
self._console.print(f"\n{banner} [bold cyan]{msg.agent_name}[/bold cyan]")
|
|
598
640
|
|
|
599
641
|
# Render response as markdown
|
|
600
642
|
md = Markdown(msg.response)
|
|
@@ -633,9 +675,11 @@ class RichConsoleRenderer:
|
|
|
633
675
|
|
|
634
676
|
async def _render_confirmation_request(self, msg: ConfirmationRequest) -> None:
|
|
635
677
|
"""Render confirmation dialog and send response back."""
|
|
636
|
-
# Show title and description
|
|
637
|
-
|
|
638
|
-
|
|
678
|
+
# Show title and description - escape to prevent markup injection
|
|
679
|
+
safe_title = escape_rich_markup(msg.title)
|
|
680
|
+
safe_description = escape_rich_markup(msg.description)
|
|
681
|
+
self._console.print(f"\n[bold yellow]{safe_title}[/bold yellow]")
|
|
682
|
+
self._console.print(safe_description)
|
|
639
683
|
|
|
640
684
|
# Show options
|
|
641
685
|
options_str = "/".join(msg.options)
|
|
@@ -669,11 +713,13 @@ class RichConsoleRenderer:
|
|
|
669
713
|
|
|
670
714
|
async def _render_selection_request(self, msg: SelectionRequest) -> None:
|
|
671
715
|
"""Render selection menu and send response back."""
|
|
672
|
-
|
|
716
|
+
safe_prompt = escape_rich_markup(msg.prompt_text)
|
|
717
|
+
self._console.print(f"\n[bold]{safe_prompt}[/bold]")
|
|
673
718
|
|
|
674
|
-
# Show numbered options
|
|
719
|
+
# Show numbered options - escape to prevent markup injection
|
|
675
720
|
for i, opt in enumerate(msg.options):
|
|
676
|
-
|
|
721
|
+
safe_opt = escape_rich_markup(opt)
|
|
722
|
+
self._console.print(f" [cyan]{i + 1}[/cyan]. {safe_opt}")
|
|
677
723
|
|
|
678
724
|
if msg.allow_cancel:
|
|
679
725
|
self._console.print(" [dim]0. Cancel[/dim]")
|