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
|
@@ -7,6 +7,7 @@ custom MCP servers with JSON configuration.
|
|
|
7
7
|
import json
|
|
8
8
|
import os
|
|
9
9
|
|
|
10
|
+
from code_puppy.command_line.utils import safe_input
|
|
10
11
|
from code_puppy.messaging import emit_error, emit_info, emit_success, emit_warning
|
|
11
12
|
|
|
12
13
|
# Example configurations for each server type
|
|
@@ -24,7 +25,7 @@ CUSTOM_SERVER_EXAMPLES = {
|
|
|
24
25
|
"type": "http",
|
|
25
26
|
"url": "http://localhost:8080/mcp",
|
|
26
27
|
"headers": {
|
|
27
|
-
"Authorization": "Bearer
|
|
28
|
+
"Authorization": "Bearer $MY_API_KEY",
|
|
28
29
|
"Content-Type": "application/json"
|
|
29
30
|
},
|
|
30
31
|
"timeout": 30
|
|
@@ -33,7 +34,7 @@ CUSTOM_SERVER_EXAMPLES = {
|
|
|
33
34
|
"type": "sse",
|
|
34
35
|
"url": "http://localhost:8080/sse",
|
|
35
36
|
"headers": {
|
|
36
|
-
"Authorization": "Bearer
|
|
37
|
+
"Authorization": "Bearer $MY_API_KEY"
|
|
37
38
|
}
|
|
38
39
|
}""",
|
|
39
40
|
}
|
|
@@ -58,7 +59,7 @@ def prompt_and_install_custom_server(manager) -> bool:
|
|
|
58
59
|
|
|
59
60
|
# Get server name
|
|
60
61
|
try:
|
|
61
|
-
server_name =
|
|
62
|
+
server_name = safe_input(" Server name: ")
|
|
62
63
|
if not server_name:
|
|
63
64
|
emit_warning("Server name is required")
|
|
64
65
|
return False
|
|
@@ -71,9 +72,7 @@ def prompt_and_install_custom_server(manager) -> bool:
|
|
|
71
72
|
existing = find_server_id_by_name(manager, server_name)
|
|
72
73
|
if existing:
|
|
73
74
|
try:
|
|
74
|
-
override =
|
|
75
|
-
f" Server '{server_name}' exists. Override? [y/N]: "
|
|
76
|
-
).strip()
|
|
75
|
+
override = safe_input(f" Server '{server_name}' exists. Override? [y/N]: ")
|
|
77
76
|
if not override.lower().startswith("y"):
|
|
78
77
|
emit_warning("Cancelled")
|
|
79
78
|
return False
|
|
@@ -89,7 +88,7 @@ def prompt_and_install_custom_server(manager) -> bool:
|
|
|
89
88
|
emit_info(" 3. 📡 sse - Server-Sent Events\n")
|
|
90
89
|
|
|
91
90
|
try:
|
|
92
|
-
type_choice =
|
|
91
|
+
type_choice = safe_input(" Enter choice [1-3]: ")
|
|
93
92
|
except (KeyboardInterrupt, EOFError):
|
|
94
93
|
emit_info("")
|
|
95
94
|
emit_warning("Cancelled")
|
|
@@ -115,8 +114,8 @@ def prompt_and_install_custom_server(manager) -> bool:
|
|
|
115
114
|
empty_count = 0
|
|
116
115
|
try:
|
|
117
116
|
while True:
|
|
118
|
-
line =
|
|
119
|
-
if line
|
|
117
|
+
line = safe_input("")
|
|
118
|
+
if line == "":
|
|
120
119
|
empty_count += 1
|
|
121
120
|
if empty_count >= 2:
|
|
122
121
|
break
|
|
@@ -12,7 +12,6 @@ from rich.text import Text
|
|
|
12
12
|
|
|
13
13
|
from code_puppy.messaging import emit_info
|
|
14
14
|
|
|
15
|
-
from .add_command import AddCommand
|
|
16
15
|
from .base import MCPCommandBase
|
|
17
16
|
from .edit_command import EditCommand
|
|
18
17
|
from .help_command import HelpCommand
|
|
@@ -63,7 +62,6 @@ class MCPCommandHandler(MCPCommandBase):
|
|
|
63
62
|
"restart": RestartCommand(),
|
|
64
63
|
"status": StatusCommand(),
|
|
65
64
|
"test": TestCommand(),
|
|
66
|
-
"add": AddCommand(),
|
|
67
65
|
"edit": EditCommand(),
|
|
68
66
|
"remove": RemoveCommand(),
|
|
69
67
|
"logs": LogsCommand(),
|
|
@@ -101,10 +101,6 @@ class HelpCommand(MCPCommandBase):
|
|
|
101
101
|
Text("/mcp logs", style="cyan")
|
|
102
102
|
+ Text(" <name> [limit] Show recent events (default limit: 10)")
|
|
103
103
|
)
|
|
104
|
-
help_lines.append(
|
|
105
|
-
Text("/mcp add", style="cyan")
|
|
106
|
-
+ Text(" [json] Add new server (JSON or wizard)")
|
|
107
|
-
)
|
|
108
104
|
help_lines.append(
|
|
109
105
|
Text("/mcp edit", style="cyan")
|
|
110
106
|
+ Text(" <name> Edit existing server config")
|
|
@@ -134,7 +130,7 @@ class HelpCommand(MCPCommandBase):
|
|
|
134
130
|
/mcp start-all # Start all servers at once
|
|
135
131
|
/mcp stop-all # Stop all running servers
|
|
136
132
|
/mcp edit filesystem # Edit an existing server config
|
|
137
|
-
/mcp
|
|
133
|
+
/mcp remove filesystem # Remove a server"""
|
|
138
134
|
help_lines.append(Text(examples_text, style="dim"))
|
|
139
135
|
|
|
140
136
|
# Combine all lines
|
|
@@ -3,7 +3,6 @@ MCP Start Command - Starts a specific MCP server.
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
import logging
|
|
6
|
-
import time
|
|
7
6
|
from typing import List, Optional
|
|
8
7
|
|
|
9
8
|
from rich.text import Text
|
|
@@ -23,6 +22,7 @@ class StartCommand(MCPCommandBase):
|
|
|
23
22
|
Command handler for starting MCP servers.
|
|
24
23
|
|
|
25
24
|
Starts a specific MCP server by name and reloads the agent.
|
|
25
|
+
The server subprocess starts asynchronously in the background.
|
|
26
26
|
"""
|
|
27
27
|
|
|
28
28
|
def execute(self, args: List[str], group_id: Optional[str] = None) -> None:
|
|
@@ -56,31 +56,49 @@ class StartCommand(MCPCommandBase):
|
|
|
56
56
|
suggest_similar_servers(self.manager, server_name, group_id=group_id)
|
|
57
57
|
return
|
|
58
58
|
|
|
59
|
-
#
|
|
59
|
+
# Get server info for better messaging (safely handle missing method)
|
|
60
|
+
server_type = "unknown"
|
|
61
|
+
try:
|
|
62
|
+
if hasattr(self.manager, "get_server_by_name"):
|
|
63
|
+
server_config = self.manager.get_server_by_name(server_name)
|
|
64
|
+
server_type = (
|
|
65
|
+
getattr(server_config, "type", "unknown")
|
|
66
|
+
if server_config
|
|
67
|
+
else "unknown"
|
|
68
|
+
)
|
|
69
|
+
except Exception:
|
|
70
|
+
pass # Default to unknown type if we can't determine it
|
|
71
|
+
|
|
72
|
+
# Start the server (schedules async start in background)
|
|
60
73
|
success = self.manager.start_server_sync(server_id)
|
|
61
74
|
|
|
62
75
|
if success:
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
if server_type == "stdio":
|
|
77
|
+
# Stdio servers start subprocess asynchronously
|
|
78
|
+
emit_success(
|
|
79
|
+
f"🚀 Starting server: {server_name} (subprocess starting in background)",
|
|
80
|
+
message_group=group_id,
|
|
81
|
+
)
|
|
82
|
+
emit_info(
|
|
83
|
+
Text.from_markup(
|
|
84
|
+
"[dim]Tip: Use /mcp status to check if the server is fully initialized[/dim]"
|
|
85
|
+
),
|
|
86
|
+
message_group=group_id,
|
|
87
|
+
)
|
|
88
|
+
else:
|
|
89
|
+
# SSE/HTTP servers connect on first use
|
|
90
|
+
emit_success(
|
|
91
|
+
f"✅ Enabled server: {server_name}",
|
|
92
|
+
message_group=group_id,
|
|
93
|
+
)
|
|
78
94
|
|
|
79
95
|
# Reload the agent to pick up the newly enabled server
|
|
96
|
+
# NOTE: We don't block or wait - the server will be ready
|
|
97
|
+
# when the next prompt runs (pydantic-ai handles connection)
|
|
80
98
|
try:
|
|
81
99
|
agent = get_current_agent()
|
|
82
100
|
agent.reload_code_generation_agent()
|
|
83
|
-
#
|
|
101
|
+
# Clear MCP tool cache - it will be repopulated on next run
|
|
84
102
|
agent.update_mcp_tool_cache_sync()
|
|
85
103
|
emit_info(
|
|
86
104
|
"Agent reloaded with updated servers",
|
|
@@ -122,7 +122,6 @@ def slide_mcp() -> str:
|
|
|
122
122
|
content += "[white]Supercharge with external tools![/white]\n\n"
|
|
123
123
|
content += "[green]Commands:[/green]\n"
|
|
124
124
|
content += " [cyan]/mcp install[/cyan] Browse catalog\n"
|
|
125
|
-
content += " [cyan]/mcp add[/cyan] Add custom server\n"
|
|
126
125
|
content += " [cyan]/mcp list[/cyan] See your servers\n\n"
|
|
127
126
|
content += "[yellow]🌟 Popular picks:[/yellow]\n"
|
|
128
127
|
content += " • GitHub integration\n"
|
|
@@ -648,30 +648,36 @@ async def get_input_with_combined_completion(
|
|
|
648
648
|
else:
|
|
649
649
|
event.current_buffer.validate_and_handle()
|
|
650
650
|
|
|
651
|
-
# Handle bracketed paste
|
|
652
|
-
#
|
|
651
|
+
# Handle bracketed paste - smart detection for text vs images.
|
|
652
|
+
# Most terminals (Windows included!) send Ctrl+V through bracketed paste.
|
|
653
|
+
# - If there's meaningful text content → paste as text (drag-and-drop file paths, copied text)
|
|
654
|
+
# - If text is empty/whitespace → check for clipboard image (image paste on Windows)
|
|
653
655
|
@bindings.add(Keys.BracketedPaste)
|
|
654
656
|
def handle_bracketed_paste(event):
|
|
655
|
-
"""Handle bracketed paste -
|
|
656
|
-
# The pasted data is in event.data
|
|
657
|
+
"""Handle bracketed paste - smart text vs image detection."""
|
|
657
658
|
pasted_data = event.data
|
|
658
659
|
|
|
659
|
-
#
|
|
660
|
+
# If we have meaningful text content, paste it (don't check for images)
|
|
661
|
+
# This handles drag-and-drop file paths and normal text paste
|
|
662
|
+
if pasted_data and pasted_data.strip():
|
|
663
|
+
# Normalize Windows line endings to Unix style
|
|
664
|
+
sanitized_data = pasted_data.replace("\r\n", "\n").replace("\r", "\n")
|
|
665
|
+
event.app.current_buffer.insert_text(sanitized_data)
|
|
666
|
+
return
|
|
667
|
+
|
|
668
|
+
# No meaningful text - check if clipboard has an image (Windows image paste!)
|
|
660
669
|
try:
|
|
661
670
|
if has_image_in_clipboard():
|
|
662
671
|
placeholder = capture_clipboard_image_to_pending()
|
|
663
672
|
if placeholder:
|
|
664
673
|
event.app.current_buffer.insert_text(placeholder + " ")
|
|
665
|
-
# The placeholder itself is visible feedback - no need for extra output
|
|
666
|
-
# Use bell for audible feedback (works in most terminals)
|
|
667
674
|
event.app.output.bell()
|
|
668
|
-
return
|
|
675
|
+
return
|
|
669
676
|
except Exception:
|
|
670
677
|
pass
|
|
671
678
|
|
|
672
|
-
#
|
|
679
|
+
# Fallback: if there was whitespace-only data, paste it
|
|
673
680
|
if pasted_data:
|
|
674
|
-
# Normalize Windows line endings to Unix style
|
|
675
681
|
sanitized_data = pasted_data.replace("\r\n", "\n").replace("\r", "\n")
|
|
676
682
|
event.app.current_buffer.insert_text(sanitized_data)
|
|
677
683
|
|
code_puppy/command_line/utils.py
CHANGED
|
@@ -37,3 +37,57 @@ def make_directory_table(path: str = None) -> Table:
|
|
|
37
37
|
for f in sorted(files):
|
|
38
38
|
table.add_row("[yellow]file[/yellow]", f"{f}")
|
|
39
39
|
return table
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _reset_windows_console() -> None:
|
|
43
|
+
"""Reset Windows console to normal input mode.
|
|
44
|
+
|
|
45
|
+
After a prompt_toolkit Application exits on Windows, the console can be
|
|
46
|
+
left in a weird state where Enter doesn't work properly. This resets it.
|
|
47
|
+
"""
|
|
48
|
+
import sys
|
|
49
|
+
|
|
50
|
+
if sys.platform != "win32":
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
import ctypes
|
|
55
|
+
|
|
56
|
+
kernel32 = ctypes.windll.kernel32
|
|
57
|
+
# Get handle to stdin
|
|
58
|
+
STD_INPUT_HANDLE = -10
|
|
59
|
+
handle = kernel32.GetStdHandle(STD_INPUT_HANDLE)
|
|
60
|
+
|
|
61
|
+
# Enable line input and echo (normal console mode)
|
|
62
|
+
# ENABLE_LINE_INPUT = 0x0002
|
|
63
|
+
# ENABLE_ECHO_INPUT = 0x0004
|
|
64
|
+
# ENABLE_PROCESSED_INPUT = 0x0001
|
|
65
|
+
NORMAL_MODE = 0x0007 # Line input + echo + processed
|
|
66
|
+
kernel32.SetConsoleMode(handle, NORMAL_MODE)
|
|
67
|
+
except Exception:
|
|
68
|
+
pass # Silently ignore errors - this is best-effort
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def safe_input(prompt_text: str = "") -> str:
|
|
72
|
+
"""Cross-platform safe input that works after prompt_toolkit Applications.
|
|
73
|
+
|
|
74
|
+
On Windows, raw input() can fail after a prompt_toolkit Application exits
|
|
75
|
+
because the terminal can be left in a weird state. This function resets
|
|
76
|
+
the Windows console mode before calling input().
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
prompt_text: The prompt to display to the user
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
The user's input string (stripped)
|
|
83
|
+
|
|
84
|
+
Raises:
|
|
85
|
+
KeyboardInterrupt: If user presses Ctrl+C
|
|
86
|
+
EOFError: If user presses Ctrl+D/Ctrl+Z
|
|
87
|
+
"""
|
|
88
|
+
# Reset Windows console to normal mode before reading input
|
|
89
|
+
_reset_windows_console()
|
|
90
|
+
|
|
91
|
+
# Use standard input() - now that console is reset, it should work
|
|
92
|
+
result = input(prompt_text)
|
|
93
|
+
return result.strip() if result else ""
|
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
|
|
|
@@ -85,7 +98,6 @@ _CURRENT_AUTOSAVE_ID: Optional[str] = None
|
|
|
85
98
|
_model_validation_cache = {}
|
|
86
99
|
_default_model_cache = None
|
|
87
100
|
_default_vision_model_cache = None
|
|
88
|
-
_default_vqa_model_cache = None
|
|
89
101
|
|
|
90
102
|
|
|
91
103
|
def ensure_config_exists():
|
|
@@ -208,6 +220,9 @@ def get_config_keys():
|
|
|
208
220
|
"diff_context_lines",
|
|
209
221
|
"default_agent",
|
|
210
222
|
"temperature",
|
|
223
|
+
"frontend_emitter_enabled",
|
|
224
|
+
"frontend_emitter_max_recent_events",
|
|
225
|
+
"frontend_emitter_queue_size",
|
|
211
226
|
]
|
|
212
227
|
# Add DBOS control key
|
|
213
228
|
default_keys.append("enable_dbos")
|
|
@@ -237,6 +252,22 @@ def set_config_value(key: str, value: str):
|
|
|
237
252
|
config.write(f)
|
|
238
253
|
|
|
239
254
|
|
|
255
|
+
# Alias for API compatibility
|
|
256
|
+
def set_value(key: str, value: str) -> None:
|
|
257
|
+
"""Set a config value. Alias for set_config_value."""
|
|
258
|
+
set_config_value(key, value)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def reset_value(key: str) -> None:
|
|
262
|
+
"""Remove a key from the config file, resetting it to default."""
|
|
263
|
+
config = configparser.ConfigParser()
|
|
264
|
+
config.read(CONFIG_FILE)
|
|
265
|
+
if DEFAULT_SECTION in config and key in config[DEFAULT_SECTION]:
|
|
266
|
+
del config[DEFAULT_SECTION][key]
|
|
267
|
+
with open(CONFIG_FILE, "w") as f:
|
|
268
|
+
config.write(f)
|
|
269
|
+
|
|
270
|
+
|
|
240
271
|
# --- MODEL STICKY EXTENSION STARTS HERE ---
|
|
241
272
|
def load_mcp_server_configs():
|
|
242
273
|
"""
|
|
@@ -326,47 +357,6 @@ def _default_vision_model_from_models_json() -> str:
|
|
|
326
357
|
return "gpt-4.1"
|
|
327
358
|
|
|
328
359
|
|
|
329
|
-
def _default_vqa_model_from_models_json() -> str:
|
|
330
|
-
"""Select a default VQA-capable model, preferring vision-ready options."""
|
|
331
|
-
global _default_vqa_model_cache
|
|
332
|
-
|
|
333
|
-
if _default_vqa_model_cache is not None:
|
|
334
|
-
return _default_vqa_model_cache
|
|
335
|
-
|
|
336
|
-
try:
|
|
337
|
-
from code_puppy.model_factory import ModelFactory
|
|
338
|
-
|
|
339
|
-
models_config = ModelFactory.load_config()
|
|
340
|
-
if models_config:
|
|
341
|
-
# Allow explicit VQA hints if present
|
|
342
|
-
for name, config in models_config.items():
|
|
343
|
-
if config.get("supports_vqa"):
|
|
344
|
-
_default_vqa_model_cache = name
|
|
345
|
-
return name
|
|
346
|
-
|
|
347
|
-
# Reuse multimodal heuristics before falling back to generic default
|
|
348
|
-
preferred_candidates = (
|
|
349
|
-
"gpt-4.1",
|
|
350
|
-
"gpt-4.1-mini",
|
|
351
|
-
"claude-4-0-sonnet",
|
|
352
|
-
"gemini-2.5-flash-preview-05-20",
|
|
353
|
-
"gpt-4.1-nano",
|
|
354
|
-
)
|
|
355
|
-
for candidate in preferred_candidates:
|
|
356
|
-
if candidate in models_config:
|
|
357
|
-
_default_vqa_model_cache = candidate
|
|
358
|
-
return candidate
|
|
359
|
-
|
|
360
|
-
_default_vqa_model_cache = _default_model_from_models_json()
|
|
361
|
-
return _default_vqa_model_cache
|
|
362
|
-
|
|
363
|
-
_default_vqa_model_cache = "gpt-4.1"
|
|
364
|
-
return "gpt-4.1"
|
|
365
|
-
except Exception:
|
|
366
|
-
_default_vqa_model_cache = "gpt-4.1"
|
|
367
|
-
return "gpt-4.1"
|
|
368
|
-
|
|
369
|
-
|
|
370
360
|
def _validate_model_exists(model_name: str) -> bool:
|
|
371
361
|
"""Check if a model exists in models.json with caching to avoid redundant calls."""
|
|
372
362
|
global _model_validation_cache
|
|
@@ -392,15 +382,10 @@ def _validate_model_exists(model_name: str) -> bool:
|
|
|
392
382
|
|
|
393
383
|
def clear_model_cache():
|
|
394
384
|
"""Clear the model validation cache. Call this when models.json changes."""
|
|
395
|
-
global
|
|
396
|
-
_model_validation_cache, \
|
|
397
|
-
_default_model_cache, \
|
|
398
|
-
_default_vision_model_cache, \
|
|
399
|
-
_default_vqa_model_cache
|
|
385
|
+
global _model_validation_cache, _default_model_cache, _default_vision_model_cache
|
|
400
386
|
_model_validation_cache.clear()
|
|
401
387
|
_default_model_cache = None
|
|
402
388
|
_default_vision_model_cache = None
|
|
403
|
-
_default_vqa_model_cache = None
|
|
404
389
|
|
|
405
390
|
|
|
406
391
|
def model_supports_setting(model_name: str, setting: str) -> bool:
|
|
@@ -471,20 +456,6 @@ def set_model_name(model: str):
|
|
|
471
456
|
clear_model_cache()
|
|
472
457
|
|
|
473
458
|
|
|
474
|
-
def get_vqa_model_name() -> str:
|
|
475
|
-
"""Return the configured VQA model, falling back to an inferred default."""
|
|
476
|
-
stored_model = get_value("vqa_model_name")
|
|
477
|
-
if stored_model and _validate_model_exists(stored_model):
|
|
478
|
-
return stored_model
|
|
479
|
-
return _default_vqa_model_from_models_json()
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
def set_vqa_model_name(model: str):
|
|
483
|
-
"""Persist the configured VQA model name and refresh caches."""
|
|
484
|
-
set_config_value("vqa_model_name", model or "")
|
|
485
|
-
clear_model_cache()
|
|
486
|
-
|
|
487
|
-
|
|
488
459
|
def get_puppy_token():
|
|
489
460
|
"""Returns the puppy_token from config, or None if not set."""
|
|
490
461
|
return get_value("puppy_token")
|
|
@@ -1291,6 +1262,8 @@ DEFAULT_BANNER_COLORS = {
|
|
|
1291
1262
|
"invoke_agent": "deep_pink4", # Ruby - agent invocation
|
|
1292
1263
|
"subagent_response": "sea_green3", # Emerald - sub-agent success
|
|
1293
1264
|
"list_agents": "dark_slate_gray3", # Slate - neutral listing
|
|
1265
|
+
# Browser/Terminal tools - same color as edit_file (gold)
|
|
1266
|
+
"terminal_tool": "dark_goldenrod", # Gold - browser terminal operations
|
|
1294
1267
|
}
|
|
1295
1268
|
|
|
1296
1269
|
|
|
@@ -1584,3 +1557,34 @@ def set_default_agent(agent_name: str) -> None:
|
|
|
1584
1557
|
agent_name: The name of the agent to set as default.
|
|
1585
1558
|
"""
|
|
1586
1559
|
set_config_value("default_agent", agent_name)
|
|
1560
|
+
|
|
1561
|
+
|
|
1562
|
+
# --- FRONTEND EMITTER CONFIGURATION ---
|
|
1563
|
+
def get_frontend_emitter_enabled() -> bool:
|
|
1564
|
+
"""Check if frontend emitter is enabled."""
|
|
1565
|
+
val = get_value("frontend_emitter_enabled")
|
|
1566
|
+
if val is None:
|
|
1567
|
+
return True # Enabled by default
|
|
1568
|
+
return str(val).lower() in ("1", "true", "yes", "on")
|
|
1569
|
+
|
|
1570
|
+
|
|
1571
|
+
def get_frontend_emitter_max_recent_events() -> int:
|
|
1572
|
+
"""Get max number of recent events to buffer."""
|
|
1573
|
+
val = get_value("frontend_emitter_max_recent_events")
|
|
1574
|
+
if val is None:
|
|
1575
|
+
return 100
|
|
1576
|
+
try:
|
|
1577
|
+
return int(val)
|
|
1578
|
+
except ValueError:
|
|
1579
|
+
return 100
|
|
1580
|
+
|
|
1581
|
+
|
|
1582
|
+
def get_frontend_emitter_queue_size() -> int:
|
|
1583
|
+
"""Get max subscriber queue size."""
|
|
1584
|
+
val = get_value("frontend_emitter_queue_size")
|
|
1585
|
+
if val is None:
|
|
1586
|
+
return 100
|
|
1587
|
+
try:
|
|
1588
|
+
return int(val)
|
|
1589
|
+
except ValueError:
|
|
1590
|
+
return 100
|
|
@@ -108,10 +108,17 @@ class AsyncServerLifecycleManager:
|
|
|
108
108
|
|
|
109
109
|
try:
|
|
110
110
|
logger.info(f"Starting server lifecycle for {server_id}")
|
|
111
|
+
logger.info(
|
|
112
|
+
f"Server {server_id} _running_count before enter: {getattr(server, '_running_count', 'N/A')}"
|
|
113
|
+
)
|
|
111
114
|
|
|
112
115
|
# Enter the server's context
|
|
113
116
|
await exit_stack.enter_async_context(server)
|
|
114
117
|
|
|
118
|
+
logger.info(
|
|
119
|
+
f"Server {server_id} _running_count after enter: {getattr(server, '_running_count', 'N/A')}"
|
|
120
|
+
)
|
|
121
|
+
|
|
115
122
|
# Store the managed context
|
|
116
123
|
async with self._lock:
|
|
117
124
|
self._servers[server_id] = ManagedServerContext(
|
|
@@ -122,26 +129,50 @@ class AsyncServerLifecycleManager:
|
|
|
122
129
|
task=asyncio.current_task(),
|
|
123
130
|
)
|
|
124
131
|
|
|
125
|
-
logger.info(
|
|
132
|
+
logger.info(
|
|
133
|
+
f"Server {server_id} started successfully and stored in _servers"
|
|
134
|
+
)
|
|
126
135
|
|
|
127
136
|
# Keep the task alive until cancelled
|
|
137
|
+
loop_count = 0
|
|
128
138
|
while True:
|
|
129
139
|
await asyncio.sleep(1)
|
|
140
|
+
loop_count += 1
|
|
130
141
|
|
|
131
142
|
# Check if server is still running
|
|
132
|
-
|
|
133
|
-
|
|
143
|
+
running_count = getattr(server, "_running_count", "N/A")
|
|
144
|
+
is_running = server.is_running
|
|
145
|
+
logger.debug(
|
|
146
|
+
f"Server {server_id} heartbeat #{loop_count}: "
|
|
147
|
+
f"is_running={is_running}, _running_count={running_count}"
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
if not is_running:
|
|
151
|
+
logger.warning(
|
|
152
|
+
f"Server {server_id} stopped unexpectedly! "
|
|
153
|
+
f"_running_count={running_count}"
|
|
154
|
+
)
|
|
134
155
|
break
|
|
135
156
|
|
|
136
157
|
except asyncio.CancelledError:
|
|
137
158
|
logger.info(f"Server {server_id} lifecycle task cancelled")
|
|
138
159
|
raise
|
|
139
160
|
except Exception as e:
|
|
140
|
-
logger.error(f"Error in server {server_id} lifecycle: {e}")
|
|
161
|
+
logger.error(f"Error in server {server_id} lifecycle: {e}", exc_info=True)
|
|
141
162
|
finally:
|
|
163
|
+
running_count = getattr(server, "_running_count", "N/A")
|
|
164
|
+
logger.info(
|
|
165
|
+
f"Server {server_id} lifecycle ending, _running_count={running_count}"
|
|
166
|
+
)
|
|
167
|
+
|
|
142
168
|
# Clean up the context
|
|
143
169
|
await exit_stack.aclose()
|
|
144
170
|
|
|
171
|
+
running_count_after = getattr(server, "_running_count", "N/A")
|
|
172
|
+
logger.info(
|
|
173
|
+
f"Server {server_id} context closed, _running_count={running_count_after}"
|
|
174
|
+
)
|
|
175
|
+
|
|
145
176
|
# Remove from managed servers
|
|
146
177
|
async with self._lock:
|
|
147
178
|
if server_id in self._servers:
|