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.
Files changed (86) hide show
  1. code_puppy/agents/__init__.py +2 -0
  2. code_puppy/agents/agent_manager.py +49 -0
  3. code_puppy/agents/agent_pack_leader.py +383 -0
  4. code_puppy/agents/agent_qa_kitten.py +12 -7
  5. code_puppy/agents/agent_terminal_qa.py +323 -0
  6. code_puppy/agents/base_agent.py +34 -252
  7. code_puppy/agents/event_stream_handler.py +350 -0
  8. code_puppy/agents/pack/__init__.py +34 -0
  9. code_puppy/agents/pack/bloodhound.py +304 -0
  10. code_puppy/agents/pack/husky.py +321 -0
  11. code_puppy/agents/pack/retriever.py +393 -0
  12. code_puppy/agents/pack/shepherd.py +348 -0
  13. code_puppy/agents/pack/terrier.py +287 -0
  14. code_puppy/agents/pack/watchdog.py +367 -0
  15. code_puppy/agents/subagent_stream_handler.py +276 -0
  16. code_puppy/api/__init__.py +13 -0
  17. code_puppy/api/app.py +169 -0
  18. code_puppy/api/main.py +21 -0
  19. code_puppy/api/pty_manager.py +446 -0
  20. code_puppy/api/routers/__init__.py +12 -0
  21. code_puppy/api/routers/agents.py +36 -0
  22. code_puppy/api/routers/commands.py +217 -0
  23. code_puppy/api/routers/config.py +74 -0
  24. code_puppy/api/routers/sessions.py +232 -0
  25. code_puppy/api/templates/terminal.html +361 -0
  26. code_puppy/api/websocket.py +154 -0
  27. code_puppy/callbacks.py +73 -0
  28. code_puppy/claude_cache_client.py +249 -34
  29. code_puppy/cli_runner.py +4 -3
  30. code_puppy/command_line/add_model_menu.py +8 -9
  31. code_puppy/command_line/core_commands.py +85 -0
  32. code_puppy/command_line/mcp/catalog_server_installer.py +5 -6
  33. code_puppy/command_line/mcp/custom_server_form.py +54 -19
  34. code_puppy/command_line/mcp/custom_server_installer.py +8 -9
  35. code_puppy/command_line/mcp/handler.py +0 -2
  36. code_puppy/command_line/mcp/help_command.py +1 -5
  37. code_puppy/command_line/mcp/start_command.py +36 -18
  38. code_puppy/command_line/onboarding_slides.py +0 -1
  39. code_puppy/command_line/prompt_toolkit_completion.py +16 -10
  40. code_puppy/command_line/utils.py +54 -0
  41. code_puppy/config.py +66 -62
  42. code_puppy/mcp_/async_lifecycle.py +35 -4
  43. code_puppy/mcp_/managed_server.py +49 -20
  44. code_puppy/mcp_/manager.py +81 -52
  45. code_puppy/messaging/__init__.py +15 -0
  46. code_puppy/messaging/message_queue.py +11 -23
  47. code_puppy/messaging/messages.py +27 -0
  48. code_puppy/messaging/queue_console.py +1 -1
  49. code_puppy/messaging/rich_renderer.py +36 -1
  50. code_puppy/messaging/spinner/__init__.py +20 -2
  51. code_puppy/messaging/subagent_console.py +461 -0
  52. code_puppy/model_utils.py +54 -0
  53. code_puppy/plugins/antigravity_oauth/antigravity_model.py +90 -19
  54. code_puppy/plugins/antigravity_oauth/transport.py +1 -0
  55. code_puppy/plugins/frontend_emitter/__init__.py +25 -0
  56. code_puppy/plugins/frontend_emitter/emitter.py +121 -0
  57. code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
  58. code_puppy/prompts/antigravity_system_prompt.md +1 -0
  59. code_puppy/status_display.py +6 -2
  60. code_puppy/tools/__init__.py +37 -1
  61. code_puppy/tools/agent_tools.py +139 -36
  62. code_puppy/tools/browser/__init__.py +37 -0
  63. code_puppy/tools/browser/browser_control.py +6 -6
  64. code_puppy/tools/browser/browser_interactions.py +21 -20
  65. code_puppy/tools/browser/browser_locators.py +9 -9
  66. code_puppy/tools/browser/browser_navigation.py +7 -7
  67. code_puppy/tools/browser/browser_screenshot.py +78 -140
  68. code_puppy/tools/browser/browser_scripts.py +15 -13
  69. code_puppy/tools/browser/camoufox_manager.py +226 -64
  70. code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
  71. code_puppy/tools/browser/terminal_command_tools.py +521 -0
  72. code_puppy/tools/browser/terminal_screenshot_tools.py +556 -0
  73. code_puppy/tools/browser/terminal_tools.py +525 -0
  74. code_puppy/tools/command_runner.py +292 -101
  75. code_puppy/tools/common.py +176 -1
  76. code_puppy/tools/display.py +84 -0
  77. code_puppy/tools/subagent_context.py +158 -0
  78. {code_puppy-0.0.341.dist-info → code_puppy-0.0.361.dist-info}/METADATA +13 -11
  79. {code_puppy-0.0.341.dist-info → code_puppy-0.0.361.dist-info}/RECORD +84 -53
  80. code_puppy/command_line/mcp/add_command.py +0 -170
  81. code_puppy/tools/browser/vqa_agent.py +0 -90
  82. {code_puppy-0.0.341.data → code_puppy-0.0.361.data}/data/code_puppy/models.json +0 -0
  83. {code_puppy-0.0.341.data → code_puppy-0.0.361.data}/data/code_puppy/models_dev_api.json +0 -0
  84. {code_puppy-0.0.341.dist-info → code_puppy-0.0.361.dist-info}/WHEEL +0 -0
  85. {code_puppy-0.0.341.dist-info → code_puppy-0.0.361.dist-info}/entry_points.txt +0 -0
  86. {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 YOUR_API_KEY",
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 YOUR_API_KEY"
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 = input(" Server name: ").strip()
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 = input(
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 = input(" Enter choice [1-3]: ").strip()
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 = input()
119
- if line.strip() == "":
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 add {"name": "test", "type": "stdio", "command": "echo"}"""
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
- # Start the server (enable and start process)
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
- # This and subsequent messages will auto-group with the first message
64
- emit_success(
65
- f"Started server: {server_name}",
66
- message_group=group_id,
67
- )
68
-
69
- # Give async tasks a moment to complete
70
- try:
71
- import asyncio
72
-
73
- asyncio.get_running_loop() # Check if in async context
74
- # If we're in async context, wait a bit for server to start
75
- time.sleep(0.5) # Small delay to let async tasks progress
76
- except RuntimeError:
77
- pass # No async loop, server will start when agent uses it
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
- # Update MCP tool cache immediately so token counts reflect the change
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 (triggered by most terminal Cmd+V / Ctrl+V)
652
- # This is the PRIMARY paste handler - works with Cmd+V on macOS terminals
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 - works with Cmd+V on macOS terminals."""
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
- # Check if clipboard has an image (the pasted text might just be empty or a file path)
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 # Don't also paste the text data
675
+ return
669
676
  except Exception:
670
677
  pass
671
678
 
672
- # No image - insert the pasted text as normal, sanitizing Windows newlines
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
 
@@ -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(f"Server {server_id} started successfully")
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
- if not server.is_running:
133
- logger.warning(f"Server {server_id} stopped unexpectedly")
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: