code-puppy 0.0.370__py3-none-any.whl → 0.0.371__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 +6 -0
- code_puppy/agents/agent_manager.py +205 -1
- code_puppy/command_line/agent_menu.py +281 -14
- code_puppy/command_line/core_commands.py +3 -3
- code_puppy/command_line/model_picker_completion.py +3 -20
- code_puppy/config.py +44 -8
- code_puppy/model_switching.py +63 -0
- code_puppy/plugins/antigravity_oauth/register_callbacks.py +15 -8
- code_puppy/plugins/chatgpt_oauth/register_callbacks.py +2 -2
- code_puppy/plugins/claude_code_oauth/register_callbacks.py +2 -30
- {code_puppy-0.0.370.dist-info → code_puppy-0.0.371.dist-info}/METADATA +1 -1
- {code_puppy-0.0.370.dist-info → code_puppy-0.0.371.dist-info}/RECORD +17 -16
- {code_puppy-0.0.370.data → code_puppy-0.0.371.data}/data/code_puppy/models.json +0 -0
- {code_puppy-0.0.370.data → code_puppy-0.0.371.data}/data/code_puppy/models_dev_api.json +0 -0
- {code_puppy-0.0.370.dist-info → code_puppy-0.0.371.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.370.dist-info → code_puppy-0.0.371.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.370.dist-info → code_puppy-0.0.371.dist-info}/licenses/LICENSE +0 -0
code_puppy/agents/__init__.py
CHANGED
|
@@ -5,9 +5,12 @@ configurations, each with their own system prompts and tool sets.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from .agent_manager import (
|
|
8
|
+
clone_agent,
|
|
9
|
+
delete_clone_agent,
|
|
8
10
|
get_agent_descriptions,
|
|
9
11
|
get_available_agents,
|
|
10
12
|
get_current_agent,
|
|
13
|
+
is_clone_agent_name,
|
|
11
14
|
load_agent,
|
|
12
15
|
refresh_agents,
|
|
13
16
|
set_current_agent,
|
|
@@ -15,8 +18,11 @@ from .agent_manager import (
|
|
|
15
18
|
from .subagent_stream_handler import subagent_stream_handler
|
|
16
19
|
|
|
17
20
|
__all__ = [
|
|
21
|
+
"clone_agent",
|
|
22
|
+
"delete_clone_agent",
|
|
18
23
|
"get_available_agents",
|
|
19
24
|
"get_current_agent",
|
|
25
|
+
"is_clone_agent_name",
|
|
20
26
|
"set_current_agent",
|
|
21
27
|
"load_agent",
|
|
22
28
|
"get_agent_descriptions",
|
|
@@ -4,6 +4,7 @@ import importlib
|
|
|
4
4
|
import json
|
|
5
5
|
import os
|
|
6
6
|
import pkgutil
|
|
7
|
+
import re
|
|
7
8
|
import uuid
|
|
8
9
|
from pathlib import Path
|
|
9
10
|
from typing import Dict, List, Optional, Type, Union
|
|
@@ -13,7 +14,7 @@ from pydantic_ai.messages import ModelMessage
|
|
|
13
14
|
from code_puppy.agents.base_agent import BaseAgent
|
|
14
15
|
from code_puppy.agents.json_agent import JSONAgent, discover_json_agents
|
|
15
16
|
from code_puppy.callbacks import on_agent_reload
|
|
16
|
-
from code_puppy.messaging import emit_warning
|
|
17
|
+
from code_puppy.messaging import emit_success, emit_warning
|
|
17
18
|
|
|
18
19
|
# Registry of available agents (Python classes and JSON file paths)
|
|
19
20
|
_AGENT_REGISTRY: Dict[str, Union[Type[BaseAgent], str]] = {}
|
|
@@ -467,3 +468,206 @@ def refresh_agents():
|
|
|
467
468
|
# Generate a message group ID for agent refreshing
|
|
468
469
|
message_group_id = str(uuid.uuid4())
|
|
469
470
|
_discover_agents(message_group_id=message_group_id)
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
_CLONE_NAME_PATTERN = re.compile(r"^(?P<base>.+)-clone-(?P<index>\d+)$")
|
|
474
|
+
_CLONE_DISPLAY_PATTERN = re.compile(r"\s*\(Clone\s+\d+\)$", re.IGNORECASE)
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def _strip_clone_suffix(agent_name: str) -> str:
|
|
478
|
+
"""Strip a trailing -clone-N suffix from a name if present."""
|
|
479
|
+
match = _CLONE_NAME_PATTERN.match(agent_name)
|
|
480
|
+
return match.group("base") if match else agent_name
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def _strip_clone_display_suffix(display_name: str) -> str:
|
|
484
|
+
"""Remove a trailing "(Clone N)" suffix from display names."""
|
|
485
|
+
cleaned = _CLONE_DISPLAY_PATTERN.sub("", display_name).strip()
|
|
486
|
+
return cleaned or display_name
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def is_clone_agent_name(agent_name: str) -> bool:
|
|
490
|
+
"""Return True if the agent name looks like a clone."""
|
|
491
|
+
return bool(_CLONE_NAME_PATTERN.match(agent_name))
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def _default_display_name(agent_name: str) -> str:
|
|
495
|
+
"""Build a default display name from an agent name."""
|
|
496
|
+
title = agent_name.title()
|
|
497
|
+
return f"{title} 🤖"
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def _build_clone_display_name(display_name: str, clone_index: int) -> str:
|
|
501
|
+
"""Build a clone display name based on the source display name."""
|
|
502
|
+
base_name = _strip_clone_display_suffix(display_name)
|
|
503
|
+
return f"{base_name} (Clone {clone_index})"
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def _filter_available_tools(tool_names: List[str]) -> List[str]:
|
|
507
|
+
"""Filter a tool list to only available tool names."""
|
|
508
|
+
from code_puppy.tools import get_available_tool_names
|
|
509
|
+
|
|
510
|
+
available_tools = set(get_available_tool_names())
|
|
511
|
+
return [tool for tool in tool_names if tool in available_tools]
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def _next_clone_index(
|
|
515
|
+
base_name: str, existing_names: set[str], agents_dir: Path
|
|
516
|
+
) -> int:
|
|
517
|
+
"""Compute the next clone index for a base name."""
|
|
518
|
+
clone_pattern = re.compile(rf"^{re.escape(base_name)}-clone-(\\d+)$")
|
|
519
|
+
indices = []
|
|
520
|
+
for name in existing_names:
|
|
521
|
+
match = clone_pattern.match(name)
|
|
522
|
+
if match:
|
|
523
|
+
indices.append(int(match.group(1)))
|
|
524
|
+
|
|
525
|
+
next_index = max(indices, default=0) + 1
|
|
526
|
+
while True:
|
|
527
|
+
clone_name = f"{base_name}-clone-{next_index}"
|
|
528
|
+
clone_path = agents_dir / f"{clone_name}.json"
|
|
529
|
+
if clone_name not in existing_names and not clone_path.exists():
|
|
530
|
+
return next_index
|
|
531
|
+
next_index += 1
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def clone_agent(agent_name: str) -> Optional[str]:
|
|
535
|
+
"""Clone an agent definition into the user agents directory.
|
|
536
|
+
|
|
537
|
+
Args:
|
|
538
|
+
agent_name: Source agent name to clone.
|
|
539
|
+
|
|
540
|
+
Returns:
|
|
541
|
+
The cloned agent name, or None if cloning failed.
|
|
542
|
+
"""
|
|
543
|
+
# Generate a message group ID for agent cloning
|
|
544
|
+
message_group_id = str(uuid.uuid4())
|
|
545
|
+
_discover_agents(message_group_id=message_group_id)
|
|
546
|
+
|
|
547
|
+
agent_ref = _AGENT_REGISTRY.get(agent_name)
|
|
548
|
+
if agent_ref is None:
|
|
549
|
+
emit_warning(f"Agent '{agent_name}' not found for cloning.")
|
|
550
|
+
return None
|
|
551
|
+
|
|
552
|
+
from ..config import get_agent_pinned_model, get_user_agents_directory
|
|
553
|
+
|
|
554
|
+
agents_dir = Path(get_user_agents_directory())
|
|
555
|
+
base_name = _strip_clone_suffix(agent_name)
|
|
556
|
+
existing_names = set(_AGENT_REGISTRY.keys())
|
|
557
|
+
clone_index = _next_clone_index(base_name, existing_names, agents_dir)
|
|
558
|
+
clone_name = f"{base_name}-clone-{clone_index}"
|
|
559
|
+
clone_path = agents_dir / f"{clone_name}.json"
|
|
560
|
+
|
|
561
|
+
try:
|
|
562
|
+
if isinstance(agent_ref, str):
|
|
563
|
+
with open(agent_ref, "r", encoding="utf-8") as f:
|
|
564
|
+
source_config = json.load(f)
|
|
565
|
+
|
|
566
|
+
source_display_name = source_config.get("display_name")
|
|
567
|
+
if not source_display_name:
|
|
568
|
+
source_display_name = _default_display_name(base_name)
|
|
569
|
+
|
|
570
|
+
clone_config = dict(source_config)
|
|
571
|
+
clone_config["name"] = clone_name
|
|
572
|
+
clone_config["display_name"] = _build_clone_display_name(
|
|
573
|
+
source_display_name, clone_index
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
tools = source_config.get("tools", [])
|
|
577
|
+
clone_config["tools"] = (
|
|
578
|
+
_filter_available_tools(tools) if isinstance(tools, list) else []
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
if not clone_config.get("model"):
|
|
582
|
+
clone_config.pop("model", None)
|
|
583
|
+
else:
|
|
584
|
+
agent_instance = agent_ref()
|
|
585
|
+
clone_config = {
|
|
586
|
+
"name": clone_name,
|
|
587
|
+
"display_name": _build_clone_display_name(
|
|
588
|
+
agent_instance.display_name, clone_index
|
|
589
|
+
),
|
|
590
|
+
"description": agent_instance.description,
|
|
591
|
+
"system_prompt": agent_instance.get_system_prompt(),
|
|
592
|
+
"tools": _filter_available_tools(agent_instance.get_available_tools()),
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
user_prompt = agent_instance.get_user_prompt()
|
|
596
|
+
if user_prompt is not None:
|
|
597
|
+
clone_config["user_prompt"] = user_prompt
|
|
598
|
+
|
|
599
|
+
tools_config = agent_instance.get_tools_config()
|
|
600
|
+
if tools_config is not None:
|
|
601
|
+
clone_config["tools_config"] = tools_config
|
|
602
|
+
|
|
603
|
+
pinned_model = get_agent_pinned_model(agent_instance.name)
|
|
604
|
+
if pinned_model:
|
|
605
|
+
clone_config["model"] = pinned_model
|
|
606
|
+
except Exception as exc:
|
|
607
|
+
emit_warning(f"Failed to build clone for '{agent_name}': {exc}")
|
|
608
|
+
return None
|
|
609
|
+
|
|
610
|
+
if clone_path.exists():
|
|
611
|
+
emit_warning(f"Clone target '{clone_name}' already exists.")
|
|
612
|
+
return None
|
|
613
|
+
|
|
614
|
+
try:
|
|
615
|
+
with open(clone_path, "w", encoding="utf-8") as f:
|
|
616
|
+
json.dump(clone_config, f, indent=2, ensure_ascii=False)
|
|
617
|
+
emit_success(f"Cloned '{agent_name}' to '{clone_name}'.")
|
|
618
|
+
return clone_name
|
|
619
|
+
except Exception as exc:
|
|
620
|
+
emit_warning(f"Failed to write clone file '{clone_path}': {exc}")
|
|
621
|
+
return None
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
def delete_clone_agent(agent_name: str) -> bool:
|
|
625
|
+
"""Delete a cloned JSON agent definition.
|
|
626
|
+
|
|
627
|
+
Args:
|
|
628
|
+
agent_name: Clone agent name to delete.
|
|
629
|
+
|
|
630
|
+
Returns:
|
|
631
|
+
True if the clone was deleted, False otherwise.
|
|
632
|
+
"""
|
|
633
|
+
message_group_id = str(uuid.uuid4())
|
|
634
|
+
_discover_agents(message_group_id=message_group_id)
|
|
635
|
+
|
|
636
|
+
if not is_clone_agent_name(agent_name):
|
|
637
|
+
emit_warning(f"Agent '{agent_name}' is not a clone.")
|
|
638
|
+
return False
|
|
639
|
+
|
|
640
|
+
if get_current_agent_name() == agent_name:
|
|
641
|
+
emit_warning("Cannot delete the active agent. Switch agents first.")
|
|
642
|
+
return False
|
|
643
|
+
|
|
644
|
+
agent_ref = _AGENT_REGISTRY.get(agent_name)
|
|
645
|
+
if agent_ref is None:
|
|
646
|
+
emit_warning(f"Clone '{agent_name}' not found.")
|
|
647
|
+
return False
|
|
648
|
+
|
|
649
|
+
if not isinstance(agent_ref, str):
|
|
650
|
+
emit_warning(f"Clone '{agent_name}' is not a JSON agent.")
|
|
651
|
+
return False
|
|
652
|
+
|
|
653
|
+
clone_path = Path(agent_ref)
|
|
654
|
+
if not clone_path.exists():
|
|
655
|
+
emit_warning(f"Clone file for '{agent_name}' does not exist.")
|
|
656
|
+
return False
|
|
657
|
+
|
|
658
|
+
from ..config import get_user_agents_directory
|
|
659
|
+
|
|
660
|
+
agents_dir = Path(get_user_agents_directory()).resolve()
|
|
661
|
+
if clone_path.resolve().parent != agents_dir:
|
|
662
|
+
emit_warning(f"Refusing to delete non-user clone '{agent_name}'.")
|
|
663
|
+
return False
|
|
664
|
+
|
|
665
|
+
try:
|
|
666
|
+
clone_path.unlink()
|
|
667
|
+
emit_success(f"Deleted clone '{agent_name}'.")
|
|
668
|
+
_AGENT_REGISTRY.pop(agent_name, None)
|
|
669
|
+
_AGENT_HISTORIES.pop(agent_name, None)
|
|
670
|
+
return True
|
|
671
|
+
except Exception as exc:
|
|
672
|
+
emit_warning(f"Failed to delete clone '{agent_name}': {exc}")
|
|
673
|
+
return False
|
|
@@ -16,11 +16,22 @@ from prompt_toolkit.layout.controls import FormattedTextControl
|
|
|
16
16
|
from prompt_toolkit.widgets import Frame
|
|
17
17
|
|
|
18
18
|
from code_puppy.agents import (
|
|
19
|
+
clone_agent,
|
|
20
|
+
delete_clone_agent,
|
|
19
21
|
get_agent_descriptions,
|
|
20
22
|
get_available_agents,
|
|
21
23
|
get_current_agent,
|
|
24
|
+
is_clone_agent_name,
|
|
22
25
|
)
|
|
26
|
+
from code_puppy.command_line.model_picker_completion import load_model_names
|
|
27
|
+
from code_puppy.config import (
|
|
28
|
+
clear_agent_pinned_model,
|
|
29
|
+
get_agent_pinned_model,
|
|
30
|
+
set_agent_pinned_model,
|
|
31
|
+
)
|
|
32
|
+
from code_puppy.messaging import emit_info, emit_success, emit_warning
|
|
23
33
|
from code_puppy.tools.command_runner import set_awaiting_user_input
|
|
34
|
+
from code_puppy.tools.common import arrow_select_async
|
|
24
35
|
|
|
25
36
|
PAGE_SIZE = 10 # Agents per page
|
|
26
37
|
|
|
@@ -87,6 +98,166 @@ def _sanitize_display_text(text: str) -> str:
|
|
|
87
98
|
return cleaned
|
|
88
99
|
|
|
89
100
|
|
|
101
|
+
def _get_pinned_model(agent_name: str) -> Optional[str]:
|
|
102
|
+
"""Return the pinned model for an agent, if any.
|
|
103
|
+
|
|
104
|
+
Checks both built-in agent config and JSON agent files.
|
|
105
|
+
"""
|
|
106
|
+
import json
|
|
107
|
+
|
|
108
|
+
# First check built-in agent config
|
|
109
|
+
try:
|
|
110
|
+
pinned = get_agent_pinned_model(agent_name)
|
|
111
|
+
if pinned:
|
|
112
|
+
return pinned
|
|
113
|
+
except Exception:
|
|
114
|
+
pass # Continue to check JSON agents
|
|
115
|
+
|
|
116
|
+
# Check if it's a JSON agent
|
|
117
|
+
try:
|
|
118
|
+
from code_puppy.agents.json_agent import discover_json_agents
|
|
119
|
+
|
|
120
|
+
json_agents = discover_json_agents()
|
|
121
|
+
if agent_name in json_agents:
|
|
122
|
+
agent_file_path = json_agents[agent_name]
|
|
123
|
+
with open(agent_file_path, "r", encoding="utf-8") as f:
|
|
124
|
+
agent_config = json.load(f)
|
|
125
|
+
model = agent_config.get("model")
|
|
126
|
+
return model if model else None
|
|
127
|
+
except Exception:
|
|
128
|
+
pass # Return None if we can't read the JSON file
|
|
129
|
+
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _build_model_picker_choices(
|
|
134
|
+
pinned_model: Optional[str],
|
|
135
|
+
model_names: List[str],
|
|
136
|
+
) -> List[str]:
|
|
137
|
+
"""Build model picker choices with pinned/unpin indicators."""
|
|
138
|
+
choices = ["✓ (unpin)" if not pinned_model else " (unpin)"]
|
|
139
|
+
|
|
140
|
+
for model_name in model_names:
|
|
141
|
+
if model_name == pinned_model:
|
|
142
|
+
choices.append(f"✓ {model_name} (pinned)")
|
|
143
|
+
else:
|
|
144
|
+
choices.append(f" {model_name}")
|
|
145
|
+
|
|
146
|
+
return choices
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _normalize_model_choice(choice: str) -> str:
|
|
150
|
+
"""Normalize a picker choice into a model name or '(unpin)' string."""
|
|
151
|
+
cleaned = choice.strip()
|
|
152
|
+
if cleaned.startswith("✓"):
|
|
153
|
+
cleaned = cleaned.lstrip("✓").strip()
|
|
154
|
+
if cleaned.endswith(" (pinned)"):
|
|
155
|
+
cleaned = cleaned[: -len(" (pinned)")].strip()
|
|
156
|
+
return cleaned
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
async def _select_pinned_model(agent_name: str) -> Optional[str]:
|
|
160
|
+
"""Prompt for a model to pin to the agent."""
|
|
161
|
+
try:
|
|
162
|
+
model_names = load_model_names() or []
|
|
163
|
+
except Exception as exc:
|
|
164
|
+
emit_warning(f"Failed to load models: {exc}")
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
pinned_model = _get_pinned_model(agent_name)
|
|
168
|
+
choices = _build_model_picker_choices(pinned_model, model_names)
|
|
169
|
+
if not choices:
|
|
170
|
+
emit_warning("No models available to pin.")
|
|
171
|
+
return None
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
choice = await arrow_select_async(
|
|
175
|
+
f"Select a model to pin for '{agent_name}'",
|
|
176
|
+
choices,
|
|
177
|
+
)
|
|
178
|
+
except KeyboardInterrupt:
|
|
179
|
+
emit_info("Model pinning cancelled")
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
return _normalize_model_choice(choice)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _reload_agent_if_current(
|
|
186
|
+
agent_name: str,
|
|
187
|
+
pinned_model: Optional[str],
|
|
188
|
+
) -> None:
|
|
189
|
+
"""Reload the current agent when its pinned model changes."""
|
|
190
|
+
current_agent = get_current_agent()
|
|
191
|
+
if not current_agent or current_agent.name != agent_name:
|
|
192
|
+
return
|
|
193
|
+
|
|
194
|
+
try:
|
|
195
|
+
if hasattr(current_agent, "refresh_config"):
|
|
196
|
+
current_agent.refresh_config()
|
|
197
|
+
current_agent.reload_code_generation_agent()
|
|
198
|
+
if pinned_model:
|
|
199
|
+
emit_info(f"Active agent reloaded with pinned model '{pinned_model}'")
|
|
200
|
+
else:
|
|
201
|
+
emit_info("Active agent reloaded with default model")
|
|
202
|
+
except Exception as exc:
|
|
203
|
+
emit_warning(f"Pinned model applied but reload failed: {exc}")
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _apply_pinned_model(agent_name: str, model_choice: str) -> None:
|
|
207
|
+
"""Persist a pinned model selection for an agent.
|
|
208
|
+
|
|
209
|
+
Handles both built-in agents (via config) and JSON agents (via JSON file).
|
|
210
|
+
"""
|
|
211
|
+
import json
|
|
212
|
+
|
|
213
|
+
# Check if this is a JSON agent or a built-in agent
|
|
214
|
+
try:
|
|
215
|
+
from code_puppy.agents.json_agent import discover_json_agents
|
|
216
|
+
|
|
217
|
+
json_agents = discover_json_agents()
|
|
218
|
+
is_json_agent = agent_name in json_agents
|
|
219
|
+
except Exception:
|
|
220
|
+
is_json_agent = False
|
|
221
|
+
|
|
222
|
+
try:
|
|
223
|
+
if is_json_agent:
|
|
224
|
+
# Handle JSON agent - modify the JSON file
|
|
225
|
+
agent_file_path = json_agents[agent_name]
|
|
226
|
+
|
|
227
|
+
with open(agent_file_path, "r", encoding="utf-8") as f:
|
|
228
|
+
agent_config = json.load(f)
|
|
229
|
+
|
|
230
|
+
if model_choice == "(unpin)":
|
|
231
|
+
# Remove the model key if it exists
|
|
232
|
+
if "model" in agent_config:
|
|
233
|
+
del agent_config["model"]
|
|
234
|
+
emit_success(f"Model pin cleared for '{agent_name}'")
|
|
235
|
+
pinned_model = None
|
|
236
|
+
else:
|
|
237
|
+
# Set the model
|
|
238
|
+
agent_config["model"] = model_choice
|
|
239
|
+
emit_success(f"Pinned '{model_choice}' to '{agent_name}'")
|
|
240
|
+
pinned_model = model_choice
|
|
241
|
+
|
|
242
|
+
# Save the updated configuration
|
|
243
|
+
with open(agent_file_path, "w", encoding="utf-8") as f:
|
|
244
|
+
json.dump(agent_config, f, indent=2, ensure_ascii=False)
|
|
245
|
+
else:
|
|
246
|
+
# Handle built-in Python agent - use config functions
|
|
247
|
+
if model_choice == "(unpin)":
|
|
248
|
+
clear_agent_pinned_model(agent_name)
|
|
249
|
+
emit_success(f"Model pin cleared for '{agent_name}'")
|
|
250
|
+
pinned_model = None
|
|
251
|
+
else:
|
|
252
|
+
set_agent_pinned_model(agent_name, model_choice)
|
|
253
|
+
emit_success(f"Pinned '{model_choice}' to '{agent_name}'")
|
|
254
|
+
pinned_model = model_choice
|
|
255
|
+
|
|
256
|
+
_reload_agent_if_current(agent_name, pinned_model)
|
|
257
|
+
except Exception as exc:
|
|
258
|
+
emit_warning(f"Failed to apply pinned model: {exc}")
|
|
259
|
+
|
|
260
|
+
|
|
90
261
|
def _get_agent_entries() -> List[Tuple[str, str, str]]:
|
|
91
262
|
"""Get all agents with their display names and descriptions.
|
|
92
263
|
|
|
@@ -141,6 +312,7 @@ def _render_menu_panel(
|
|
|
141
312
|
name, display_name, _ = entries[i]
|
|
142
313
|
is_selected = i == selected_idx
|
|
143
314
|
is_current = name == current_agent_name
|
|
315
|
+
pinned_model = _get_pinned_model(name)
|
|
144
316
|
|
|
145
317
|
# Sanitize display name to avoid emoji rendering issues
|
|
146
318
|
safe_display_name = _sanitize_display_text(display_name)
|
|
@@ -153,6 +325,10 @@ def _render_menu_panel(
|
|
|
153
325
|
lines.append(("", " "))
|
|
154
326
|
lines.append(("", safe_display_name))
|
|
155
327
|
|
|
328
|
+
if pinned_model:
|
|
329
|
+
safe_pinned_model = _sanitize_display_text(pinned_model)
|
|
330
|
+
lines.append(("fg:ansiyellow", f" → {safe_pinned_model}"))
|
|
331
|
+
|
|
156
332
|
# Add current marker
|
|
157
333
|
if is_current:
|
|
158
334
|
lines.append(("fg:ansicyan", " ← current"))
|
|
@@ -167,6 +343,12 @@ def _render_menu_panel(
|
|
|
167
343
|
lines.append(("", "Page\n"))
|
|
168
344
|
lines.append(("fg:green", " Enter "))
|
|
169
345
|
lines.append(("", "Select\n"))
|
|
346
|
+
lines.append(("fg:ansibrightblack", " P "))
|
|
347
|
+
lines.append(("", "Pin model\n"))
|
|
348
|
+
lines.append(("fg:ansibrightblack", " C "))
|
|
349
|
+
lines.append(("", "Clone\n"))
|
|
350
|
+
lines.append(("fg:ansibrightblack", " D "))
|
|
351
|
+
lines.append(("", "Delete clone\n"))
|
|
170
352
|
lines.append(("fg:ansibrightred", " Ctrl+C "))
|
|
171
353
|
lines.append(("", "Cancel"))
|
|
172
354
|
|
|
@@ -198,6 +380,7 @@ def _render_preview_panel(
|
|
|
198
380
|
|
|
199
381
|
name, display_name, description = entry
|
|
200
382
|
is_current = name == current_agent_name
|
|
383
|
+
pinned_model = _get_pinned_model(name)
|
|
201
384
|
|
|
202
385
|
# Sanitize text to avoid emoji rendering issues
|
|
203
386
|
safe_display_name = _sanitize_display_text(display_name)
|
|
@@ -213,6 +396,15 @@ def _render_preview_panel(
|
|
|
213
396
|
lines.append(("fg:ansicyan", safe_display_name))
|
|
214
397
|
lines.append(("", "\n\n"))
|
|
215
398
|
|
|
399
|
+
# Pinned model
|
|
400
|
+
lines.append(("bold", "Pinned Model: "))
|
|
401
|
+
if pinned_model:
|
|
402
|
+
safe_pinned_model = _sanitize_display_text(pinned_model)
|
|
403
|
+
lines.append(("fg:ansiyellow", safe_pinned_model))
|
|
404
|
+
else:
|
|
405
|
+
lines.append(("fg:ansibrightblack", "default"))
|
|
406
|
+
lines.append(("", "\n\n"))
|
|
407
|
+
|
|
216
408
|
# Description
|
|
217
409
|
lines.append(("bold", "Description:"))
|
|
218
410
|
lines.append(("", "\n"))
|
|
@@ -261,8 +453,6 @@ async def interactive_agent_picker() -> Optional[str]:
|
|
|
261
453
|
current_agent_name = current_agent.name if current_agent else ""
|
|
262
454
|
|
|
263
455
|
if not entries:
|
|
264
|
-
from code_puppy.messaging import emit_info
|
|
265
|
-
|
|
266
456
|
emit_info("No agents found.")
|
|
267
457
|
return None
|
|
268
458
|
|
|
@@ -270,14 +460,38 @@ async def interactive_agent_picker() -> Optional[str]:
|
|
|
270
460
|
selected_idx = [0] # Current selection (global index)
|
|
271
461
|
current_page = [0] # Current page
|
|
272
462
|
result = [None] # Selected agent name
|
|
463
|
+
pending_action = [None] # 'pin', 'clone', 'delete', or None
|
|
273
464
|
|
|
274
|
-
total_pages = (len(entries) + PAGE_SIZE - 1) // PAGE_SIZE
|
|
465
|
+
total_pages = [max(1, (len(entries) + PAGE_SIZE - 1) // PAGE_SIZE)]
|
|
275
466
|
|
|
276
467
|
def get_current_entry() -> Optional[Tuple[str, str, str]]:
|
|
277
468
|
if 0 <= selected_idx[0] < len(entries):
|
|
278
469
|
return entries[selected_idx[0]]
|
|
279
470
|
return None
|
|
280
471
|
|
|
472
|
+
def refresh_entries(selected_name: Optional[str] = None) -> None:
|
|
473
|
+
nonlocal entries
|
|
474
|
+
|
|
475
|
+
entries = _get_agent_entries()
|
|
476
|
+
total_pages[0] = max(1, (len(entries) + PAGE_SIZE - 1) // PAGE_SIZE)
|
|
477
|
+
|
|
478
|
+
if not entries:
|
|
479
|
+
selected_idx[0] = 0
|
|
480
|
+
current_page[0] = 0
|
|
481
|
+
return
|
|
482
|
+
|
|
483
|
+
if selected_name:
|
|
484
|
+
for idx, (name, _, _) in enumerate(entries):
|
|
485
|
+
if name == selected_name:
|
|
486
|
+
selected_idx[0] = idx
|
|
487
|
+
break
|
|
488
|
+
else:
|
|
489
|
+
selected_idx[0] = min(selected_idx[0], len(entries) - 1)
|
|
490
|
+
else:
|
|
491
|
+
selected_idx[0] = min(selected_idx[0], len(entries) - 1)
|
|
492
|
+
|
|
493
|
+
current_page[0] = selected_idx[0] // PAGE_SIZE
|
|
494
|
+
|
|
281
495
|
# Build UI
|
|
282
496
|
menu_control = FormattedTextControl(text="")
|
|
283
497
|
preview_control = FormattedTextControl(text="")
|
|
@@ -336,11 +550,29 @@ async def interactive_agent_picker() -> Optional[str]:
|
|
|
336
550
|
|
|
337
551
|
@kb.add("right")
|
|
338
552
|
def _(event):
|
|
339
|
-
if current_page[0] < total_pages - 1:
|
|
553
|
+
if current_page[0] < total_pages[0] - 1:
|
|
340
554
|
current_page[0] += 1
|
|
341
555
|
selected_idx[0] = current_page[0] * PAGE_SIZE
|
|
342
556
|
update_display()
|
|
343
557
|
|
|
558
|
+
@kb.add("p")
|
|
559
|
+
def _(event):
|
|
560
|
+
if get_current_entry():
|
|
561
|
+
pending_action[0] = "pin"
|
|
562
|
+
event.app.exit()
|
|
563
|
+
|
|
564
|
+
@kb.add("c")
|
|
565
|
+
def _(event):
|
|
566
|
+
if get_current_entry():
|
|
567
|
+
pending_action[0] = "clone"
|
|
568
|
+
event.app.exit()
|
|
569
|
+
|
|
570
|
+
@kb.add("d")
|
|
571
|
+
def _(event):
|
|
572
|
+
if get_current_entry():
|
|
573
|
+
pending_action[0] = "delete"
|
|
574
|
+
event.app.exit()
|
|
575
|
+
|
|
344
576
|
@kb.add("enter")
|
|
345
577
|
def _(event):
|
|
346
578
|
entry = get_current_entry()
|
|
@@ -370,15 +602,52 @@ async def interactive_agent_picker() -> Optional[str]:
|
|
|
370
602
|
time.sleep(0.05)
|
|
371
603
|
|
|
372
604
|
try:
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
sys.stdout.write("\033[2J\033[H")
|
|
378
|
-
sys.stdout.flush()
|
|
605
|
+
while True:
|
|
606
|
+
pending_action[0] = None
|
|
607
|
+
result[0] = None
|
|
608
|
+
update_display()
|
|
379
609
|
|
|
380
|
-
|
|
381
|
-
|
|
610
|
+
# Clear the current buffer
|
|
611
|
+
sys.stdout.write("\033[2J\033[H")
|
|
612
|
+
sys.stdout.flush()
|
|
613
|
+
|
|
614
|
+
# Run application
|
|
615
|
+
await app.run_async()
|
|
616
|
+
|
|
617
|
+
if pending_action[0] == "pin":
|
|
618
|
+
entry = get_current_entry()
|
|
619
|
+
if entry:
|
|
620
|
+
selected_model = await _select_pinned_model(entry[0])
|
|
621
|
+
if selected_model:
|
|
622
|
+
_apply_pinned_model(entry[0], selected_model)
|
|
623
|
+
continue
|
|
624
|
+
|
|
625
|
+
if pending_action[0] == "clone":
|
|
626
|
+
entry = get_current_entry()
|
|
627
|
+
selected_name = None
|
|
628
|
+
if entry:
|
|
629
|
+
cloned_name = clone_agent(entry[0])
|
|
630
|
+
selected_name = cloned_name or entry[0]
|
|
631
|
+
refresh_entries(selected_name=selected_name)
|
|
632
|
+
continue
|
|
633
|
+
|
|
634
|
+
if pending_action[0] == "delete":
|
|
635
|
+
entry = get_current_entry()
|
|
636
|
+
selected_name = None
|
|
637
|
+
if entry:
|
|
638
|
+
agent_name = entry[0]
|
|
639
|
+
selected_name = agent_name
|
|
640
|
+
if not is_clone_agent_name(agent_name):
|
|
641
|
+
emit_warning("Only cloned agents can be deleted.")
|
|
642
|
+
elif agent_name == current_agent_name:
|
|
643
|
+
emit_warning("Cannot delete the active agent. Switch first.")
|
|
644
|
+
else:
|
|
645
|
+
if delete_clone_agent(agent_name):
|
|
646
|
+
selected_name = None
|
|
647
|
+
refresh_entries(selected_name=selected_name)
|
|
648
|
+
continue
|
|
649
|
+
|
|
650
|
+
break
|
|
382
651
|
|
|
383
652
|
finally:
|
|
384
653
|
# Exit alternate screen buffer once at end
|
|
@@ -388,8 +657,6 @@ async def interactive_agent_picker() -> Optional[str]:
|
|
|
388
657
|
set_awaiting_user_input(False)
|
|
389
658
|
|
|
390
659
|
# Clear exit message
|
|
391
|
-
from code_puppy.messaging import emit_info
|
|
392
|
-
|
|
393
660
|
emit_info("✓ Exited agent picker")
|
|
394
661
|
|
|
395
662
|
return result[0]
|
|
@@ -169,7 +169,7 @@ def handle_tutorial_command(command: str) -> bool:
|
|
|
169
169
|
reset_onboarding,
|
|
170
170
|
run_onboarding_wizard,
|
|
171
171
|
)
|
|
172
|
-
from code_puppy.
|
|
172
|
+
from code_puppy.model_switching import set_model_and_reload_agent
|
|
173
173
|
|
|
174
174
|
# Always reset so user can re-run the tutorial anytime
|
|
175
175
|
reset_onboarding()
|
|
@@ -184,7 +184,7 @@ def handle_tutorial_command(command: str) -> bool:
|
|
|
184
184
|
from code_puppy.plugins.chatgpt_oauth.oauth_flow import run_oauth_flow
|
|
185
185
|
|
|
186
186
|
run_oauth_flow()
|
|
187
|
-
|
|
187
|
+
set_model_and_reload_agent("chatgpt-gpt-5.2-codex")
|
|
188
188
|
elif result == "claude":
|
|
189
189
|
emit_info("🔐 Starting Claude Code OAuth flow...")
|
|
190
190
|
from code_puppy.plugins.claude_code_oauth.register_callbacks import (
|
|
@@ -192,7 +192,7 @@ def handle_tutorial_command(command: str) -> bool:
|
|
|
192
192
|
)
|
|
193
193
|
|
|
194
194
|
_perform_authentication()
|
|
195
|
-
|
|
195
|
+
set_model_and_reload_agent("claude-code-claude-opus-4-5-20251101")
|
|
196
196
|
elif result == "completed":
|
|
197
197
|
emit_info("🎉 Tutorial complete! Happy coding!")
|
|
198
198
|
elif result == "skipped":
|
|
@@ -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
|
|
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
|
-
|
|
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):
|
code_puppy/config.py
CHANGED
|
@@ -123,6 +123,9 @@ REQUIRED_KEYS = ["puppy_name", "owner_name"]
|
|
|
123
123
|
# Runtime-only autosave session ID (per-process)
|
|
124
124
|
_CURRENT_AUTOSAVE_ID: Optional[str] = None
|
|
125
125
|
|
|
126
|
+
# Session-local model name (initialized from file on first access, then cached)
|
|
127
|
+
_SESSION_MODEL: Optional[str] = None
|
|
128
|
+
|
|
126
129
|
# Cache containers for model validation and defaults
|
|
127
130
|
_model_validation_cache = {}
|
|
128
131
|
_default_model_cache = None
|
|
@@ -419,6 +422,16 @@ def clear_model_cache():
|
|
|
419
422
|
_default_vision_model_cache = None
|
|
420
423
|
|
|
421
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
|
|
433
|
+
|
|
434
|
+
|
|
422
435
|
def model_supports_setting(model_name: str, setting: str) -> bool:
|
|
423
436
|
"""Check if a model supports a particular setting (e.g., 'temperature', 'seed').
|
|
424
437
|
|
|
@@ -459,26 +472,49 @@ def model_supports_setting(model_name: str, setting: str) -> bool:
|
|
|
459
472
|
def get_global_model_name():
|
|
460
473
|
"""Return a valid model name for Code Puppy to use.
|
|
461
474
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
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.
|
|
467
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
|
|
468
491
|
|
|
492
|
+
# First access - initialize from file
|
|
469
493
|
stored_model = get_value("model")
|
|
470
494
|
|
|
471
495
|
if stored_model:
|
|
472
496
|
# Use cached validation to avoid hitting ModelFactory every time
|
|
473
497
|
if _validate_model_exists(stored_model):
|
|
474
|
-
|
|
498
|
+
_SESSION_MODEL = stored_model
|
|
499
|
+
return _SESSION_MODEL
|
|
475
500
|
|
|
476
501
|
# Either no stored model or it's not valid – choose default from models.json
|
|
477
|
-
|
|
502
|
+
_SESSION_MODEL = _default_model_from_models_json()
|
|
503
|
+
return _SESSION_MODEL
|
|
478
504
|
|
|
479
505
|
|
|
480
506
|
def set_model_name(model: str):
|
|
481
|
-
"""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
|
|
482
518
|
config = configparser.ConfigParser()
|
|
483
519
|
config.read(CONFIG_FILE)
|
|
484
520
|
if DEFAULT_SECTION not in config:
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Shared helpers for switching models and reloading agents safely."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from code_puppy.config import set_model_name
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _get_effective_agent_model(agent) -> Optional[str]:
|
|
11
|
+
"""Safely fetch the effective model name for an agent."""
|
|
12
|
+
try:
|
|
13
|
+
return agent.get_model_name()
|
|
14
|
+
except Exception:
|
|
15
|
+
return None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def set_model_and_reload_agent(
|
|
19
|
+
model_name: str,
|
|
20
|
+
*,
|
|
21
|
+
warn_on_pinned_mismatch: bool = True,
|
|
22
|
+
) -> None:
|
|
23
|
+
"""Set the global model and reload the active agent.
|
|
24
|
+
|
|
25
|
+
This keeps model switching consistent across commands while avoiding
|
|
26
|
+
direct imports that can trigger circular dependencies.
|
|
27
|
+
"""
|
|
28
|
+
from code_puppy.messaging import emit_info, emit_warning
|
|
29
|
+
|
|
30
|
+
set_model_name(model_name)
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
from code_puppy.agents import get_current_agent
|
|
34
|
+
|
|
35
|
+
current_agent = get_current_agent()
|
|
36
|
+
if current_agent is None:
|
|
37
|
+
emit_warning("Model changed but no active agent was found to reload")
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
# JSON agents may need to refresh their config before reload
|
|
41
|
+
if hasattr(current_agent, "refresh_config"):
|
|
42
|
+
try:
|
|
43
|
+
current_agent.refresh_config()
|
|
44
|
+
except Exception:
|
|
45
|
+
# Non-fatal, continue to reload
|
|
46
|
+
...
|
|
47
|
+
|
|
48
|
+
if warn_on_pinned_mismatch:
|
|
49
|
+
effective_model = _get_effective_agent_model(current_agent)
|
|
50
|
+
if effective_model and effective_model != model_name:
|
|
51
|
+
display_name = getattr(
|
|
52
|
+
current_agent, "display_name", current_agent.name
|
|
53
|
+
)
|
|
54
|
+
emit_warning(
|
|
55
|
+
"Active agent "
|
|
56
|
+
f"'{display_name}' is pinned to '{effective_model}', "
|
|
57
|
+
f"so '{model_name}' will not take effect until unpinned."
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
current_agent.reload_code_generation_agent()
|
|
61
|
+
emit_info("Active agent reloaded")
|
|
62
|
+
except Exception as exc:
|
|
63
|
+
emit_warning(f"Model changed but agent reload failed: {exc}")
|
|
@@ -10,8 +10,8 @@ from typing import Any, Dict, List, Optional, Tuple
|
|
|
10
10
|
from urllib.parse import parse_qs, urlparse
|
|
11
11
|
|
|
12
12
|
from code_puppy.callbacks import register_callback
|
|
13
|
-
from code_puppy.config import set_model_name
|
|
14
13
|
from code_puppy.messaging import emit_error, emit_info, emit_success, emit_warning
|
|
14
|
+
from code_puppy.model_switching import set_model_and_reload_agent
|
|
15
15
|
|
|
16
16
|
from ..oauth_puppy_html import oauth_failure_html, oauth_success_html
|
|
17
17
|
from .accounts import AccountManager
|
|
@@ -165,8 +165,16 @@ def _await_callback(context: Any) -> Optional[Tuple[str, str, str]]:
|
|
|
165
165
|
return result.code, result.state, redirect_uri
|
|
166
166
|
|
|
167
167
|
|
|
168
|
-
def _perform_authentication(
|
|
169
|
-
|
|
168
|
+
def _perform_authentication(
|
|
169
|
+
add_account: bool = False,
|
|
170
|
+
reload_agent: bool = True,
|
|
171
|
+
) -> bool:
|
|
172
|
+
"""Run the OAuth authentication flow.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
add_account: Whether to add a new account to the pool.
|
|
176
|
+
reload_agent: Whether to reload the current agent after auth.
|
|
177
|
+
"""
|
|
170
178
|
context = prepare_oauth_context()
|
|
171
179
|
callback_result = _await_callback(context)
|
|
172
180
|
|
|
@@ -226,8 +234,8 @@ def _perform_authentication(add_account: bool = False) -> bool:
|
|
|
226
234
|
else:
|
|
227
235
|
emit_warning("Failed to configure models. Try running /antigravity-auth again.")
|
|
228
236
|
|
|
229
|
-
|
|
230
|
-
|
|
237
|
+
if reload_agent:
|
|
238
|
+
reload_current_agent()
|
|
231
239
|
return True
|
|
232
240
|
|
|
233
241
|
|
|
@@ -378,9 +386,8 @@ def _handle_custom_command(command: str, name: str) -> Optional[bool]:
|
|
|
378
386
|
"Existing tokens found. This will refresh your authentication."
|
|
379
387
|
)
|
|
380
388
|
|
|
381
|
-
if _perform_authentication():
|
|
382
|
-
|
|
383
|
-
set_model_name("antigravity-gemini-3-pro-high")
|
|
389
|
+
if _perform_authentication(reload_agent=False):
|
|
390
|
+
set_model_and_reload_agent("antigravity-gemini-3-pro-high")
|
|
384
391
|
return True
|
|
385
392
|
|
|
386
393
|
if name == "antigravity-add":
|
|
@@ -6,8 +6,8 @@ import os
|
|
|
6
6
|
from typing import List, Optional, Tuple
|
|
7
7
|
|
|
8
8
|
from code_puppy.callbacks import register_callback
|
|
9
|
-
from code_puppy.config import set_model_name
|
|
10
9
|
from code_puppy.messaging import emit_info, emit_success, emit_warning
|
|
10
|
+
from code_puppy.model_switching import set_model_and_reload_agent
|
|
11
11
|
|
|
12
12
|
from .config import CHATGPT_OAUTH_CONFIG, get_token_storage_path
|
|
13
13
|
from .oauth_flow import run_oauth_flow
|
|
@@ -76,7 +76,7 @@ def _handle_custom_command(command: str, name: str) -> Optional[bool]:
|
|
|
76
76
|
|
|
77
77
|
if name == "chatgpt-auth":
|
|
78
78
|
run_oauth_flow()
|
|
79
|
-
|
|
79
|
+
set_model_and_reload_agent("chatgpt-gpt-5.2-codex")
|
|
80
80
|
return True
|
|
81
81
|
|
|
82
82
|
if name == "chatgpt-status":
|
|
@@ -12,8 +12,8 @@ from typing import Any, Dict, List, Optional, Tuple
|
|
|
12
12
|
from urllib.parse import parse_qs, urlparse
|
|
13
13
|
|
|
14
14
|
from code_puppy.callbacks import register_callback
|
|
15
|
-
from code_puppy.config import set_model_name
|
|
16
15
|
from code_puppy.messaging import emit_error, emit_info, emit_success, emit_warning
|
|
16
|
+
from code_puppy.model_switching import set_model_and_reload_agent
|
|
17
17
|
|
|
18
18
|
from ..oauth_puppy_html import oauth_failure_html, oauth_success_html
|
|
19
19
|
from .config import CLAUDE_CODE_OAUTH_CONFIG, get_token_storage_path
|
|
@@ -181,31 +181,6 @@ def _custom_help() -> List[Tuple[str, str]]:
|
|
|
181
181
|
]
|
|
182
182
|
|
|
183
183
|
|
|
184
|
-
def _reload_current_agent() -> None:
|
|
185
|
-
"""Reload the current agent so new auth tokens are picked up immediately."""
|
|
186
|
-
try:
|
|
187
|
-
from code_puppy.agents import get_current_agent
|
|
188
|
-
|
|
189
|
-
current_agent = get_current_agent()
|
|
190
|
-
if current_agent is None:
|
|
191
|
-
logger.debug("No current agent to reload")
|
|
192
|
-
return
|
|
193
|
-
|
|
194
|
-
# JSON agents may need to refresh their config before reload
|
|
195
|
-
if hasattr(current_agent, "refresh_config"):
|
|
196
|
-
try:
|
|
197
|
-
current_agent.refresh_config()
|
|
198
|
-
except Exception:
|
|
199
|
-
# Non-fatal, continue to reload
|
|
200
|
-
pass
|
|
201
|
-
|
|
202
|
-
current_agent.reload_code_generation_agent()
|
|
203
|
-
emit_info("Active agent reloaded with new authentication")
|
|
204
|
-
except Exception as e:
|
|
205
|
-
emit_warning(f"Authentication succeeded but agent reload failed: {e}")
|
|
206
|
-
logger.exception("Failed to reload agent after authentication")
|
|
207
|
-
|
|
208
|
-
|
|
209
184
|
def _perform_authentication() -> None:
|
|
210
185
|
context = prepare_oauth_context()
|
|
211
186
|
code = _await_callback(context)
|
|
@@ -245,9 +220,6 @@ def _perform_authentication() -> None:
|
|
|
245
220
|
"Claude Code models added to your configuration. Use the `claude-code-` prefix!"
|
|
246
221
|
)
|
|
247
222
|
|
|
248
|
-
# Reload the current agent so the new auth token is picked up immediately
|
|
249
|
-
_reload_current_agent()
|
|
250
|
-
|
|
251
223
|
|
|
252
224
|
def _handle_custom_command(command: str, name: str) -> Optional[bool]:
|
|
253
225
|
if not name:
|
|
@@ -261,7 +233,7 @@ def _handle_custom_command(command: str, name: str) -> Optional[bool]:
|
|
|
261
233
|
"Existing Claude Code tokens found. Continuing will overwrite them."
|
|
262
234
|
)
|
|
263
235
|
_perform_authentication()
|
|
264
|
-
|
|
236
|
+
set_model_and_reload_agent("claude-code-claude-opus-4-5-20251101")
|
|
265
237
|
return True
|
|
266
238
|
|
|
267
239
|
if name == "claude-code-status":
|
|
@@ -4,7 +4,7 @@ code_puppy/callbacks.py,sha256=Pp0VyeXJBEtk-N_RSWr5pbveelovsdLUiJ4f11dzwGw,10775
|
|
|
4
4
|
code_puppy/chatgpt_codex_client.py,sha256=upMuAfOhMB7SEpVw4CU4GjgaeZ8X65ri3yNM-dnlmYA,12308
|
|
5
5
|
code_puppy/claude_cache_client.py,sha256=1-rIDtZBJ_aiAZSprdsq0ty2urftu6F0uzJDn_OE41Q,23966
|
|
6
6
|
code_puppy/cli_runner.py,sha256=w5CLKgQYYaT7My3Cga2StXYol-u6DBxNzzUuhhsfhsA,34952
|
|
7
|
-
code_puppy/config.py,sha256=
|
|
7
|
+
code_puppy/config.py,sha256=aKWADF6PdHnr9_0ZVZHwBh5NH9uSAx1lmIiycfaYEF8,54737
|
|
8
8
|
code_puppy/error_logging.py,sha256=a80OILCUtJhexI6a9GM-r5LqIdjvSRzggfgPp2jv1X0,3297
|
|
9
9
|
code_puppy/gemini_code_assist.py,sha256=KGS7sO5OLc83nDF3xxS-QiU6vxW9vcm6hmzilu79Ef8,13867
|
|
10
10
|
code_puppy/gemini_model.py,sha256=i8XXmx9s1eWEXpJ8U288w0yayTt6Nq8V-hxpUHhti4s,25984
|
|
@@ -12,6 +12,7 @@ code_puppy/http_utils.py,sha256=SAH6EOdbR6Cbfmi-4EtHDqRDBUV5bWtGc-5nr44F0Is,1041
|
|
|
12
12
|
code_puppy/keymap.py,sha256=IvMkTlB_bIqOWpbTpmftkdyjhtD5todXuEIw1zCZ4u0,3584
|
|
13
13
|
code_puppy/main.py,sha256=82r3vZy_XcyEsenLn82BnUusaoyL3Bpm_Th_jKgqecE,273
|
|
14
14
|
code_puppy/model_factory.py,sha256=854Bo8wx59dOirAMhH0YuSXVHs-IxQBT_yrJGyVjKcA,39924
|
|
15
|
+
code_puppy/model_switching.py,sha256=3IsnSWKHLWzI5d2WDYNg0Xr78BeYNN1WrZuzas-lYJ4,2064
|
|
15
16
|
code_puppy/model_utils.py,sha256=sNTjclnS2hfV2o27qXSAZjaA3d72ucVVI3gOvAKpaBQ,3799
|
|
16
17
|
code_puppy/models.json,sha256=FMQdE_yvP_8y0xxt3K918UkFL9cZMYAqW1SfXcQkU_k,3105
|
|
17
18
|
code_puppy/models_dev_api.json,sha256=wHjkj-IM_fx1oHki6-GqtOoCrRMR0ScK0f-Iz0UEcy8,548187
|
|
@@ -25,7 +26,7 @@ code_puppy/summarization_agent.py,sha256=6Pu_Wp_rF-HAhoX9u2uXTabRVkOZUYwRoMP1lzN
|
|
|
25
26
|
code_puppy/terminal_utils.py,sha256=TaS19x7EZqudlBUAQwLMzBMNxBHBNInvQQREXqRGtkM,12984
|
|
26
27
|
code_puppy/uvx_detection.py,sha256=tP9X9Nvzow--KIqtqjgrHQkSxMJ3EevfoaeoB9VLY2o,7224
|
|
27
28
|
code_puppy/version_checker.py,sha256=aq2Mwxl1CR9sEFBgrPt3OQOowLOBUp9VaQYWJhuUv8Q,1780
|
|
28
|
-
code_puppy/agents/__init__.py,sha256=
|
|
29
|
+
code_puppy/agents/__init__.py,sha256=OjdPBQoywc50OM6lO6QydEUuPeekbFQBL3kxR3lGczo,748
|
|
29
30
|
code_puppy/agents/agent_c_reviewer.py,sha256=1kO_89hcrhlS4sJ6elDLSEx-h43jAaWGgvIL0SZUuKo,8214
|
|
30
31
|
code_puppy/agents/agent_code_puppy.py,sha256=-u9VkqoE_GuB8zae9OeFMv64qt94cFs-_tzK2stIk5A,8406
|
|
31
32
|
code_puppy/agents/agent_code_reviewer.py,sha256=V9pznpi7z1XTYBjRj1Em8S71PbFXLvU8z0gCmPAQxSc,4635
|
|
@@ -33,7 +34,7 @@ code_puppy/agents/agent_cpp_reviewer.py,sha256=lbaGU4aKSNBrxsYfN86BKOeKBgL8kS9sL
|
|
|
33
34
|
code_puppy/agents/agent_creator_agent.py,sha256=pYnDRCn8qWivAeu-GA-WYn_gZ67KT1I9ZyHbaNssIII,25027
|
|
34
35
|
code_puppy/agents/agent_golang_reviewer.py,sha256=VEAwiQW06occkfABVz9Y7wStQ8pFtX94DAvZdRSRuzs,9319
|
|
35
36
|
code_puppy/agents/agent_javascript_reviewer.py,sha256=ATSXl278kPU4F6hiYsMMGkGrrWDlJqPwaYwYGNuo9J0,9494
|
|
36
|
-
code_puppy/agents/agent_manager.py,sha256=
|
|
37
|
+
code_puppy/agents/agent_manager.py,sha256=7bgG2ni6xUu8JNiuuMSUeYloNqpTjNXCF8M4V2DRB3o,22824
|
|
37
38
|
code_puppy/agents/agent_pack_leader.py,sha256=DrP5rnYZbqkOm4ClK_Q4-aehjqXXVlq1UFs1bu11zbA,15766
|
|
38
39
|
code_puppy/agents/agent_planning.py,sha256=LtFqQixrDUPudSvmhbntK-zRbDHn0lSi1xrKFVqCwDo,6902
|
|
39
40
|
code_puppy/agents/agent_python_programmer.py,sha256=R-7XoGIFJ58EY9LE9mWGcQQ8gSsMzi-1HD6wigJQPL8,6846
|
|
@@ -68,7 +69,7 @@ code_puppy/api/routers/sessions.py,sha256=GqYRT7IJYPpEdTseLF3FIpbvvD86lIqwwPswL3
|
|
|
68
69
|
code_puppy/api/templates/terminal.html,sha256=9alh6tTbLyXPDjBvkXw8nEWPXB-m_LIceGGRYpSLuyo,13125
|
|
69
70
|
code_puppy/command_line/__init__.py,sha256=y7WeRemfYppk8KVbCGeAIiTuiOszIURCDjOMZv_YRmU,45
|
|
70
71
|
code_puppy/command_line/add_model_menu.py,sha256=CpURhxPvUhLHLBV_uwH1ODfJ-WAcGklvlsjEf5Vfvg4,43255
|
|
71
|
-
code_puppy/command_line/agent_menu.py,sha256=
|
|
72
|
+
code_puppy/command_line/agent_menu.py,sha256=4SVPS0eA7YfpxacNk0Kel16bzqQ3bBGe8dqCCOI2A8s,20915
|
|
72
73
|
code_puppy/command_line/attachments.py,sha256=4Q5I2Es4j0ltnz5wjw2z0QXMsiMJvEfWRkPf_lJeITM,13093
|
|
73
74
|
code_puppy/command_line/autosave_menu.py,sha256=de7nOmFmEH6x5T7C95U8N8xgxxeF-l5lgaJzGJsF3ZY,19824
|
|
74
75
|
code_puppy/command_line/clipboard.py,sha256=oe9bfAX5RnT81FiYrDmhvHaePS1tAT-NFG1fSXubSD4,16869
|
|
@@ -76,12 +77,12 @@ code_puppy/command_line/colors_menu.py,sha256=LoFVfJ-Mo-Eq9hnb2Rj5mn7oBCnadAGr-8
|
|
|
76
77
|
code_puppy/command_line/command_handler.py,sha256=CY9F27eovZJK_kpU1YmbroYLWGTCuouCOQ-TXfDp-nw,10916
|
|
77
78
|
code_puppy/command_line/command_registry.py,sha256=qFySsw1g8dol3kgi0p6cXrIDlP11_OhOoaQ5nAadWXg,4416
|
|
78
79
|
code_puppy/command_line/config_commands.py,sha256=qS9Cm758DPz2QGvHLhAV4Tp_Xfgo3PyoCoLDusbnmCw,25742
|
|
79
|
-
code_puppy/command_line/core_commands.py,sha256=
|
|
80
|
+
code_puppy/command_line/core_commands.py,sha256=QTQt2CS9_6ExcgS6BLgRZWkXDaSb-KC_tWplUkOGaMA,27133
|
|
80
81
|
code_puppy/command_line/diff_menu.py,sha256=_Gr9SP9fbItk-08dya9WTAR53s_PlyAvEnbt-8VWKPk,24141
|
|
81
82
|
code_puppy/command_line/file_path_completion.py,sha256=gw8NpIxa6GOpczUJRyh7VNZwoXKKn-yvCqit7h2y6Gg,2931
|
|
82
83
|
code_puppy/command_line/load_context_completion.py,sha256=a3JvLDeLLSYxVgTjAdqWzS4spjv6ccCrK2LKZgVJ1IM,2202
|
|
83
84
|
code_puppy/command_line/mcp_completion.py,sha256=eKzW2O7gun7HoHekOW0XVXhNS5J2xCtK7aaWyA8bkZk,6952
|
|
84
|
-
code_puppy/command_line/model_picker_completion.py,sha256=
|
|
85
|
+
code_puppy/command_line/model_picker_completion.py,sha256=YRudzwGVtIjr02MyeIdmbkDhS00ENjCt9k3nATT3KdM,6143
|
|
85
86
|
code_puppy/command_line/model_settings_menu.py,sha256=TPdKdG3yPKCG8c5_0mn6nIszXY6aL9vmzdHslyDE9yY,32632
|
|
86
87
|
code_puppy/command_line/motd.py,sha256=XuIk3UTLawwVFM-NfoaJGU5F2hPLASTFXq84UdDMT0Q,2408
|
|
87
88
|
code_puppy/command_line/onboarding_slides.py,sha256=itqAsuHzjHpD_XNz6FniBIYr6dNyP1AW_XQZQ6SbVek,7125
|
|
@@ -152,7 +153,7 @@ code_puppy/plugins/antigravity_oauth/antigravity_model.py,sha256=ZFarvPYgYixQxEm
|
|
|
152
153
|
code_puppy/plugins/antigravity_oauth/config.py,sha256=BoQgqf5I2XoHWnBBo9vhCIc_XwPj9Mbp0Z95ygWwt78,1362
|
|
153
154
|
code_puppy/plugins/antigravity_oauth/constants.py,sha256=qsrA10JJvzNuY0OobvvwCQcoGpILBninllcUUMKkUrQ,4644
|
|
154
155
|
code_puppy/plugins/antigravity_oauth/oauth.py,sha256=ZHXJtZP63l6brOpX1WdLfuUClIleA79-4y36YUJc6Wo,15137
|
|
155
|
-
code_puppy/plugins/antigravity_oauth/register_callbacks.py,sha256=
|
|
156
|
+
code_puppy/plugins/antigravity_oauth/register_callbacks.py,sha256=ogW823_ysCph8kGunPZ60mzbk4drx_NEO_4-WtMB4SA,13331
|
|
156
157
|
code_puppy/plugins/antigravity_oauth/storage.py,sha256=LW1DkY6Z-GRbBDrIitT6glKemZptp3NzldIrLRqTAK0,8971
|
|
157
158
|
code_puppy/plugins/antigravity_oauth/test_plugin.py,sha256=n0kjFG8Vt2n1j0GgTRSdSyhF0t9xxE8Ht60SH5CSwzw,11027
|
|
158
159
|
code_puppy/plugins/antigravity_oauth/token.py,sha256=WbiFCkrZvChpGXvwIYsJMgqU9xdJ81KwR062lFlnL3U,5038
|
|
@@ -161,14 +162,14 @@ code_puppy/plugins/antigravity_oauth/utils.py,sha256=mXHRv0l07r27VjtSsIy9rlpkUhe
|
|
|
161
162
|
code_puppy/plugins/chatgpt_oauth/__init__.py,sha256=Kjc6Hsz1sWvMD2OdAlWZvJRiKJSj4fx22boa-aVFKjA,189
|
|
162
163
|
code_puppy/plugins/chatgpt_oauth/config.py,sha256=H_wAH9Duyn8WH2Kq8oe72uda-_4qu1uXLPun_SDdtsk,2023
|
|
163
164
|
code_puppy/plugins/chatgpt_oauth/oauth_flow.py,sha256=i-CP2gpzEBT3ogUt-oTMexiP2on41N6PbRGIy2lZF30,11028
|
|
164
|
-
code_puppy/plugins/chatgpt_oauth/register_callbacks.py,sha256=
|
|
165
|
+
code_puppy/plugins/chatgpt_oauth/register_callbacks.py,sha256=KNi5-R0EXtkBm3p55ttAxuA_ApaOs_tsGDnPt-5vgGA,2998
|
|
165
166
|
code_puppy/plugins/chatgpt_oauth/test_plugin.py,sha256=oHX7Eb_Hb4rgRpOWdhtFp8Jj6_FDuvXQITRPiNy4tRo,9622
|
|
166
167
|
code_puppy/plugins/chatgpt_oauth/utils.py,sha256=fzpsCQOv0kqPWmG5vNEV_GLSUrMQh8cF7tdIjSOt1Dc,16504
|
|
167
168
|
code_puppy/plugins/claude_code_oauth/README.md,sha256=76nHhMlhk61DZa5g0Q2fc0AtpplLmpbwuWFZt7PHH5g,5458
|
|
168
169
|
code_puppy/plugins/claude_code_oauth/SETUP.md,sha256=lnGzofPLogBy3oPPFLv5_cZ7vjg_GYrIyYnF-EoTJKg,3278
|
|
169
170
|
code_puppy/plugins/claude_code_oauth/__init__.py,sha256=mCcOU-wM7LNCDjr-w-WLPzom8nTF1UNt4nqxGE6Rt0k,187
|
|
170
171
|
code_puppy/plugins/claude_code_oauth/config.py,sha256=DjGySCkvjSGZds6DYErLMAi3TItt8iSLGvyJN98nSEM,2013
|
|
171
|
-
code_puppy/plugins/claude_code_oauth/register_callbacks.py,sha256=
|
|
172
|
+
code_puppy/plugins/claude_code_oauth/register_callbacks.py,sha256=ZnLQfwssbQXdCpnMGHjzEIzXTLc_OZ1UGS4npU5k5m8,9168
|
|
172
173
|
code_puppy/plugins/claude_code_oauth/test_plugin.py,sha256=yQy4EeZl4bjrcog1d8BjknoDTRK75mRXXvkSQJYSSEM,9286
|
|
173
174
|
code_puppy/plugins/claude_code_oauth/utils.py,sha256=Ltk_m2efGNQSSvmb1lxgcPp_vcEig8VMydTZffQP-2c,17433
|
|
174
175
|
code_puppy/plugins/customizable_commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -207,10 +208,10 @@ code_puppy/tools/browser/chromium_terminal_manager.py,sha256=w1thQ_ACb6oV45L93TS
|
|
|
207
208
|
code_puppy/tools/browser/terminal_command_tools.py,sha256=9byOZku-dwvTtCl532xt7Lumed_jTn0sLvUe_X75XCQ,19068
|
|
208
209
|
code_puppy/tools/browser/terminal_screenshot_tools.py,sha256=J_21YO_495NvYgNFu9KQP6VYg2K_f8CtSdZuF94Yhnw,18448
|
|
209
210
|
code_puppy/tools/browser/terminal_tools.py,sha256=F5LjVH3udSCFHmqC3O1UJLoLozZFZsEdX42jOmkqkW0,17853
|
|
210
|
-
code_puppy-0.0.
|
|
211
|
-
code_puppy-0.0.
|
|
212
|
-
code_puppy-0.0.
|
|
213
|
-
code_puppy-0.0.
|
|
214
|
-
code_puppy-0.0.
|
|
215
|
-
code_puppy-0.0.
|
|
216
|
-
code_puppy-0.0.
|
|
211
|
+
code_puppy-0.0.371.data/data/code_puppy/models.json,sha256=FMQdE_yvP_8y0xxt3K918UkFL9cZMYAqW1SfXcQkU_k,3105
|
|
212
|
+
code_puppy-0.0.371.data/data/code_puppy/models_dev_api.json,sha256=wHjkj-IM_fx1oHki6-GqtOoCrRMR0ScK0f-Iz0UEcy8,548187
|
|
213
|
+
code_puppy-0.0.371.dist-info/METADATA,sha256=PgrZRPN6si-vJu5KRe9vqvpSK3bluQGtev9mT_Ov7P0,27604
|
|
214
|
+
code_puppy-0.0.371.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
215
|
+
code_puppy-0.0.371.dist-info/entry_points.txt,sha256=Tp4eQC99WY3HOKd3sdvb22vZODRq0XkZVNpXOag_KdI,91
|
|
216
|
+
code_puppy-0.0.371.dist-info/licenses/LICENSE,sha256=31u8x0SPgdOq3izJX41kgFazWsM43zPEF9eskzqbJMY,1075
|
|
217
|
+
code_puppy-0.0.371.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|