code-puppy 0.0.348__py3-none-any.whl → 0.0.372__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 (87) hide show
  1. code_puppy/agents/__init__.py +8 -0
  2. code_puppy/agents/agent_manager.py +272 -1
  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 +11 -8
  7. code_puppy/agents/event_stream_handler.py +101 -8
  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/chatgpt_codex_client.py +53 -0
  29. code_puppy/claude_cache_client.py +294 -41
  30. code_puppy/command_line/add_model_menu.py +13 -4
  31. code_puppy/command_line/agent_menu.py +662 -0
  32. code_puppy/command_line/core_commands.py +89 -112
  33. code_puppy/command_line/model_picker_completion.py +3 -20
  34. code_puppy/command_line/model_settings_menu.py +21 -3
  35. code_puppy/config.py +145 -70
  36. code_puppy/gemini_model.py +706 -0
  37. code_puppy/http_utils.py +6 -3
  38. code_puppy/messaging/__init__.py +15 -0
  39. code_puppy/messaging/messages.py +27 -0
  40. code_puppy/messaging/queue_console.py +1 -1
  41. code_puppy/messaging/rich_renderer.py +36 -1
  42. code_puppy/messaging/spinner/__init__.py +20 -2
  43. code_puppy/messaging/subagent_console.py +461 -0
  44. code_puppy/model_factory.py +50 -16
  45. code_puppy/model_switching.py +63 -0
  46. code_puppy/model_utils.py +27 -24
  47. code_puppy/models.json +12 -12
  48. code_puppy/plugins/antigravity_oauth/antigravity_model.py +206 -172
  49. code_puppy/plugins/antigravity_oauth/register_callbacks.py +15 -8
  50. code_puppy/plugins/antigravity_oauth/transport.py +236 -45
  51. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +2 -2
  52. code_puppy/plugins/claude_code_oauth/register_callbacks.py +2 -30
  53. code_puppy/plugins/claude_code_oauth/utils.py +4 -1
  54. code_puppy/plugins/frontend_emitter/__init__.py +25 -0
  55. code_puppy/plugins/frontend_emitter/emitter.py +121 -0
  56. code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
  57. code_puppy/prompts/antigravity_system_prompt.md +1 -0
  58. code_puppy/pydantic_patches.py +52 -0
  59. code_puppy/status_display.py +6 -2
  60. code_puppy/tools/__init__.py +37 -1
  61. code_puppy/tools/agent_tools.py +83 -33
  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_manager.py +316 -0
  67. code_puppy/tools/browser/browser_navigation.py +7 -7
  68. code_puppy/tools/browser/browser_screenshot.py +78 -140
  69. code_puppy/tools/browser/browser_scripts.py +15 -13
  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.348.data → code_puppy-0.0.372.data}/data/code_puppy/models.json +12 -12
  79. {code_puppy-0.0.348.dist-info → code_puppy-0.0.372.dist-info}/METADATA +17 -16
  80. {code_puppy-0.0.348.dist-info → code_puppy-0.0.372.dist-info}/RECORD +84 -51
  81. code_puppy/prompts/codex_system_prompt.md +0 -310
  82. code_puppy/tools/browser/camoufox_manager.py +0 -235
  83. code_puppy/tools/browser/vqa_agent.py +0 -90
  84. {code_puppy-0.0.348.data → code_puppy-0.0.372.data}/data/code_puppy/models_dev_api.json +0 -0
  85. {code_puppy-0.0.348.dist-info → code_puppy-0.0.372.dist-info}/WHEEL +0 -0
  86. {code_puppy-0.0.348.dist-info → code_puppy-0.0.372.dist-info}/entry_points.txt +0 -0
  87. {code_puppy-0.0.348.dist-info → code_puppy-0.0.372.dist-info}/licenses/LICENSE +0 -0
@@ -6,6 +6,7 @@ discovered by the command registry system.
6
6
 
7
7
  import os
8
8
 
9
+ from code_puppy.command_line.agent_menu import interactive_agent_picker
9
10
  from code_puppy.command_line.command_registry import register_command
10
11
  from code_puppy.command_line.model_picker_completion import update_model_in_input
11
12
  from code_puppy.command_line.motd import print_motd
@@ -168,7 +169,7 @@ def handle_tutorial_command(command: str) -> bool:
168
169
  reset_onboarding,
169
170
  run_onboarding_wizard,
170
171
  )
171
- from code_puppy.config import set_model_name
172
+ from code_puppy.model_switching import set_model_and_reload_agent
172
173
 
173
174
  # Always reset so user can re-run the tutorial anytime
174
175
  reset_onboarding()
@@ -183,7 +184,7 @@ def handle_tutorial_command(command: str) -> bool:
183
184
  from code_puppy.plugins.chatgpt_oauth.oauth_flow import run_oauth_flow
184
185
 
185
186
  run_oauth_flow()
186
- set_model_name("chatgpt-gpt-5.2-codex")
187
+ set_model_and_reload_agent("chatgpt-gpt-5.2-codex")
187
188
  elif result == "claude":
188
189
  emit_info("🔐 Starting Claude Code OAuth flow...")
189
190
  from code_puppy.plugins.claude_code_oauth.register_callbacks import (
@@ -191,7 +192,7 @@ def handle_tutorial_command(command: str) -> bool:
191
192
  )
192
193
 
193
194
  _perform_authentication()
194
- set_model_name("claude-code-claude-opus-4-5-20251101")
195
+ set_model_and_reload_agent("claude-code-claude-opus-4-5-20251101")
195
196
  elif result == "completed":
196
197
  emit_info("🎉 Tutorial complete! Happy coding!")
197
198
  elif result == "skipped":
@@ -398,115 +399,6 @@ def handle_agent_command(command: str) -> bool:
398
399
  return True
399
400
 
400
401
 
401
- async def interactive_agent_picker() -> str | None:
402
- """Show an interactive arrow-key selector to pick an agent (async version).
403
-
404
- Returns:
405
- The selected agent name, or None if cancelled
406
- """
407
- import sys
408
- import time
409
-
410
- from rich.console import Console
411
- from rich.panel import Panel
412
- from rich.text import Text
413
-
414
- from code_puppy.agents import (
415
- get_agent_descriptions,
416
- get_available_agents,
417
- get_current_agent,
418
- )
419
- from code_puppy.tools.command_runner import set_awaiting_user_input
420
- from code_puppy.tools.common import arrow_select_async
421
-
422
- # Load available agents
423
- available_agents = get_available_agents()
424
- descriptions = get_agent_descriptions()
425
- current_agent = get_current_agent()
426
-
427
- # Build choices with current agent indicator and keep track of agent names
428
- choices = []
429
- agent_names = list(available_agents.keys())
430
- for agent_name in agent_names:
431
- display_name = available_agents[agent_name]
432
- if agent_name == current_agent.name:
433
- choices.append(f"✓ {agent_name} - {display_name} (current)")
434
- else:
435
- choices.append(f" {agent_name} - {display_name}")
436
-
437
- # Create preview callback to show agent description
438
- def get_preview(index: int) -> str:
439
- """Get the description for the agent at the given index."""
440
- agent_name = agent_names[index]
441
- description = descriptions.get(agent_name, "No description available")
442
- return description
443
-
444
- # Create panel content
445
- panel_content = Text()
446
- panel_content.append("🐶 Select an agent to use\n", style="bold cyan")
447
- panel_content.append("Current agent: ", style="dim")
448
- panel_content.append(f"{current_agent.name}", style="bold green")
449
- panel_content.append(" - ", style="dim")
450
- panel_content.append(current_agent.display_name, style="bold green")
451
- panel_content.append("\n", style="dim")
452
- panel_content.append(current_agent.description, style="dim italic")
453
-
454
- # Display panel
455
- panel = Panel(
456
- panel_content,
457
- title="[bold white]Agent Selection[/bold white]",
458
- border_style="cyan",
459
- padding=(1, 2),
460
- )
461
-
462
- # Pause spinners BEFORE showing panel
463
- set_awaiting_user_input(True)
464
- time.sleep(0.3) # Let spinners fully stop
465
-
466
- local_console = Console()
467
- emit_info("")
468
- local_console.print(panel)
469
- emit_info("")
470
-
471
- # Flush output before prompt_toolkit takes control
472
- sys.stdout.flush()
473
- sys.stderr.flush()
474
- time.sleep(0.1)
475
-
476
- selected_agent = None
477
-
478
- try:
479
- # Final flush
480
- sys.stdout.flush()
481
-
482
- # Show arrow-key selector with preview (async version)
483
- choice = await arrow_select_async(
484
- "💭 Which agent would you like to use?",
485
- choices,
486
- preview_callback=get_preview,
487
- )
488
-
489
- # Extract agent name from choice (remove prefix and suffix)
490
- if choice:
491
- # Remove the "✓ " or " " prefix and extract agent name (before " - ")
492
- choice_stripped = choice.strip().lstrip("✓").strip()
493
- # Split on " - " and take the first part (agent name)
494
- agent_name = choice_stripped.split(" - ")[0].strip()
495
- # Remove " (current)" suffix if present
496
- if agent_name.endswith(" (current)"):
497
- agent_name = agent_name[:-10].strip()
498
- selected_agent = agent_name
499
-
500
- except (KeyboardInterrupt, EOFError):
501
- emit_error("Cancelled by user")
502
- selected_agent = None
503
-
504
- finally:
505
- set_awaiting_user_input(False)
506
-
507
- return selected_agent
508
-
509
-
510
402
  async def interactive_model_picker() -> str | None:
511
403
  """Show an interactive arrow-key selector to pick a model (async version).
512
404
 
@@ -772,6 +664,91 @@ def handle_mcp_command(command: str) -> bool:
772
664
  return handler.handle_mcp_command(command)
773
665
 
774
666
 
667
+ @register_command(
668
+ name="api",
669
+ description="Manage the Code Puppy API server",
670
+ usage="/api [start|stop|status]",
671
+ category="core",
672
+ detailed_help="Start, stop, or check status of the local FastAPI server for GUI integration.",
673
+ )
674
+ def handle_api_command(command: str) -> bool:
675
+ """Handle the /api command."""
676
+ import os
677
+ import signal
678
+ import subprocess
679
+ import sys
680
+ from pathlib import Path
681
+
682
+ from code_puppy.config import STATE_DIR
683
+ from code_puppy.messaging import emit_error, emit_info, emit_success
684
+
685
+ parts = command.split()
686
+ subcommand = parts[1] if len(parts) > 1 else "status"
687
+
688
+ pid_file = Path(STATE_DIR) / "api_server.pid"
689
+
690
+ if subcommand == "start":
691
+ # Check if already running
692
+ if pid_file.exists():
693
+ try:
694
+ pid = int(pid_file.read_text().strip())
695
+ os.kill(pid, 0) # Check if process exists
696
+ emit_info(f"API server already running (PID {pid})")
697
+ return True
698
+ except (OSError, ValueError):
699
+ pid_file.unlink(missing_ok=True) # Stale PID file
700
+
701
+ # Start the server in background
702
+ emit_info("Starting API server on http://127.0.0.1:8765 ...")
703
+ proc = subprocess.Popen(
704
+ [sys.executable, "-m", "code_puppy.api.main"],
705
+ stdout=subprocess.DEVNULL,
706
+ stderr=subprocess.DEVNULL,
707
+ start_new_session=True,
708
+ )
709
+ pid_file.parent.mkdir(parents=True, exist_ok=True)
710
+ pid_file.write_text(str(proc.pid))
711
+ emit_success(f"API server started (PID {proc.pid})")
712
+ emit_info("Docs available at http://127.0.0.1:8765/docs")
713
+ return True
714
+
715
+ elif subcommand == "stop":
716
+ if not pid_file.exists():
717
+ emit_info("API server is not running")
718
+ return True
719
+
720
+ try:
721
+ pid = int(pid_file.read_text().strip())
722
+ os.kill(pid, signal.SIGTERM)
723
+ pid_file.unlink()
724
+ emit_success(f"API server stopped (PID {pid})")
725
+ except (OSError, ValueError) as e:
726
+ pid_file.unlink(missing_ok=True)
727
+ emit_error(f"Error stopping server: {e}")
728
+ return True
729
+
730
+ elif subcommand == "status":
731
+ if not pid_file.exists():
732
+ emit_info("API server is not running")
733
+ return True
734
+
735
+ try:
736
+ pid = int(pid_file.read_text().strip())
737
+ os.kill(pid, 0) # Check if process exists
738
+ emit_success(f"API server is running (PID {pid})")
739
+ emit_info("URL: http://127.0.0.1:8765")
740
+ emit_info("Docs: http://127.0.0.1:8765/docs")
741
+ except (OSError, ValueError):
742
+ pid_file.unlink(missing_ok=True)
743
+ emit_info("API server is not running (stale PID file removed)")
744
+ return True
745
+
746
+ else:
747
+ emit_error(f"Unknown subcommand: {subcommand}")
748
+ emit_info("Usage: /api [start|stop|status]")
749
+ return True
750
+
751
+
775
752
  @register_command(
776
753
  name="generate-pr-description",
777
754
  description="Generate comprehensive PR description",
@@ -6,8 +6,9 @@ from prompt_toolkit.completion import Completer, Completion
6
6
  from prompt_toolkit.document import Document
7
7
  from prompt_toolkit.history import FileHistory
8
8
 
9
- from code_puppy.config import get_global_model_name, set_model_name
9
+ from code_puppy.config import get_global_model_name
10
10
  from code_puppy.model_factory import ModelFactory
11
+ from code_puppy.model_switching import set_model_and_reload_agent
11
12
 
12
13
 
13
14
  def load_model_names():
@@ -28,25 +29,7 @@ def set_active_model(model_name: str):
28
29
  """
29
30
  Sets the active model name by updating the config (for persistence).
30
31
  """
31
- from code_puppy.messaging import emit_info, emit_warning
32
-
33
- set_model_name(model_name)
34
- # Reload the currently active agent so the new model takes effect immediately
35
- try:
36
- from code_puppy.agents import get_current_agent
37
-
38
- current_agent = get_current_agent()
39
- # JSON agents may need to refresh their config before reload
40
- if hasattr(current_agent, "refresh_config"):
41
- try:
42
- current_agent.refresh_config()
43
- except Exception:
44
- # Non-fatal, continue to reload
45
- ...
46
- current_agent.reload_code_generation_agent()
47
- emit_info("Active agent reloaded")
48
- except Exception as e:
49
- emit_warning(f"Model changed but agent reload failed: {e}")
32
+ set_model_and_reload_agent(model_name)
50
33
 
51
34
 
52
35
  class ModelNameCompleter(Completer):
@@ -39,10 +39,10 @@ SETTING_DEFINITIONS: Dict[str, Dict] = {
39
39
  "description": "Controls randomness. Lower = more deterministic, higher = more creative.",
40
40
  "type": "numeric",
41
41
  "min": 0.0,
42
- "max": 1.0, # Clamped to 0-1 per user request
43
- "step": 0.1,
42
+ "max": 1.0,
43
+ "step": 0.05,
44
44
  "default": None, # None means use model default
45
- "format": "{:.1f}",
45
+ "format": "{:.2f}",
46
46
  },
47
47
  "seed": {
48
48
  "name": "Seed",
@@ -54,6 +54,16 @@ SETTING_DEFINITIONS: Dict[str, Dict] = {
54
54
  "default": None,
55
55
  "format": "{:.0f}",
56
56
  },
57
+ "top_p": {
58
+ "name": "Top-P (Nucleus Sampling)",
59
+ "description": "Controls token diversity. 0.0 = least random (only most likely tokens), 1.0 = most random (sample from all tokens).",
60
+ "type": "numeric",
61
+ "min": 0.0,
62
+ "max": 1.0,
63
+ "step": 0.05,
64
+ "default": None,
65
+ "format": "{:.2f}",
66
+ },
57
67
  "reasoning_effort": {
58
68
  "name": "Reasoning Effort",
59
69
  "description": "Controls how much effort GPT-5 models spend on reasoning. Higher = more thorough but slower.",
@@ -90,6 +100,12 @@ SETTING_DEFINITIONS: Dict[str, Dict] = {
90
100
  "type": "boolean",
91
101
  "default": False,
92
102
  },
103
+ "clear_thinking": {
104
+ "name": "Clear Thinking",
105
+ "description": "False = Preserved Thinking (keep <think> blocks visible). True = strip thinking from responses.",
106
+ "type": "boolean",
107
+ "default": False,
108
+ },
93
109
  }
94
110
 
95
111
 
@@ -569,6 +585,8 @@ class ModelSettingsMenu:
569
585
  # Default to a sensible starting point for numeric
570
586
  if setting_key == "temperature":
571
587
  self.edit_value = 0.7
588
+ elif setting_key == "top_p":
589
+ self.edit_value = 0.9 # Common default for top_p
572
590
  elif setting_key == "seed":
573
591
  self.edit_value = 42
574
592
  elif setting_key == "budget_tokens":
code_puppy/config.py CHANGED
@@ -75,17 +75,61 @@ 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
+
91
+ # Pack agents - the specialized sub-agents coordinated by Pack Leader
92
+ PACK_AGENT_NAMES = frozenset(
93
+ [
94
+ "pack-leader",
95
+ "bloodhound",
96
+ "husky",
97
+ "shepherd",
98
+ "terrier",
99
+ "watchdog",
100
+ "retriever",
101
+ ]
102
+ )
103
+
104
+
105
+ def get_pack_agents_enabled() -> bool:
106
+ """Return True if pack agents are enabled (default False).
107
+
108
+ When False (default), pack agents (pack-leader, bloodhound, husky, shepherd,
109
+ terrier, watchdog, retriever) are hidden from `list_agents` tool and `/agents`
110
+ command. They cannot be invoked by other agents or selected by users.
111
+
112
+ When True, pack agents are available for use.
113
+ """
114
+ cfg_val = get_value("enable_pack_agents")
115
+ if cfg_val is None:
116
+ return False
117
+ return str(cfg_val).strip().lower() in {"1", "true", "yes", "on"}
118
+
119
+
78
120
  DEFAULT_SECTION = "puppy"
79
121
  REQUIRED_KEYS = ["puppy_name", "owner_name"]
80
122
 
81
123
  # Runtime-only autosave session ID (per-process)
82
124
  _CURRENT_AUTOSAVE_ID: Optional[str] = None
83
125
 
126
+ # Session-local model name (initialized from file on first access, then cached)
127
+ _SESSION_MODEL: Optional[str] = None
128
+
84
129
  # Cache containers for model validation and defaults
85
130
  _model_validation_cache = {}
86
131
  _default_model_cache = None
87
132
  _default_vision_model_cache = None
88
- _default_vqa_model_cache = None
89
133
 
90
134
 
91
135
  def ensure_config_exists():
@@ -208,9 +252,14 @@ def get_config_keys():
208
252
  "diff_context_lines",
209
253
  "default_agent",
210
254
  "temperature",
255
+ "frontend_emitter_enabled",
256
+ "frontend_emitter_max_recent_events",
257
+ "frontend_emitter_queue_size",
211
258
  ]
212
259
  # Add DBOS control key
213
260
  default_keys.append("enable_dbos")
261
+ # Add pack agents control key
262
+ default_keys.append("enable_pack_agents")
214
263
  # Add cancel agent key configuration
215
264
  default_keys.append("cancel_agent_key")
216
265
  # Add banner color keys
@@ -237,6 +286,22 @@ def set_config_value(key: str, value: str):
237
286
  config.write(f)
238
287
 
239
288
 
289
+ # Alias for API compatibility
290
+ def set_value(key: str, value: str) -> None:
291
+ """Set a config value. Alias for set_config_value."""
292
+ set_config_value(key, value)
293
+
294
+
295
+ def reset_value(key: str) -> None:
296
+ """Remove a key from the config file, resetting it to default."""
297
+ config = configparser.ConfigParser()
298
+ config.read(CONFIG_FILE)
299
+ if DEFAULT_SECTION in config and key in config[DEFAULT_SECTION]:
300
+ del config[DEFAULT_SECTION][key]
301
+ with open(CONFIG_FILE, "w") as f:
302
+ config.write(f)
303
+
304
+
240
305
  # --- MODEL STICKY EXTENSION STARTS HERE ---
241
306
  def load_mcp_server_configs():
242
307
  """
@@ -326,47 +391,6 @@ def _default_vision_model_from_models_json() -> str:
326
391
  return "gpt-4.1"
327
392
 
328
393
 
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
394
  def _validate_model_exists(model_name: str) -> bool:
371
395
  """Check if a model exists in models.json with caching to avoid redundant calls."""
372
396
  global _model_validation_cache
@@ -392,15 +416,20 @@ def _validate_model_exists(model_name: str) -> bool:
392
416
 
393
417
  def clear_model_cache():
394
418
  """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
419
+ global _model_validation_cache, _default_model_cache, _default_vision_model_cache
400
420
  _model_validation_cache.clear()
401
421
  _default_model_cache = None
402
422
  _default_vision_model_cache = None
403
- _default_vqa_model_cache = None
423
+
424
+
425
+ def reset_session_model():
426
+ """Reset the session-local model cache.
427
+
428
+ This is primarily for testing purposes. In normal operation, the session
429
+ model is set once at startup and only changes via set_model_name().
430
+ """
431
+ global _SESSION_MODEL
432
+ _SESSION_MODEL = None
404
433
 
405
434
 
406
435
  def model_supports_setting(model_name: str, setting: str) -> bool:
@@ -414,6 +443,10 @@ def model_supports_setting(model_name: str, setting: str) -> bool:
414
443
  True if the model supports the setting, False otherwise.
415
444
  Defaults to True for backwards compatibility if model config doesn't specify.
416
445
  """
446
+ # GLM-4.7 models always support clear_thinking setting
447
+ if setting == "clear_thinking" and "glm-4.7" in model_name.lower():
448
+ return True
449
+
417
450
  try:
418
451
  from code_puppy.model_factory import ModelFactory
419
452
 
@@ -439,26 +472,49 @@ def model_supports_setting(model_name: str, setting: str) -> bool:
439
472
  def get_global_model_name():
440
473
  """Return a valid model name for Code Puppy to use.
441
474
 
442
- 1. Look at ``model`` in *puppy.cfg*.
443
- 2. If that value exists **and** is present in *models.json*, use it.
444
- 3. Otherwise return the first model listed in *models.json*.
445
- 4. As a last resort (e.g.
446
- *models.json* unreadable) fall back to ``claude-4-0-sonnet``.
475
+ Uses session-local caching so that model changes in other terminals
476
+ don't affect this running instance. The file is only read once at startup.
477
+
478
+ 1. If _SESSION_MODEL is set, return it (session cache)
479
+ 2. Otherwise, look at ``model`` in *puppy.cfg*
480
+ 3. If that value exists **and** is present in *models.json*, use it
481
+ 4. Otherwise return the first model listed in *models.json*
482
+ 5. As a last resort fall back to ``claude-4-0-sonnet``
483
+
484
+ The result is cached in _SESSION_MODEL for subsequent calls.
447
485
  """
486
+ global _SESSION_MODEL
487
+
488
+ # Return cached session model if already initialized
489
+ if _SESSION_MODEL is not None:
490
+ return _SESSION_MODEL
448
491
 
492
+ # First access - initialize from file
449
493
  stored_model = get_value("model")
450
494
 
451
495
  if stored_model:
452
496
  # Use cached validation to avoid hitting ModelFactory every time
453
497
  if _validate_model_exists(stored_model):
454
- return stored_model
498
+ _SESSION_MODEL = stored_model
499
+ return _SESSION_MODEL
455
500
 
456
501
  # Either no stored model or it's not valid – choose default from models.json
457
- return _default_model_from_models_json()
502
+ _SESSION_MODEL = _default_model_from_models_json()
503
+ return _SESSION_MODEL
458
504
 
459
505
 
460
506
  def set_model_name(model: str):
461
- """Sets the model name in the persistent config file."""
507
+ """Sets the model name in both the session cache and persistent config file.
508
+
509
+ Updates _SESSION_MODEL immediately for this process, and writes to the
510
+ config file so new terminals will pick up this model as their default.
511
+ """
512
+ global _SESSION_MODEL
513
+
514
+ # Update session cache immediately
515
+ _SESSION_MODEL = model
516
+
517
+ # Also persist to file for new terminal sessions
462
518
  config = configparser.ConfigParser()
463
519
  config.read(CONFIG_FILE)
464
520
  if DEFAULT_SECTION not in config:
@@ -471,20 +527,6 @@ def set_model_name(model: str):
471
527
  clear_model_cache()
472
528
 
473
529
 
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
530
  def get_puppy_token():
489
531
  """Returns the puppy_token from config, or None if not set."""
490
532
  return get_value("puppy_token")
@@ -1291,6 +1333,8 @@ DEFAULT_BANNER_COLORS = {
1291
1333
  "invoke_agent": "deep_pink4", # Ruby - agent invocation
1292
1334
  "subagent_response": "sea_green3", # Emerald - sub-agent success
1293
1335
  "list_agents": "dark_slate_gray3", # Slate - neutral listing
1336
+ # Browser/Terminal tools - same color as edit_file (gold)
1337
+ "terminal_tool": "dark_goldenrod", # Gold - browser terminal operations
1294
1338
  }
1295
1339
 
1296
1340
 
@@ -1584,3 +1628,34 @@ def set_default_agent(agent_name: str) -> None:
1584
1628
  agent_name: The name of the agent to set as default.
1585
1629
  """
1586
1630
  set_config_value("default_agent", agent_name)
1631
+
1632
+
1633
+ # --- FRONTEND EMITTER CONFIGURATION ---
1634
+ def get_frontend_emitter_enabled() -> bool:
1635
+ """Check if frontend emitter is enabled."""
1636
+ val = get_value("frontend_emitter_enabled")
1637
+ if val is None:
1638
+ return True # Enabled by default
1639
+ return str(val).lower() in ("1", "true", "yes", "on")
1640
+
1641
+
1642
+ def get_frontend_emitter_max_recent_events() -> int:
1643
+ """Get max number of recent events to buffer."""
1644
+ val = get_value("frontend_emitter_max_recent_events")
1645
+ if val is None:
1646
+ return 100
1647
+ try:
1648
+ return int(val)
1649
+ except ValueError:
1650
+ return 100
1651
+
1652
+
1653
+ def get_frontend_emitter_queue_size() -> int:
1654
+ """Get max subscriber queue size."""
1655
+ val = get_value("frontend_emitter_queue_size")
1656
+ if val is None:
1657
+ return 100
1658
+ try:
1659
+ return int(val)
1660
+ except ValueError:
1661
+ return 100