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.
@@ -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
- # Initial display
374
- update_display()
375
-
376
- # Clear the current buffer
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
- # Run application
381
- await app.run_async()
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.config import set_model_name
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
- set_model_name("chatgpt-gpt-5.2-codex")
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
- set_model_name("claude-code-claude-opus-4-5-20251101")
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, 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):
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
- 1. Look at ``model`` in *puppy.cfg*.
463
- 2. If that value exists **and** is present in *models.json*, use it.
464
- 3. Otherwise return the first model listed in *models.json*.
465
- 4. As a last resort (e.g.
466
- *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.
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
- return stored_model
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
- return _default_model_from_models_json()
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(add_account: bool = False) -> bool:
169
- """Run the OAuth authentication flow."""
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
- # Reload agent
230
- reload_current_agent()
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
- # Set a default model
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
- set_model_name("chatgpt-gpt-5.2-codex")
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
- set_model_name("claude-code-claude-opus-4-5-20251101")
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":
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-puppy
3
- Version: 0.0.370
3
+ Version: 0.0.371
4
4
  Summary: Code generation agent
5
5
  Project-URL: repository, https://github.com/mpfaffenberger/code_puppy
6
6
  Project-URL: HomePage, https://github.com/mpfaffenberger/code_puppy
@@ -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=blowBU3bBOdQSuLYKBUrb7f7CxHH_e25a_A4lQGsjgk,53494
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=9giVbeBb8YAc51PC9EhR0h15NGwbzKLB3HECCK5iaKo,610
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=i4s9RnKyMTB0e8yg3YhU7MMx_dtzr1s4lwY-tcsyyjc,15610
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=uItsjRXZNdgYbfNC3hWIuaf9k3jYYdRbVZgqVkmHW_M,11660
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=OF3u5MKmj78zEvX15k5BdcU9CbVTEJJYKz24HX_Dmtk,27088
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=nDnlf0qFCG2zAm_mWW2eMYwVC7eROVQrFe92hZqOKa8,6810
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=uKIvfzH-dXj1g_5_gbD1FFgJ_fOYlsFtt5UL1EGqBc0,13121
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=oPfAOdh5hp3Jg3tK5ylk1sy0ydxBebK9a9w1EL1dw9I,2965
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=g8sl-i7jIOF6OFALeaLqTF3mS4tD8GR_FCzvPjVw2js,10165
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.370.data/data/code_puppy/models.json,sha256=FMQdE_yvP_8y0xxt3K918UkFL9cZMYAqW1SfXcQkU_k,3105
211
- code_puppy-0.0.370.data/data/code_puppy/models_dev_api.json,sha256=wHjkj-IM_fx1oHki6-GqtOoCrRMR0ScK0f-Iz0UEcy8,548187
212
- code_puppy-0.0.370.dist-info/METADATA,sha256=QRcr3bKbeLgximqWhFtS83g4w3IJDKrEWMT3DDSEeDM,27604
213
- code_puppy-0.0.370.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
214
- code_puppy-0.0.370.dist-info/entry_points.txt,sha256=Tp4eQC99WY3HOKd3sdvb22vZODRq0XkZVNpXOag_KdI,91
215
- code_puppy-0.0.370.dist-info/licenses/LICENSE,sha256=31u8x0SPgdOq3izJX41kgFazWsM43zPEF9eskzqbJMY,1075
216
- code_puppy-0.0.370.dist-info/RECORD,,
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,,