code-puppy 0.0.341__py3-none-any.whl → 0.0.361__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/__init__.py +2 -0
- code_puppy/agents/agent_manager.py +49 -0
- code_puppy/agents/agent_pack_leader.py +383 -0
- code_puppy/agents/agent_qa_kitten.py +12 -7
- code_puppy/agents/agent_terminal_qa.py +323 -0
- code_puppy/agents/base_agent.py +34 -252
- code_puppy/agents/event_stream_handler.py +350 -0
- code_puppy/agents/pack/__init__.py +34 -0
- code_puppy/agents/pack/bloodhound.py +304 -0
- code_puppy/agents/pack/husky.py +321 -0
- code_puppy/agents/pack/retriever.py +393 -0
- code_puppy/agents/pack/shepherd.py +348 -0
- code_puppy/agents/pack/terrier.py +287 -0
- code_puppy/agents/pack/watchdog.py +367 -0
- code_puppy/agents/subagent_stream_handler.py +276 -0
- code_puppy/api/__init__.py +13 -0
- code_puppy/api/app.py +169 -0
- code_puppy/api/main.py +21 -0
- code_puppy/api/pty_manager.py +446 -0
- code_puppy/api/routers/__init__.py +12 -0
- code_puppy/api/routers/agents.py +36 -0
- code_puppy/api/routers/commands.py +217 -0
- code_puppy/api/routers/config.py +74 -0
- code_puppy/api/routers/sessions.py +232 -0
- code_puppy/api/templates/terminal.html +361 -0
- code_puppy/api/websocket.py +154 -0
- code_puppy/callbacks.py +73 -0
- code_puppy/claude_cache_client.py +249 -34
- code_puppy/cli_runner.py +4 -3
- code_puppy/command_line/add_model_menu.py +8 -9
- code_puppy/command_line/core_commands.py +85 -0
- code_puppy/command_line/mcp/catalog_server_installer.py +5 -6
- code_puppy/command_line/mcp/custom_server_form.py +54 -19
- code_puppy/command_line/mcp/custom_server_installer.py +8 -9
- code_puppy/command_line/mcp/handler.py +0 -2
- code_puppy/command_line/mcp/help_command.py +1 -5
- code_puppy/command_line/mcp/start_command.py +36 -18
- code_puppy/command_line/onboarding_slides.py +0 -1
- code_puppy/command_line/prompt_toolkit_completion.py +16 -10
- code_puppy/command_line/utils.py +54 -0
- code_puppy/config.py +66 -62
- code_puppy/mcp_/async_lifecycle.py +35 -4
- code_puppy/mcp_/managed_server.py +49 -20
- code_puppy/mcp_/manager.py +81 -52
- code_puppy/messaging/__init__.py +15 -0
- code_puppy/messaging/message_queue.py +11 -23
- code_puppy/messaging/messages.py +27 -0
- code_puppy/messaging/queue_console.py +1 -1
- code_puppy/messaging/rich_renderer.py +36 -1
- code_puppy/messaging/spinner/__init__.py +20 -2
- code_puppy/messaging/subagent_console.py +461 -0
- code_puppy/model_utils.py +54 -0
- code_puppy/plugins/antigravity_oauth/antigravity_model.py +90 -19
- code_puppy/plugins/antigravity_oauth/transport.py +1 -0
- code_puppy/plugins/frontend_emitter/__init__.py +25 -0
- code_puppy/plugins/frontend_emitter/emitter.py +121 -0
- code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
- code_puppy/prompts/antigravity_system_prompt.md +1 -0
- code_puppy/status_display.py +6 -2
- code_puppy/tools/__init__.py +37 -1
- code_puppy/tools/agent_tools.py +139 -36
- code_puppy/tools/browser/__init__.py +37 -0
- code_puppy/tools/browser/browser_control.py +6 -6
- code_puppy/tools/browser/browser_interactions.py +21 -20
- code_puppy/tools/browser/browser_locators.py +9 -9
- code_puppy/tools/browser/browser_navigation.py +7 -7
- code_puppy/tools/browser/browser_screenshot.py +78 -140
- code_puppy/tools/browser/browser_scripts.py +15 -13
- code_puppy/tools/browser/camoufox_manager.py +226 -64
- code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
- code_puppy/tools/browser/terminal_command_tools.py +521 -0
- code_puppy/tools/browser/terminal_screenshot_tools.py +556 -0
- code_puppy/tools/browser/terminal_tools.py +525 -0
- code_puppy/tools/command_runner.py +292 -101
- code_puppy/tools/common.py +176 -1
- code_puppy/tools/display.py +84 -0
- code_puppy/tools/subagent_context.py +158 -0
- {code_puppy-0.0.341.dist-info → code_puppy-0.0.361.dist-info}/METADATA +13 -11
- {code_puppy-0.0.341.dist-info → code_puppy-0.0.361.dist-info}/RECORD +84 -53
- code_puppy/command_line/mcp/add_command.py +0 -170
- code_puppy/tools/browser/vqa_agent.py +0 -90
- {code_puppy-0.0.341.data → code_puppy-0.0.361.data}/data/code_puppy/models.json +0 -0
- {code_puppy-0.0.341.data → code_puppy-0.0.361.data}/data/code_puppy/models_dev_api.json +0 -0
- {code_puppy-0.0.341.dist-info → code_puppy-0.0.361.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.341.dist-info → code_puppy-0.0.361.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.341.dist-info → code_puppy-0.0.361.dist-info}/licenses/LICENSE +0 -0
|
@@ -28,6 +28,31 @@ from code_puppy.mcp_.blocking_startup import BlockingMCPServerStdio
|
|
|
28
28
|
from code_puppy.messaging import emit_info
|
|
29
29
|
|
|
30
30
|
|
|
31
|
+
def _expand_env_vars(value: Any) -> Any:
|
|
32
|
+
"""
|
|
33
|
+
Recursively expand environment variables in config values.
|
|
34
|
+
|
|
35
|
+
Supports $VAR and ${VAR} syntax. Works with:
|
|
36
|
+
- Strings: expands env vars
|
|
37
|
+
- Dicts: recursively expands all string values
|
|
38
|
+
- Lists: recursively expands all string elements
|
|
39
|
+
- Other types: returned as-is
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
value: The value to expand env vars in
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
The value with env vars expanded
|
|
46
|
+
"""
|
|
47
|
+
if isinstance(value, str):
|
|
48
|
+
return os.path.expandvars(value)
|
|
49
|
+
elif isinstance(value, dict):
|
|
50
|
+
return {k: _expand_env_vars(v) for k, v in value.items()}
|
|
51
|
+
elif isinstance(value, list):
|
|
52
|
+
return [_expand_env_vars(item) for item in value]
|
|
53
|
+
return value
|
|
54
|
+
|
|
55
|
+
|
|
31
56
|
class ServerState(Enum):
|
|
32
57
|
"""Enumeration of possible server states."""
|
|
33
58
|
|
|
@@ -153,9 +178,9 @@ class ManagedMCPServer:
|
|
|
153
178
|
if "url" not in config:
|
|
154
179
|
raise ValueError("SSE server requires 'url' in config")
|
|
155
180
|
|
|
156
|
-
# Prepare arguments for MCPServerSSE
|
|
181
|
+
# Prepare arguments for MCPServerSSE (expand env vars in URL)
|
|
157
182
|
sse_kwargs = {
|
|
158
|
-
"url": config["url"],
|
|
183
|
+
"url": _expand_env_vars(config["url"]),
|
|
159
184
|
}
|
|
160
185
|
|
|
161
186
|
# Add optional parameters if provided
|
|
@@ -177,23 +202,26 @@ class ManagedMCPServer:
|
|
|
177
202
|
if "command" not in config:
|
|
178
203
|
raise ValueError("Stdio server requires 'command' in config")
|
|
179
204
|
|
|
180
|
-
# Handle command and arguments
|
|
181
|
-
command = config["command"]
|
|
205
|
+
# Handle command and arguments (expand env vars)
|
|
206
|
+
command = _expand_env_vars(config["command"])
|
|
182
207
|
args = config.get("args", [])
|
|
183
208
|
if isinstance(args, str):
|
|
184
|
-
# If args is a string, split it
|
|
185
|
-
args = args.split()
|
|
209
|
+
# If args is a string, split it then expand
|
|
210
|
+
args = [_expand_env_vars(a) for a in args.split()]
|
|
211
|
+
else:
|
|
212
|
+
args = _expand_env_vars(args)
|
|
186
213
|
|
|
187
214
|
# Prepare arguments for MCPServerStdio
|
|
188
215
|
stdio_kwargs = {"command": command, "args": list(args) if args else []}
|
|
189
216
|
|
|
190
|
-
# Add optional parameters if provided
|
|
217
|
+
# Add optional parameters if provided (expand env vars in env and cwd)
|
|
191
218
|
if "env" in config:
|
|
192
|
-
stdio_kwargs["env"] = config["env"]
|
|
219
|
+
stdio_kwargs["env"] = _expand_env_vars(config["env"])
|
|
193
220
|
if "cwd" in config:
|
|
194
|
-
stdio_kwargs["cwd"] = config["cwd"]
|
|
195
|
-
|
|
196
|
-
|
|
221
|
+
stdio_kwargs["cwd"] = _expand_env_vars(config["cwd"])
|
|
222
|
+
# Default timeout of 60s for stdio servers - some servers like Serena take a while to start
|
|
223
|
+
# Users can override this in their config
|
|
224
|
+
stdio_kwargs["timeout"] = config.get("timeout", 60)
|
|
197
225
|
if "read_timeout" in config:
|
|
198
226
|
stdio_kwargs["read_timeout"] = config["read_timeout"]
|
|
199
227
|
|
|
@@ -212,9 +240,9 @@ class ManagedMCPServer:
|
|
|
212
240
|
if "url" not in config:
|
|
213
241
|
raise ValueError("HTTP server requires 'url' in config")
|
|
214
242
|
|
|
215
|
-
# Prepare arguments for MCPServerStreamableHTTP
|
|
243
|
+
# Prepare arguments for MCPServerStreamableHTTP (expand env vars in URL)
|
|
216
244
|
http_kwargs = {
|
|
217
|
-
"url": config["url"],
|
|
245
|
+
"url": _expand_env_vars(config["url"]),
|
|
218
246
|
}
|
|
219
247
|
|
|
220
248
|
# Add optional parameters if provided
|
|
@@ -223,13 +251,14 @@ class ManagedMCPServer:
|
|
|
223
251
|
if "read_timeout" in config:
|
|
224
252
|
http_kwargs["read_timeout"] = config["read_timeout"]
|
|
225
253
|
|
|
226
|
-
#
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
254
|
+
# Pass headers directly instead of creating http_client
|
|
255
|
+
# Note: There's a bug in MCP 1.25.0 where passing http_client
|
|
256
|
+
# causes "'_AsyncGeneratorContextManager' object has no attribute 'stream'"
|
|
257
|
+
# The workaround is to pass headers directly and let pydantic-ai
|
|
258
|
+
# create the http_client internally.
|
|
259
|
+
if config.get("headers"):
|
|
260
|
+
# Expand environment variables in headers
|
|
261
|
+
http_kwargs["headers"] = _expand_env_vars(config["headers"])
|
|
233
262
|
|
|
234
263
|
self._pydantic_server = MCPServerStreamableHTTP(
|
|
235
264
|
**http_kwargs, process_tool_call=process_tool_call
|
code_puppy/mcp_/manager.py
CHANGED
|
@@ -469,41 +469,57 @@ class MCPManager:
|
|
|
469
469
|
def start_server_sync(self, server_id: str) -> bool:
|
|
470
470
|
"""
|
|
471
471
|
Synchronous wrapper for start_server.
|
|
472
|
+
|
|
473
|
+
IMPORTANT: This schedules the server start as a background task.
|
|
474
|
+
The server subprocess will start asynchronously - it may not be
|
|
475
|
+
immediately ready when this function returns.
|
|
472
476
|
"""
|
|
473
477
|
try:
|
|
474
|
-
asyncio.get_running_loop()
|
|
475
|
-
# We're in an async context
|
|
476
|
-
#
|
|
478
|
+
loop = asyncio.get_running_loop()
|
|
479
|
+
# We're in an async context - schedule the server start as a background task
|
|
480
|
+
# DO NOT use blocking time.sleep() here as it freezes the event loop!
|
|
481
|
+
|
|
482
|
+
# First, enable the server immediately so it's recognized as "starting"
|
|
483
|
+
managed_server = self._managed_servers.get(server_id)
|
|
484
|
+
if managed_server:
|
|
485
|
+
managed_server.enable()
|
|
486
|
+
self.status_tracker.set_status(server_id, ServerState.STARTING)
|
|
487
|
+
self.status_tracker.record_start_time(server_id)
|
|
477
488
|
|
|
478
|
-
#
|
|
479
|
-
|
|
480
|
-
|
|
489
|
+
# Schedule the async start_server to run in the background
|
|
490
|
+
# This will properly start the subprocess and lifecycle task
|
|
491
|
+
async def start_server_background():
|
|
492
|
+
try:
|
|
493
|
+
result = await self.start_server(server_id)
|
|
494
|
+
if result:
|
|
495
|
+
logger.info(f"Background server start completed: {server_id}")
|
|
496
|
+
else:
|
|
497
|
+
logger.warning(f"Background server start failed: {server_id}")
|
|
498
|
+
return result
|
|
499
|
+
except Exception as e:
|
|
500
|
+
logger.error(f"Background server start error for {server_id}: {e}")
|
|
501
|
+
self.status_tracker.set_status(server_id, ServerState.ERROR)
|
|
502
|
+
return False
|
|
481
503
|
|
|
482
|
-
#
|
|
483
|
-
task =
|
|
504
|
+
# Create the task - it will run when the event loop gets control
|
|
505
|
+
task = loop.create_task(
|
|
506
|
+
start_server_background(), name=f"start_server_{server_id}"
|
|
507
|
+
)
|
|
484
508
|
|
|
485
|
-
#
|
|
486
|
-
|
|
509
|
+
# Store task reference to prevent garbage collection
|
|
510
|
+
if not hasattr(self, "_pending_start_tasks"):
|
|
511
|
+
self._pending_start_tasks = {}
|
|
512
|
+
self._pending_start_tasks[server_id] = task
|
|
487
513
|
|
|
488
|
-
|
|
514
|
+
# Add callback to clean up task reference when done
|
|
515
|
+
def cleanup_task(t):
|
|
516
|
+
if hasattr(self, "_pending_start_tasks"):
|
|
517
|
+
self._pending_start_tasks.pop(server_id, None)
|
|
489
518
|
|
|
490
|
-
|
|
491
|
-
if task.done():
|
|
492
|
-
try:
|
|
493
|
-
result = task.result()
|
|
494
|
-
return result
|
|
495
|
-
except Exception:
|
|
496
|
-
pass
|
|
519
|
+
task.add_done_callback(cleanup_task)
|
|
497
520
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
if managed_server:
|
|
501
|
-
managed_server.enable()
|
|
502
|
-
self.status_tracker.set_status(server_id, ServerState.RUNNING)
|
|
503
|
-
self.status_tracker.record_start_time(server_id)
|
|
504
|
-
logger.info(f"Enabled server synchronously: {server_id}")
|
|
505
|
-
return True
|
|
506
|
-
return False
|
|
521
|
+
logger.info(f"Scheduled background start for server: {server_id}")
|
|
522
|
+
return True # Return immediately - server will start in background
|
|
507
523
|
|
|
508
524
|
except RuntimeError:
|
|
509
525
|
# No async loop, just enable the server
|
|
@@ -582,39 +598,52 @@ class MCPManager:
|
|
|
582
598
|
def stop_server_sync(self, server_id: str) -> bool:
|
|
583
599
|
"""
|
|
584
600
|
Synchronous wrapper for stop_server.
|
|
601
|
+
|
|
602
|
+
IMPORTANT: This schedules the server stop as a background task.
|
|
603
|
+
The server subprocess will stop asynchronously.
|
|
585
604
|
"""
|
|
586
605
|
try:
|
|
587
|
-
asyncio.get_running_loop()
|
|
606
|
+
loop = asyncio.get_running_loop()
|
|
607
|
+
# We're in an async context - schedule the server stop as a background task
|
|
608
|
+
# DO NOT use blocking time.sleep() here as it freezes the event loop!
|
|
588
609
|
|
|
589
|
-
#
|
|
590
|
-
|
|
591
|
-
|
|
610
|
+
# First, disable the server immediately
|
|
611
|
+
managed_server = self._managed_servers.get(server_id)
|
|
612
|
+
if managed_server:
|
|
613
|
+
managed_server.disable()
|
|
614
|
+
self.status_tracker.set_status(server_id, ServerState.STOPPING)
|
|
615
|
+
self.status_tracker.record_stop_time(server_id)
|
|
592
616
|
|
|
593
|
-
# Schedule the
|
|
594
|
-
|
|
617
|
+
# Schedule the async stop_server to run in the background
|
|
618
|
+
async def stop_server_background():
|
|
619
|
+
try:
|
|
620
|
+
result = await self.stop_server(server_id)
|
|
621
|
+
if result:
|
|
622
|
+
logger.info(f"Background server stop completed: {server_id}")
|
|
623
|
+
return result
|
|
624
|
+
except Exception as e:
|
|
625
|
+
logger.error(f"Background server stop error for {server_id}: {e}")
|
|
626
|
+
return False
|
|
595
627
|
|
|
596
|
-
#
|
|
597
|
-
|
|
628
|
+
# Create the task - it will run when the event loop gets control
|
|
629
|
+
task = loop.create_task(
|
|
630
|
+
stop_server_background(), name=f"stop_server_{server_id}"
|
|
631
|
+
)
|
|
598
632
|
|
|
599
|
-
|
|
633
|
+
# Store task reference to prevent garbage collection
|
|
634
|
+
if not hasattr(self, "_pending_stop_tasks"):
|
|
635
|
+
self._pending_stop_tasks = {}
|
|
636
|
+
self._pending_stop_tasks[server_id] = task
|
|
600
637
|
|
|
601
|
-
#
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
return result
|
|
606
|
-
except Exception:
|
|
607
|
-
pass
|
|
638
|
+
# Add callback to clean up task reference when done
|
|
639
|
+
def cleanup_task(t):
|
|
640
|
+
if hasattr(self, "_pending_stop_tasks"):
|
|
641
|
+
self._pending_stop_tasks.pop(server_id, None)
|
|
608
642
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
self.status_tracker.set_status(server_id, ServerState.STOPPED)
|
|
614
|
-
self.status_tracker.record_stop_time(server_id)
|
|
615
|
-
logger.info(f"Disabled server synchronously: {server_id}")
|
|
616
|
-
return True
|
|
617
|
-
return False
|
|
643
|
+
task.add_done_callback(cleanup_task)
|
|
644
|
+
|
|
645
|
+
logger.info(f"Scheduled background stop for server: {server_id}")
|
|
646
|
+
return True # Return immediately - server will stop in background
|
|
618
647
|
|
|
619
648
|
except RuntimeError:
|
|
620
649
|
# No async loop, just disable the server
|
code_puppy/messaging/__init__.py
CHANGED
|
@@ -113,6 +113,7 @@ from .messages import ( # Enums, Base, Text, File ops, Diff, Shell, Agent, etc.
|
|
|
113
113
|
StatusPanelMessage,
|
|
114
114
|
SubAgentInvocationMessage,
|
|
115
115
|
SubAgentResponseMessage,
|
|
116
|
+
SubAgentStatusMessage,
|
|
116
117
|
TextMessage,
|
|
117
118
|
UserInputRequest,
|
|
118
119
|
VersionCheckMessage,
|
|
@@ -120,6 +121,14 @@ from .messages import ( # Enums, Base, Text, File ops, Diff, Shell, Agent, etc.
|
|
|
120
121
|
from .queue_console import QueueConsole, get_queue_console
|
|
121
122
|
from .renderers import InteractiveRenderer, SynchronousInteractiveRenderer
|
|
122
123
|
|
|
124
|
+
# Sub-agent console manager
|
|
125
|
+
from .subagent_console import (
|
|
126
|
+
AgentState,
|
|
127
|
+
SubAgentConsoleManager,
|
|
128
|
+
get_subagent_console_manager,
|
|
129
|
+
STATUS_STYLES as SUBAGENT_STATUS_STYLES,
|
|
130
|
+
)
|
|
131
|
+
|
|
123
132
|
# Renderer
|
|
124
133
|
from .rich_renderer import (
|
|
125
134
|
DEFAULT_STYLES,
|
|
@@ -193,6 +202,7 @@ __all__ = [
|
|
|
193
202
|
"AgentResponseMessage",
|
|
194
203
|
"SubAgentInvocationMessage",
|
|
195
204
|
"SubAgentResponseMessage",
|
|
205
|
+
"SubAgentStatusMessage",
|
|
196
206
|
"UserInputRequest",
|
|
197
207
|
"ConfirmationRequest",
|
|
198
208
|
"SelectionRequest",
|
|
@@ -229,4 +239,9 @@ __all__ = [
|
|
|
229
239
|
"DIFF_STYLES",
|
|
230
240
|
# Markdown patches
|
|
231
241
|
"patch_markdown_headings",
|
|
242
|
+
# Sub-agent console manager
|
|
243
|
+
"AgentState",
|
|
244
|
+
"SubAgentConsoleManager",
|
|
245
|
+
"get_subagent_console_manager",
|
|
246
|
+
"SUBAGENT_STATUS_STYLES",
|
|
232
247
|
]
|
|
@@ -329,31 +329,19 @@ def emit_divider(content: str = "─" * 100 + "\n", **metadata):
|
|
|
329
329
|
|
|
330
330
|
|
|
331
331
|
def emit_prompt(prompt_text: str, timeout: float = None) -> str:
|
|
332
|
-
"""Emit a human input request and wait for response.
|
|
333
|
-
# TUI mode has been removed, always use interactive mode input
|
|
334
|
-
if True:
|
|
335
|
-
# Emit the prompt as a message for display
|
|
336
|
-
from code_puppy.messaging import emit_info
|
|
332
|
+
"""Emit a human input request and wait for response.
|
|
337
333
|
|
|
338
|
-
|
|
334
|
+
Uses safe_input for cross-platform compatibility, especially on Windows
|
|
335
|
+
where raw input() can fail after prompt_toolkit Applications.
|
|
336
|
+
"""
|
|
337
|
+
from code_puppy.command_line.utils import safe_input
|
|
338
|
+
from code_puppy.messaging import emit_info
|
|
339
339
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
console = Console()
|
|
346
|
-
response = console.input("[cyan]>>> [/cyan]")
|
|
347
|
-
return response
|
|
348
|
-
except Exception:
|
|
349
|
-
# Fallback to basic input
|
|
350
|
-
response = input(">>> ")
|
|
351
|
-
return response
|
|
352
|
-
|
|
353
|
-
# In TUI mode, use the queue system
|
|
354
|
-
queue = get_global_queue()
|
|
355
|
-
prompt_id = queue.create_prompt_request(prompt_text)
|
|
356
|
-
return queue.wait_for_prompt_response(prompt_id, timeout)
|
|
340
|
+
emit_info(prompt_text)
|
|
341
|
+
|
|
342
|
+
# Use safe_input which resets Windows console state before reading
|
|
343
|
+
response = safe_input(">>> ")
|
|
344
|
+
return response
|
|
357
345
|
|
|
358
346
|
|
|
359
347
|
def provide_prompt_response(prompt_id: str, response: str):
|
code_puppy/messaging/messages.py
CHANGED
|
@@ -292,6 +292,31 @@ class SubAgentResponseMessage(BaseMessage):
|
|
|
292
292
|
)
|
|
293
293
|
|
|
294
294
|
|
|
295
|
+
class SubAgentStatusMessage(BaseMessage):
|
|
296
|
+
"""Real-time status update for a running sub-agent."""
|
|
297
|
+
|
|
298
|
+
category: MessageCategory = MessageCategory.AGENT
|
|
299
|
+
session_id: str = Field(description="Unique session ID of the sub-agent")
|
|
300
|
+
agent_name: str = Field(description="Name of the agent (e.g., 'code-puppy')")
|
|
301
|
+
model_name: str = Field(description="Model being used by this agent")
|
|
302
|
+
status: Literal[
|
|
303
|
+
"starting", "running", "thinking", "tool_calling", "completed", "error"
|
|
304
|
+
] = Field(description="Current status of the agent")
|
|
305
|
+
tool_call_count: int = Field(
|
|
306
|
+
default=0, ge=0, description="Number of tools called so far"
|
|
307
|
+
)
|
|
308
|
+
token_count: int = Field(default=0, ge=0, description="Estimated tokens in context")
|
|
309
|
+
current_tool: Optional[str] = Field(
|
|
310
|
+
default=None, description="Name of tool currently being called"
|
|
311
|
+
)
|
|
312
|
+
elapsed_seconds: float = Field(
|
|
313
|
+
default=0.0, ge=0, description="Time since agent started"
|
|
314
|
+
)
|
|
315
|
+
error_message: Optional[str] = Field(
|
|
316
|
+
default=None, description="Error message if status is 'error'"
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
|
|
295
320
|
# =============================================================================
|
|
296
321
|
# User Interaction Messages (Agent → User)
|
|
297
322
|
# =============================================================================
|
|
@@ -417,6 +442,7 @@ AnyMessage = Union[
|
|
|
417
442
|
AgentResponseMessage,
|
|
418
443
|
SubAgentInvocationMessage,
|
|
419
444
|
SubAgentResponseMessage,
|
|
445
|
+
SubAgentStatusMessage,
|
|
420
446
|
UserInputRequest,
|
|
421
447
|
ConfirmationRequest,
|
|
422
448
|
SelectionRequest,
|
|
@@ -458,6 +484,7 @@ __all__ = [
|
|
|
458
484
|
"AgentResponseMessage",
|
|
459
485
|
"SubAgentInvocationMessage",
|
|
460
486
|
"SubAgentResponseMessage",
|
|
487
|
+
"SubAgentStatusMessage",
|
|
461
488
|
# User interaction
|
|
462
489
|
"UserInputRequest",
|
|
463
490
|
"ConfirmationRequest",
|
|
@@ -239,7 +239,7 @@ class QueueConsole:
|
|
|
239
239
|
# Show the user's response in the chat as well
|
|
240
240
|
if user_response:
|
|
241
241
|
self.queue.emit_simple(
|
|
242
|
-
MessageType.
|
|
242
|
+
MessageType.INFO, f"User response: {user_response}"
|
|
243
243
|
)
|
|
244
244
|
|
|
245
245
|
return user_response
|
|
@@ -18,7 +18,9 @@ from rich.rule import Rule
|
|
|
18
18
|
# Note: Syntax import removed - file content not displayed, only header
|
|
19
19
|
from rich.table import Table
|
|
20
20
|
|
|
21
|
+
from code_puppy.config import get_subagent_verbose
|
|
21
22
|
from code_puppy.tools.common import format_diff_with_colors
|
|
23
|
+
from code_puppy.tools.subagent_context import is_subagent
|
|
22
24
|
|
|
23
25
|
from .bus import MessageBus
|
|
24
26
|
from .commands import (
|
|
@@ -159,6 +161,14 @@ class RichConsoleRenderer:
|
|
|
159
161
|
color = self._get_banner_color(banner_name)
|
|
160
162
|
return f"[bold white on {color}] {text} [/bold white on {color}]"
|
|
161
163
|
|
|
164
|
+
def _should_suppress_subagent_output(self) -> bool:
|
|
165
|
+
"""Check if sub-agent output should be suppressed.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
True if we're in a sub-agent context and verbose mode is disabled
|
|
169
|
+
"""
|
|
170
|
+
return is_subagent() and not get_subagent_verbose()
|
|
171
|
+
|
|
162
172
|
# =========================================================================
|
|
163
173
|
# Lifecycle (Synchronous - for compatibility with main.py)
|
|
164
174
|
# =========================================================================
|
|
@@ -275,7 +285,8 @@ class RichConsoleRenderer:
|
|
|
275
285
|
elif isinstance(message, SubAgentInvocationMessage):
|
|
276
286
|
self._render_subagent_invocation(message)
|
|
277
287
|
elif isinstance(message, SubAgentResponseMessage):
|
|
278
|
-
|
|
288
|
+
# Skip rendering - we now display sub-agent responses via display_non_streamed_result
|
|
289
|
+
pass
|
|
279
290
|
elif isinstance(message, UserInputRequest):
|
|
280
291
|
# Can't handle async user input in sync context - skip
|
|
281
292
|
self._console.print("[dim]User input requested (requires async)[/dim]")
|
|
@@ -356,6 +367,10 @@ class RichConsoleRenderer:
|
|
|
356
367
|
- Total size
|
|
357
368
|
- Number of subdirectories
|
|
358
369
|
"""
|
|
370
|
+
# Skip for sub-agents unless verbose mode
|
|
371
|
+
if self._should_suppress_subagent_output():
|
|
372
|
+
return
|
|
373
|
+
|
|
359
374
|
import os
|
|
360
375
|
from collections import defaultdict
|
|
361
376
|
|
|
@@ -478,6 +493,10 @@ class RichConsoleRenderer:
|
|
|
478
493
|
|
|
479
494
|
The file content is for the LLM only, not for display in the UI.
|
|
480
495
|
"""
|
|
496
|
+
# Skip for sub-agents unless verbose mode
|
|
497
|
+
if self._should_suppress_subagent_output():
|
|
498
|
+
return
|
|
499
|
+
|
|
481
500
|
# Build line info
|
|
482
501
|
line_info = ""
|
|
483
502
|
if msg.start_line is not None and msg.num_lines is not None:
|
|
@@ -492,6 +511,10 @@ class RichConsoleRenderer:
|
|
|
492
511
|
|
|
493
512
|
def _render_grep_result(self, msg: GrepResultMessage) -> None:
|
|
494
513
|
"""Render grep results grouped by file matching old format."""
|
|
514
|
+
# Skip for sub-agents unless verbose mode
|
|
515
|
+
if self._should_suppress_subagent_output():
|
|
516
|
+
return
|
|
517
|
+
|
|
495
518
|
import re
|
|
496
519
|
|
|
497
520
|
# Header
|
|
@@ -572,6 +595,10 @@ class RichConsoleRenderer:
|
|
|
572
595
|
|
|
573
596
|
def _render_diff(self, msg: DiffMessage) -> None:
|
|
574
597
|
"""Render a diff with beautiful syntax highlighting."""
|
|
598
|
+
# Skip for sub-agents unless verbose mode
|
|
599
|
+
if self._should_suppress_subagent_output():
|
|
600
|
+
return
|
|
601
|
+
|
|
575
602
|
# Operation-specific styling
|
|
576
603
|
op_icons = {"create": "✨", "modify": "✏️", "delete": "🗑️"}
|
|
577
604
|
op_colors = {"create": "green", "modify": "yellow", "delete": "red"}
|
|
@@ -616,6 +643,10 @@ class RichConsoleRenderer:
|
|
|
616
643
|
|
|
617
644
|
def _render_shell_start(self, msg: ShellStartMessage) -> None:
|
|
618
645
|
"""Render shell command start notification."""
|
|
646
|
+
# Skip for sub-agents unless verbose mode
|
|
647
|
+
if self._should_suppress_subagent_output():
|
|
648
|
+
return
|
|
649
|
+
|
|
619
650
|
# Escape command to prevent Rich markup injection
|
|
620
651
|
safe_command = escape_rich_markup(msg.command)
|
|
621
652
|
# Header showing command is starting
|
|
@@ -700,6 +731,10 @@ class RichConsoleRenderer:
|
|
|
700
731
|
|
|
701
732
|
def _render_subagent_invocation(self, msg: SubAgentInvocationMessage) -> None:
|
|
702
733
|
"""Render sub-agent invocation header with nice formatting."""
|
|
734
|
+
# Skip for sub-agents unless verbose mode (avoid nested invocation banners)
|
|
735
|
+
if self._should_suppress_subagent_output():
|
|
736
|
+
return
|
|
737
|
+
|
|
703
738
|
# Header with agent name and session
|
|
704
739
|
session_type = (
|
|
705
740
|
"New session"
|
|
@@ -24,7 +24,16 @@ def unregister_spinner(spinner):
|
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
def pause_all_spinners():
|
|
27
|
-
"""Pause all active spinners.
|
|
27
|
+
"""Pause all active spinners.
|
|
28
|
+
|
|
29
|
+
No-op when called from a sub-agent context to prevent
|
|
30
|
+
parallel sub-agents from interfering with the main spinner.
|
|
31
|
+
"""
|
|
32
|
+
# Lazy import to avoid circular dependency
|
|
33
|
+
from code_puppy.tools.subagent_context import is_subagent
|
|
34
|
+
|
|
35
|
+
if is_subagent():
|
|
36
|
+
return # Sub-agents don't control the main spinner
|
|
28
37
|
for spinner in _active_spinners:
|
|
29
38
|
try:
|
|
30
39
|
spinner.pause()
|
|
@@ -34,7 +43,16 @@ def pause_all_spinners():
|
|
|
34
43
|
|
|
35
44
|
|
|
36
45
|
def resume_all_spinners():
|
|
37
|
-
"""Resume all active spinners.
|
|
46
|
+
"""Resume all active spinners.
|
|
47
|
+
|
|
48
|
+
No-op when called from a sub-agent context to prevent
|
|
49
|
+
parallel sub-agents from interfering with the main spinner.
|
|
50
|
+
"""
|
|
51
|
+
# Lazy import to avoid circular dependency
|
|
52
|
+
from code_puppy.tools.subagent_context import is_subagent
|
|
53
|
+
|
|
54
|
+
if is_subagent():
|
|
55
|
+
return # Sub-agents don't control the main spinner
|
|
38
56
|
for spinner in _active_spinners:
|
|
39
57
|
try:
|
|
40
58
|
spinner.resume()
|