code-puppy 0.0.354__py3-none-any.whl → 0.0.356__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/event_stream_handler.py +74 -1
- code_puppy/agents/subagent_stream_handler.py +276 -0
- code_puppy/api/__init__.py +13 -0
- code_puppy/api/app.py +92 -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 +198 -0
- code_puppy/api/routers/config.py +74 -0
- code_puppy/api/routers/sessions.py +191 -0
- code_puppy/api/templates/terminal.html +361 -0
- code_puppy/api/websocket.py +154 -0
- code_puppy/callbacks.py +73 -0
- code_puppy/command_line/core_commands.py +85 -0
- code_puppy/config.py +63 -0
- code_puppy/messaging/__init__.py +15 -0
- code_puppy/messaging/messages.py +27 -0
- code_puppy/messaging/rich_renderer.py +34 -0
- code_puppy/messaging/spinner/__init__.py +20 -2
- code_puppy/messaging/subagent_console.py +461 -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/status_display.py +6 -2
- code_puppy/tools/agent_tools.py +53 -49
- code_puppy/tools/command_runner.py +292 -100
- code_puppy/tools/common.py +176 -1
- code_puppy/tools/display.py +6 -1
- code_puppy/tools/subagent_context.py +158 -0
- {code_puppy-0.0.354.dist-info → code_puppy-0.0.356.dist-info}/METADATA +4 -3
- {code_puppy-0.0.354.dist-info → code_puppy-0.0.356.dist-info}/RECORD +38 -21
- {code_puppy-0.0.354.data → code_puppy-0.0.356.data}/data/code_puppy/models.json +0 -0
- {code_puppy-0.0.354.data → code_puppy-0.0.356.data}/data/code_puppy/models_dev_api.json +0 -0
- {code_puppy-0.0.354.dist-info → code_puppy-0.0.356.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.354.dist-info → code_puppy-0.0.356.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.354.dist-info → code_puppy-0.0.356.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""WebSocket endpoints for Code Puppy API.
|
|
2
|
+
|
|
3
|
+
Provides real-time communication channels:
|
|
4
|
+
- /ws/events - Server-sent events stream
|
|
5
|
+
- /ws/terminal - Interactive PTY terminal sessions
|
|
6
|
+
- /ws/health - Simple health check endpoint
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import base64
|
|
11
|
+
import logging
|
|
12
|
+
import uuid
|
|
13
|
+
|
|
14
|
+
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def setup_websocket(app: FastAPI) -> None:
|
|
20
|
+
"""Setup WebSocket endpoints for the application."""
|
|
21
|
+
|
|
22
|
+
@app.websocket("/ws/events")
|
|
23
|
+
async def websocket_events(websocket: WebSocket) -> None:
|
|
24
|
+
"""Stream real-time events to connected clients."""
|
|
25
|
+
await websocket.accept()
|
|
26
|
+
logger.info("Events WebSocket client connected")
|
|
27
|
+
|
|
28
|
+
from code_puppy.plugins.frontend_emitter.emitter import (
|
|
29
|
+
get_recent_events,
|
|
30
|
+
subscribe,
|
|
31
|
+
unsubscribe,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
event_queue = subscribe()
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
recent_events = get_recent_events()
|
|
38
|
+
for event in recent_events:
|
|
39
|
+
await websocket.send_json(event)
|
|
40
|
+
|
|
41
|
+
while True:
|
|
42
|
+
try:
|
|
43
|
+
event = await asyncio.wait_for(event_queue.get(), timeout=30.0)
|
|
44
|
+
await websocket.send_json(event)
|
|
45
|
+
except asyncio.TimeoutError:
|
|
46
|
+
try:
|
|
47
|
+
await websocket.send_json({"type": "ping"})
|
|
48
|
+
except Exception:
|
|
49
|
+
break
|
|
50
|
+
except WebSocketDisconnect:
|
|
51
|
+
logger.info("Events WebSocket client disconnected")
|
|
52
|
+
except Exception as e:
|
|
53
|
+
logger.error(f"Events WebSocket error: {e}")
|
|
54
|
+
finally:
|
|
55
|
+
unsubscribe(event_queue)
|
|
56
|
+
|
|
57
|
+
@app.websocket("/ws/terminal")
|
|
58
|
+
async def websocket_terminal(websocket: WebSocket) -> None:
|
|
59
|
+
"""Interactive terminal WebSocket endpoint."""
|
|
60
|
+
await websocket.accept()
|
|
61
|
+
logger.info("Terminal WebSocket client connected")
|
|
62
|
+
|
|
63
|
+
from code_puppy.api.pty_manager import get_pty_manager
|
|
64
|
+
|
|
65
|
+
manager = get_pty_manager()
|
|
66
|
+
session_id = str(uuid.uuid4())[:8]
|
|
67
|
+
session = None
|
|
68
|
+
|
|
69
|
+
# Get the current event loop for thread-safe scheduling
|
|
70
|
+
loop = asyncio.get_running_loop()
|
|
71
|
+
|
|
72
|
+
# Queue to receive PTY output in a thread-safe way
|
|
73
|
+
output_queue: asyncio.Queue[bytes] = asyncio.Queue()
|
|
74
|
+
|
|
75
|
+
# Output callback - called from thread pool, puts data in queue
|
|
76
|
+
def on_output(data: bytes) -> None:
|
|
77
|
+
try:
|
|
78
|
+
loop.call_soon_threadsafe(output_queue.put_nowait, data)
|
|
79
|
+
except Exception as e:
|
|
80
|
+
logger.error(f"on_output error: {e}")
|
|
81
|
+
|
|
82
|
+
async def output_sender() -> None:
|
|
83
|
+
"""Coroutine that sends queued output to WebSocket."""
|
|
84
|
+
try:
|
|
85
|
+
while True:
|
|
86
|
+
data = await output_queue.get()
|
|
87
|
+
await websocket.send_json(
|
|
88
|
+
{
|
|
89
|
+
"type": "output",
|
|
90
|
+
"data": base64.b64encode(data).decode("ascii"),
|
|
91
|
+
}
|
|
92
|
+
)
|
|
93
|
+
except asyncio.CancelledError:
|
|
94
|
+
pass
|
|
95
|
+
except Exception as e:
|
|
96
|
+
logger.error(f"output_sender error: {e}")
|
|
97
|
+
|
|
98
|
+
sender_task = None
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
# Create PTY session
|
|
102
|
+
session = await manager.create_session(
|
|
103
|
+
session_id=session_id,
|
|
104
|
+
on_output=on_output,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# Send session info
|
|
108
|
+
await websocket.send_json({"type": "session", "id": session_id})
|
|
109
|
+
|
|
110
|
+
# Start output sender task
|
|
111
|
+
sender_task = asyncio.create_task(output_sender())
|
|
112
|
+
|
|
113
|
+
# Handle incoming messages
|
|
114
|
+
while True:
|
|
115
|
+
try:
|
|
116
|
+
msg = await websocket.receive_json()
|
|
117
|
+
|
|
118
|
+
if msg.get("type") == "input":
|
|
119
|
+
data = msg.get("data", "")
|
|
120
|
+
if isinstance(data, str):
|
|
121
|
+
data = data.encode("utf-8")
|
|
122
|
+
await manager.write(session_id, data)
|
|
123
|
+
elif msg.get("type") == "resize":
|
|
124
|
+
cols = msg.get("cols", 80)
|
|
125
|
+
rows = msg.get("rows", 24)
|
|
126
|
+
await manager.resize(session_id, cols, rows)
|
|
127
|
+
except WebSocketDisconnect:
|
|
128
|
+
break
|
|
129
|
+
except Exception as e:
|
|
130
|
+
logger.error(f"Terminal WebSocket error: {e}")
|
|
131
|
+
break
|
|
132
|
+
except Exception as e:
|
|
133
|
+
logger.error(f"Terminal session error: {e}")
|
|
134
|
+
finally:
|
|
135
|
+
if sender_task:
|
|
136
|
+
sender_task.cancel()
|
|
137
|
+
try:
|
|
138
|
+
await sender_task
|
|
139
|
+
except asyncio.CancelledError:
|
|
140
|
+
pass
|
|
141
|
+
if session:
|
|
142
|
+
await manager.close_session(session_id)
|
|
143
|
+
logger.info("Terminal WebSocket disconnected")
|
|
144
|
+
|
|
145
|
+
@app.websocket("/ws/health")
|
|
146
|
+
async def websocket_health(websocket: WebSocket) -> None:
|
|
147
|
+
"""Simple WebSocket health check - echoes messages back."""
|
|
148
|
+
await websocket.accept()
|
|
149
|
+
try:
|
|
150
|
+
while True:
|
|
151
|
+
data = await websocket.receive_text()
|
|
152
|
+
await websocket.send_text(f"echo: {data}")
|
|
153
|
+
except WebSocketDisconnect:
|
|
154
|
+
pass
|
code_puppy/callbacks.py
CHANGED
|
@@ -18,6 +18,9 @@ PhaseType = Literal[
|
|
|
18
18
|
"custom_command",
|
|
19
19
|
"custom_command_help",
|
|
20
20
|
"file_permission",
|
|
21
|
+
"pre_tool_call",
|
|
22
|
+
"post_tool_call",
|
|
23
|
+
"stream_event",
|
|
21
24
|
]
|
|
22
25
|
CallbackFunc = Callable[..., Any]
|
|
23
26
|
|
|
@@ -36,6 +39,9 @@ _callbacks: Dict[PhaseType, List[CallbackFunc]] = {
|
|
|
36
39
|
"custom_command": [],
|
|
37
40
|
"custom_command_help": [],
|
|
38
41
|
"file_permission": [],
|
|
42
|
+
"pre_tool_call": [],
|
|
43
|
+
"post_tool_call": [],
|
|
44
|
+
"stream_event": [],
|
|
39
45
|
}
|
|
40
46
|
|
|
41
47
|
logger = logging.getLogger(__name__)
|
|
@@ -271,3 +277,70 @@ def on_file_permission(
|
|
|
271
277
|
message_group,
|
|
272
278
|
operation_data,
|
|
273
279
|
)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
async def on_pre_tool_call(
|
|
283
|
+
tool_name: str, tool_args: dict, context: Any = None
|
|
284
|
+
) -> List[Any]:
|
|
285
|
+
"""Trigger callbacks before a tool is called.
|
|
286
|
+
|
|
287
|
+
This allows plugins to inspect, modify, or log tool calls before
|
|
288
|
+
they are executed.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
tool_name: Name of the tool being called
|
|
292
|
+
tool_args: Arguments being passed to the tool
|
|
293
|
+
context: Optional context data for the tool call
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
List of results from registered callbacks.
|
|
297
|
+
"""
|
|
298
|
+
return await _trigger_callbacks("pre_tool_call", tool_name, tool_args, context)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
async def on_post_tool_call(
|
|
302
|
+
tool_name: str,
|
|
303
|
+
tool_args: dict,
|
|
304
|
+
result: Any,
|
|
305
|
+
duration_ms: float,
|
|
306
|
+
context: Any = None,
|
|
307
|
+
) -> List[Any]:
|
|
308
|
+
"""Trigger callbacks after a tool completes.
|
|
309
|
+
|
|
310
|
+
This allows plugins to inspect tool results, log execution times,
|
|
311
|
+
or perform post-processing.
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
tool_name: Name of the tool that was called
|
|
315
|
+
tool_args: Arguments that were passed to the tool
|
|
316
|
+
result: The result returned by the tool
|
|
317
|
+
duration_ms: Execution time in milliseconds
|
|
318
|
+
context: Optional context data for the tool call
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
List of results from registered callbacks.
|
|
322
|
+
"""
|
|
323
|
+
return await _trigger_callbacks(
|
|
324
|
+
"post_tool_call", tool_name, tool_args, result, duration_ms, context
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
async def on_stream_event(
|
|
329
|
+
event_type: str, event_data: Any, agent_session_id: str | None = None
|
|
330
|
+
) -> List[Any]:
|
|
331
|
+
"""Trigger callbacks for streaming events.
|
|
332
|
+
|
|
333
|
+
This allows plugins to react to streaming events in real-time,
|
|
334
|
+
such as tokens being generated, tool calls starting, etc.
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
event_type: Type of the streaming event
|
|
338
|
+
event_data: Data associated with the event
|
|
339
|
+
agent_session_id: Optional session ID of the agent emitting the event
|
|
340
|
+
|
|
341
|
+
Returns:
|
|
342
|
+
List of results from registered callbacks.
|
|
343
|
+
"""
|
|
344
|
+
return await _trigger_callbacks(
|
|
345
|
+
"stream_event", event_type, event_data, agent_session_id
|
|
346
|
+
)
|
|
@@ -772,6 +772,91 @@ def handle_mcp_command(command: str) -> bool:
|
|
|
772
772
|
return handler.handle_mcp_command(command)
|
|
773
773
|
|
|
774
774
|
|
|
775
|
+
@register_command(
|
|
776
|
+
name="api",
|
|
777
|
+
description="Manage the Code Puppy API server",
|
|
778
|
+
usage="/api [start|stop|status]",
|
|
779
|
+
category="core",
|
|
780
|
+
detailed_help="Start, stop, or check status of the local FastAPI server for GUI integration.",
|
|
781
|
+
)
|
|
782
|
+
def handle_api_command(command: str) -> bool:
|
|
783
|
+
"""Handle the /api command."""
|
|
784
|
+
import os
|
|
785
|
+
import signal
|
|
786
|
+
import subprocess
|
|
787
|
+
import sys
|
|
788
|
+
from pathlib import Path
|
|
789
|
+
|
|
790
|
+
from code_puppy.config import STATE_DIR
|
|
791
|
+
from code_puppy.messaging import emit_error, emit_info, emit_success
|
|
792
|
+
|
|
793
|
+
parts = command.split()
|
|
794
|
+
subcommand = parts[1] if len(parts) > 1 else "status"
|
|
795
|
+
|
|
796
|
+
pid_file = Path(STATE_DIR) / "api_server.pid"
|
|
797
|
+
|
|
798
|
+
if subcommand == "start":
|
|
799
|
+
# Check if already running
|
|
800
|
+
if pid_file.exists():
|
|
801
|
+
try:
|
|
802
|
+
pid = int(pid_file.read_text().strip())
|
|
803
|
+
os.kill(pid, 0) # Check if process exists
|
|
804
|
+
emit_info(f"API server already running (PID {pid})")
|
|
805
|
+
return True
|
|
806
|
+
except (OSError, ValueError):
|
|
807
|
+
pid_file.unlink(missing_ok=True) # Stale PID file
|
|
808
|
+
|
|
809
|
+
# Start the server in background
|
|
810
|
+
emit_info("Starting API server on http://127.0.0.1:8765 ...")
|
|
811
|
+
proc = subprocess.Popen(
|
|
812
|
+
[sys.executable, "-m", "code_puppy.api.main"],
|
|
813
|
+
stdout=subprocess.DEVNULL,
|
|
814
|
+
stderr=subprocess.DEVNULL,
|
|
815
|
+
start_new_session=True,
|
|
816
|
+
)
|
|
817
|
+
pid_file.parent.mkdir(parents=True, exist_ok=True)
|
|
818
|
+
pid_file.write_text(str(proc.pid))
|
|
819
|
+
emit_success(f"API server started (PID {proc.pid})")
|
|
820
|
+
emit_info("Docs available at http://127.0.0.1:8765/docs")
|
|
821
|
+
return True
|
|
822
|
+
|
|
823
|
+
elif subcommand == "stop":
|
|
824
|
+
if not pid_file.exists():
|
|
825
|
+
emit_info("API server is not running")
|
|
826
|
+
return True
|
|
827
|
+
|
|
828
|
+
try:
|
|
829
|
+
pid = int(pid_file.read_text().strip())
|
|
830
|
+
os.kill(pid, signal.SIGTERM)
|
|
831
|
+
pid_file.unlink()
|
|
832
|
+
emit_success(f"API server stopped (PID {pid})")
|
|
833
|
+
except (OSError, ValueError) as e:
|
|
834
|
+
pid_file.unlink(missing_ok=True)
|
|
835
|
+
emit_error(f"Error stopping server: {e}")
|
|
836
|
+
return True
|
|
837
|
+
|
|
838
|
+
elif subcommand == "status":
|
|
839
|
+
if not pid_file.exists():
|
|
840
|
+
emit_info("API server is not running")
|
|
841
|
+
return True
|
|
842
|
+
|
|
843
|
+
try:
|
|
844
|
+
pid = int(pid_file.read_text().strip())
|
|
845
|
+
os.kill(pid, 0) # Check if process exists
|
|
846
|
+
emit_success(f"API server is running (PID {pid})")
|
|
847
|
+
emit_info("URL: http://127.0.0.1:8765")
|
|
848
|
+
emit_info("Docs: http://127.0.0.1:8765/docs")
|
|
849
|
+
except (OSError, ValueError):
|
|
850
|
+
pid_file.unlink(missing_ok=True)
|
|
851
|
+
emit_info("API server is not running (stale PID file removed)")
|
|
852
|
+
return True
|
|
853
|
+
|
|
854
|
+
else:
|
|
855
|
+
emit_error(f"Unknown subcommand: {subcommand}")
|
|
856
|
+
emit_info("Usage: /api [start|stop|status]")
|
|
857
|
+
return True
|
|
858
|
+
|
|
859
|
+
|
|
775
860
|
@register_command(
|
|
776
861
|
name="generate-pr-description",
|
|
777
862
|
description="Generate comprehensive PR description",
|
code_puppy/config.py
CHANGED
|
@@ -75,6 +75,19 @@ def get_use_dbos() -> bool:
|
|
|
75
75
|
return str(cfg_val).strip().lower() in {"1", "true", "yes", "on"}
|
|
76
76
|
|
|
77
77
|
|
|
78
|
+
def get_subagent_verbose() -> bool:
|
|
79
|
+
"""Return True if sub-agent verbose output is enabled (default False).
|
|
80
|
+
|
|
81
|
+
When False (default), sub-agents produce quiet, sparse output suitable
|
|
82
|
+
for parallel execution. When True, sub-agents produce full verbose output
|
|
83
|
+
like the main agent (useful for debugging).
|
|
84
|
+
"""
|
|
85
|
+
cfg_val = get_value("subagent_verbose")
|
|
86
|
+
if cfg_val is None:
|
|
87
|
+
return False
|
|
88
|
+
return str(cfg_val).strip().lower() in {"1", "true", "yes", "on"}
|
|
89
|
+
|
|
90
|
+
|
|
78
91
|
DEFAULT_SECTION = "puppy"
|
|
79
92
|
REQUIRED_KEYS = ["puppy_name", "owner_name"]
|
|
80
93
|
|
|
@@ -208,6 +221,9 @@ def get_config_keys():
|
|
|
208
221
|
"diff_context_lines",
|
|
209
222
|
"default_agent",
|
|
210
223
|
"temperature",
|
|
224
|
+
"frontend_emitter_enabled",
|
|
225
|
+
"frontend_emitter_max_recent_events",
|
|
226
|
+
"frontend_emitter_queue_size",
|
|
211
227
|
]
|
|
212
228
|
# Add DBOS control key
|
|
213
229
|
default_keys.append("enable_dbos")
|
|
@@ -237,6 +253,22 @@ def set_config_value(key: str, value: str):
|
|
|
237
253
|
config.write(f)
|
|
238
254
|
|
|
239
255
|
|
|
256
|
+
# Alias for API compatibility
|
|
257
|
+
def set_value(key: str, value: str) -> None:
|
|
258
|
+
"""Set a config value. Alias for set_config_value."""
|
|
259
|
+
set_config_value(key, value)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def reset_value(key: str) -> None:
|
|
263
|
+
"""Remove a key from the config file, resetting it to default."""
|
|
264
|
+
config = configparser.ConfigParser()
|
|
265
|
+
config.read(CONFIG_FILE)
|
|
266
|
+
if DEFAULT_SECTION in config and key in config[DEFAULT_SECTION]:
|
|
267
|
+
del config[DEFAULT_SECTION][key]
|
|
268
|
+
with open(CONFIG_FILE, "w") as f:
|
|
269
|
+
config.write(f)
|
|
270
|
+
|
|
271
|
+
|
|
240
272
|
# --- MODEL STICKY EXTENSION STARTS HERE ---
|
|
241
273
|
def load_mcp_server_configs():
|
|
242
274
|
"""
|
|
@@ -1584,3 +1616,34 @@ def set_default_agent(agent_name: str) -> None:
|
|
|
1584
1616
|
agent_name: The name of the agent to set as default.
|
|
1585
1617
|
"""
|
|
1586
1618
|
set_config_value("default_agent", agent_name)
|
|
1619
|
+
|
|
1620
|
+
|
|
1621
|
+
# --- FRONTEND EMITTER CONFIGURATION ---
|
|
1622
|
+
def get_frontend_emitter_enabled() -> bool:
|
|
1623
|
+
"""Check if frontend emitter is enabled."""
|
|
1624
|
+
val = get_value("frontend_emitter_enabled")
|
|
1625
|
+
if val is None:
|
|
1626
|
+
return True # Enabled by default
|
|
1627
|
+
return str(val).lower() in ("1", "true", "yes", "on")
|
|
1628
|
+
|
|
1629
|
+
|
|
1630
|
+
def get_frontend_emitter_max_recent_events() -> int:
|
|
1631
|
+
"""Get max number of recent events to buffer."""
|
|
1632
|
+
val = get_value("frontend_emitter_max_recent_events")
|
|
1633
|
+
if val is None:
|
|
1634
|
+
return 100
|
|
1635
|
+
try:
|
|
1636
|
+
return int(val)
|
|
1637
|
+
except ValueError:
|
|
1638
|
+
return 100
|
|
1639
|
+
|
|
1640
|
+
|
|
1641
|
+
def get_frontend_emitter_queue_size() -> int:
|
|
1642
|
+
"""Get max subscriber queue size."""
|
|
1643
|
+
val = get_value("frontend_emitter_queue_size")
|
|
1644
|
+
if val is None:
|
|
1645
|
+
return 100
|
|
1646
|
+
try:
|
|
1647
|
+
return int(val)
|
|
1648
|
+
except ValueError:
|
|
1649
|
+
return 100
|
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
|
]
|
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",
|
|
@@ -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
|
# =========================================================================
|
|
@@ -357,6 +367,10 @@ class RichConsoleRenderer:
|
|
|
357
367
|
- Total size
|
|
358
368
|
- Number of subdirectories
|
|
359
369
|
"""
|
|
370
|
+
# Skip for sub-agents unless verbose mode
|
|
371
|
+
if self._should_suppress_subagent_output():
|
|
372
|
+
return
|
|
373
|
+
|
|
360
374
|
import os
|
|
361
375
|
from collections import defaultdict
|
|
362
376
|
|
|
@@ -479,6 +493,10 @@ class RichConsoleRenderer:
|
|
|
479
493
|
|
|
480
494
|
The file content is for the LLM only, not for display in the UI.
|
|
481
495
|
"""
|
|
496
|
+
# Skip for sub-agents unless verbose mode
|
|
497
|
+
if self._should_suppress_subagent_output():
|
|
498
|
+
return
|
|
499
|
+
|
|
482
500
|
# Build line info
|
|
483
501
|
line_info = ""
|
|
484
502
|
if msg.start_line is not None and msg.num_lines is not None:
|
|
@@ -493,6 +511,10 @@ class RichConsoleRenderer:
|
|
|
493
511
|
|
|
494
512
|
def _render_grep_result(self, msg: GrepResultMessage) -> None:
|
|
495
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
|
+
|
|
496
518
|
import re
|
|
497
519
|
|
|
498
520
|
# Header
|
|
@@ -573,6 +595,10 @@ class RichConsoleRenderer:
|
|
|
573
595
|
|
|
574
596
|
def _render_diff(self, msg: DiffMessage) -> None:
|
|
575
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
|
+
|
|
576
602
|
# Operation-specific styling
|
|
577
603
|
op_icons = {"create": "✨", "modify": "✏️", "delete": "🗑️"}
|
|
578
604
|
op_colors = {"create": "green", "modify": "yellow", "delete": "red"}
|
|
@@ -617,6 +643,10 @@ class RichConsoleRenderer:
|
|
|
617
643
|
|
|
618
644
|
def _render_shell_start(self, msg: ShellStartMessage) -> None:
|
|
619
645
|
"""Render shell command start notification."""
|
|
646
|
+
# Skip for sub-agents unless verbose mode
|
|
647
|
+
if self._should_suppress_subagent_output():
|
|
648
|
+
return
|
|
649
|
+
|
|
620
650
|
# Escape command to prevent Rich markup injection
|
|
621
651
|
safe_command = escape_rich_markup(msg.command)
|
|
622
652
|
# Header showing command is starting
|
|
@@ -701,6 +731,10 @@ class RichConsoleRenderer:
|
|
|
701
731
|
|
|
702
732
|
def _render_subagent_invocation(self, msg: SubAgentInvocationMessage) -> None:
|
|
703
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
|
+
|
|
704
738
|
# Header with agent name and session
|
|
705
739
|
session_type = (
|
|
706
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()
|