code-puppy 0.0.169__py3-none-any.whl → 0.0.366__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (243) hide show
  1. code_puppy/__init__.py +7 -1
  2. code_puppy/agents/__init__.py +8 -8
  3. code_puppy/agents/agent_c_reviewer.py +155 -0
  4. code_puppy/agents/agent_code_puppy.py +9 -2
  5. code_puppy/agents/agent_code_reviewer.py +90 -0
  6. code_puppy/agents/agent_cpp_reviewer.py +132 -0
  7. code_puppy/agents/agent_creator_agent.py +48 -9
  8. code_puppy/agents/agent_golang_reviewer.py +151 -0
  9. code_puppy/agents/agent_javascript_reviewer.py +160 -0
  10. code_puppy/agents/agent_manager.py +146 -199
  11. code_puppy/agents/agent_pack_leader.py +383 -0
  12. code_puppy/agents/agent_planning.py +163 -0
  13. code_puppy/agents/agent_python_programmer.py +165 -0
  14. code_puppy/agents/agent_python_reviewer.py +90 -0
  15. code_puppy/agents/agent_qa_expert.py +163 -0
  16. code_puppy/agents/agent_qa_kitten.py +208 -0
  17. code_puppy/agents/agent_security_auditor.py +181 -0
  18. code_puppy/agents/agent_terminal_qa.py +323 -0
  19. code_puppy/agents/agent_typescript_reviewer.py +166 -0
  20. code_puppy/agents/base_agent.py +1713 -1
  21. code_puppy/agents/event_stream_handler.py +350 -0
  22. code_puppy/agents/json_agent.py +12 -1
  23. code_puppy/agents/pack/__init__.py +34 -0
  24. code_puppy/agents/pack/bloodhound.py +304 -0
  25. code_puppy/agents/pack/husky.py +321 -0
  26. code_puppy/agents/pack/retriever.py +393 -0
  27. code_puppy/agents/pack/shepherd.py +348 -0
  28. code_puppy/agents/pack/terrier.py +287 -0
  29. code_puppy/agents/pack/watchdog.py +367 -0
  30. code_puppy/agents/prompt_reviewer.py +145 -0
  31. code_puppy/agents/subagent_stream_handler.py +276 -0
  32. code_puppy/api/__init__.py +13 -0
  33. code_puppy/api/app.py +169 -0
  34. code_puppy/api/main.py +21 -0
  35. code_puppy/api/pty_manager.py +446 -0
  36. code_puppy/api/routers/__init__.py +12 -0
  37. code_puppy/api/routers/agents.py +36 -0
  38. code_puppy/api/routers/commands.py +217 -0
  39. code_puppy/api/routers/config.py +74 -0
  40. code_puppy/api/routers/sessions.py +232 -0
  41. code_puppy/api/templates/terminal.html +361 -0
  42. code_puppy/api/websocket.py +154 -0
  43. code_puppy/callbacks.py +174 -4
  44. code_puppy/chatgpt_codex_client.py +283 -0
  45. code_puppy/claude_cache_client.py +586 -0
  46. code_puppy/cli_runner.py +916 -0
  47. code_puppy/command_line/add_model_menu.py +1079 -0
  48. code_puppy/command_line/agent_menu.py +395 -0
  49. code_puppy/command_line/attachments.py +395 -0
  50. code_puppy/command_line/autosave_menu.py +605 -0
  51. code_puppy/command_line/clipboard.py +527 -0
  52. code_puppy/command_line/colors_menu.py +520 -0
  53. code_puppy/command_line/command_handler.py +233 -627
  54. code_puppy/command_line/command_registry.py +150 -0
  55. code_puppy/command_line/config_commands.py +715 -0
  56. code_puppy/command_line/core_commands.py +792 -0
  57. code_puppy/command_line/diff_menu.py +863 -0
  58. code_puppy/command_line/load_context_completion.py +15 -22
  59. code_puppy/command_line/mcp/base.py +1 -4
  60. code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
  61. code_puppy/command_line/mcp/custom_server_form.py +688 -0
  62. code_puppy/command_line/mcp/custom_server_installer.py +195 -0
  63. code_puppy/command_line/mcp/edit_command.py +148 -0
  64. code_puppy/command_line/mcp/handler.py +9 -4
  65. code_puppy/command_line/mcp/help_command.py +6 -5
  66. code_puppy/command_line/mcp/install_command.py +16 -27
  67. code_puppy/command_line/mcp/install_menu.py +685 -0
  68. code_puppy/command_line/mcp/list_command.py +3 -3
  69. code_puppy/command_line/mcp/logs_command.py +174 -65
  70. code_puppy/command_line/mcp/remove_command.py +2 -2
  71. code_puppy/command_line/mcp/restart_command.py +12 -4
  72. code_puppy/command_line/mcp/search_command.py +17 -11
  73. code_puppy/command_line/mcp/start_all_command.py +22 -13
  74. code_puppy/command_line/mcp/start_command.py +50 -31
  75. code_puppy/command_line/mcp/status_command.py +6 -7
  76. code_puppy/command_line/mcp/stop_all_command.py +11 -8
  77. code_puppy/command_line/mcp/stop_command.py +11 -10
  78. code_puppy/command_line/mcp/test_command.py +2 -2
  79. code_puppy/command_line/mcp/utils.py +1 -1
  80. code_puppy/command_line/mcp/wizard_utils.py +22 -18
  81. code_puppy/command_line/mcp_completion.py +174 -0
  82. code_puppy/command_line/model_picker_completion.py +89 -30
  83. code_puppy/command_line/model_settings_menu.py +884 -0
  84. code_puppy/command_line/motd.py +14 -8
  85. code_puppy/command_line/onboarding_slides.py +179 -0
  86. code_puppy/command_line/onboarding_wizard.py +340 -0
  87. code_puppy/command_line/pin_command_completion.py +329 -0
  88. code_puppy/command_line/prompt_toolkit_completion.py +626 -75
  89. code_puppy/command_line/session_commands.py +296 -0
  90. code_puppy/command_line/utils.py +54 -0
  91. code_puppy/config.py +1181 -51
  92. code_puppy/error_logging.py +118 -0
  93. code_puppy/gemini_code_assist.py +385 -0
  94. code_puppy/gemini_model.py +602 -0
  95. code_puppy/http_utils.py +220 -104
  96. code_puppy/keymap.py +128 -0
  97. code_puppy/main.py +5 -594
  98. code_puppy/{mcp → mcp_}/__init__.py +17 -0
  99. code_puppy/{mcp → mcp_}/async_lifecycle.py +35 -4
  100. code_puppy/{mcp → mcp_}/blocking_startup.py +70 -43
  101. code_puppy/{mcp → mcp_}/captured_stdio_server.py +2 -2
  102. code_puppy/{mcp → mcp_}/config_wizard.py +5 -5
  103. code_puppy/{mcp → mcp_}/dashboard.py +15 -6
  104. code_puppy/{mcp → mcp_}/examples/retry_example.py +4 -1
  105. code_puppy/{mcp → mcp_}/managed_server.py +66 -39
  106. code_puppy/{mcp → mcp_}/manager.py +146 -52
  107. code_puppy/mcp_/mcp_logs.py +224 -0
  108. code_puppy/{mcp → mcp_}/registry.py +6 -6
  109. code_puppy/{mcp → mcp_}/server_registry_catalog.py +25 -8
  110. code_puppy/messaging/__init__.py +199 -2
  111. code_puppy/messaging/bus.py +610 -0
  112. code_puppy/messaging/commands.py +167 -0
  113. code_puppy/messaging/markdown_patches.py +57 -0
  114. code_puppy/messaging/message_queue.py +17 -48
  115. code_puppy/messaging/messages.py +500 -0
  116. code_puppy/messaging/queue_console.py +1 -24
  117. code_puppy/messaging/renderers.py +43 -146
  118. code_puppy/messaging/rich_renderer.py +1027 -0
  119. code_puppy/messaging/spinner/__init__.py +33 -5
  120. code_puppy/messaging/spinner/console_spinner.py +92 -52
  121. code_puppy/messaging/spinner/spinner_base.py +29 -0
  122. code_puppy/messaging/subagent_console.py +461 -0
  123. code_puppy/model_factory.py +686 -80
  124. code_puppy/model_utils.py +167 -0
  125. code_puppy/models.json +86 -104
  126. code_puppy/models_dev_api.json +1 -0
  127. code_puppy/models_dev_parser.py +592 -0
  128. code_puppy/plugins/__init__.py +164 -10
  129. code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
  130. code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
  131. code_puppy/plugins/antigravity_oauth/antigravity_model.py +704 -0
  132. code_puppy/plugins/antigravity_oauth/config.py +42 -0
  133. code_puppy/plugins/antigravity_oauth/constants.py +136 -0
  134. code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
  135. code_puppy/plugins/antigravity_oauth/register_callbacks.py +406 -0
  136. code_puppy/plugins/antigravity_oauth/storage.py +271 -0
  137. code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
  138. code_puppy/plugins/antigravity_oauth/token.py +167 -0
  139. code_puppy/plugins/antigravity_oauth/transport.py +767 -0
  140. code_puppy/plugins/antigravity_oauth/utils.py +169 -0
  141. code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
  142. code_puppy/plugins/chatgpt_oauth/config.py +52 -0
  143. code_puppy/plugins/chatgpt_oauth/oauth_flow.py +328 -0
  144. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +94 -0
  145. code_puppy/plugins/chatgpt_oauth/test_plugin.py +293 -0
  146. code_puppy/plugins/chatgpt_oauth/utils.py +489 -0
  147. code_puppy/plugins/claude_code_oauth/README.md +167 -0
  148. code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
  149. code_puppy/plugins/claude_code_oauth/__init__.py +6 -0
  150. code_puppy/plugins/claude_code_oauth/config.py +50 -0
  151. code_puppy/plugins/claude_code_oauth/register_callbacks.py +308 -0
  152. code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
  153. code_puppy/plugins/claude_code_oauth/utils.py +518 -0
  154. code_puppy/plugins/customizable_commands/__init__.py +0 -0
  155. code_puppy/plugins/customizable_commands/register_callbacks.py +169 -0
  156. code_puppy/plugins/example_custom_command/README.md +280 -0
  157. code_puppy/plugins/example_custom_command/register_callbacks.py +51 -0
  158. code_puppy/plugins/file_permission_handler/__init__.py +4 -0
  159. code_puppy/plugins/file_permission_handler/register_callbacks.py +523 -0
  160. code_puppy/plugins/frontend_emitter/__init__.py +25 -0
  161. code_puppy/plugins/frontend_emitter/emitter.py +121 -0
  162. code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
  163. code_puppy/plugins/oauth_puppy_html.py +228 -0
  164. code_puppy/plugins/shell_safety/__init__.py +6 -0
  165. code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
  166. code_puppy/plugins/shell_safety/command_cache.py +156 -0
  167. code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
  168. code_puppy/prompts/antigravity_system_prompt.md +1 -0
  169. code_puppy/prompts/codex_system_prompt.md +310 -0
  170. code_puppy/pydantic_patches.py +131 -0
  171. code_puppy/reopenable_async_client.py +8 -8
  172. code_puppy/round_robin_model.py +10 -15
  173. code_puppy/session_storage.py +294 -0
  174. code_puppy/status_display.py +21 -4
  175. code_puppy/summarization_agent.py +52 -14
  176. code_puppy/terminal_utils.py +418 -0
  177. code_puppy/tools/__init__.py +139 -6
  178. code_puppy/tools/agent_tools.py +548 -49
  179. code_puppy/tools/browser/__init__.py +37 -0
  180. code_puppy/tools/browser/browser_control.py +289 -0
  181. code_puppy/tools/browser/browser_interactions.py +545 -0
  182. code_puppy/tools/browser/browser_locators.py +640 -0
  183. code_puppy/tools/browser/browser_manager.py +316 -0
  184. code_puppy/tools/browser/browser_navigation.py +251 -0
  185. code_puppy/tools/browser/browser_screenshot.py +179 -0
  186. code_puppy/tools/browser/browser_scripts.py +462 -0
  187. code_puppy/tools/browser/browser_workflows.py +221 -0
  188. code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
  189. code_puppy/tools/browser/terminal_command_tools.py +521 -0
  190. code_puppy/tools/browser/terminal_screenshot_tools.py +556 -0
  191. code_puppy/tools/browser/terminal_tools.py +525 -0
  192. code_puppy/tools/command_runner.py +941 -153
  193. code_puppy/tools/common.py +1146 -6
  194. code_puppy/tools/display.py +84 -0
  195. code_puppy/tools/file_modifications.py +288 -89
  196. code_puppy/tools/file_operations.py +352 -266
  197. code_puppy/tools/subagent_context.py +158 -0
  198. code_puppy/uvx_detection.py +242 -0
  199. code_puppy/version_checker.py +30 -11
  200. code_puppy-0.0.366.data/data/code_puppy/models.json +110 -0
  201. code_puppy-0.0.366.data/data/code_puppy/models_dev_api.json +1 -0
  202. {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/METADATA +184 -67
  203. code_puppy-0.0.366.dist-info/RECORD +217 -0
  204. {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/WHEEL +1 -1
  205. {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/entry_points.txt +1 -0
  206. code_puppy/agent.py +0 -231
  207. code_puppy/agents/agent_orchestrator.json +0 -26
  208. code_puppy/agents/runtime_manager.py +0 -272
  209. code_puppy/command_line/mcp/add_command.py +0 -183
  210. code_puppy/command_line/meta_command_handler.py +0 -153
  211. code_puppy/message_history_processor.py +0 -490
  212. code_puppy/messaging/spinner/textual_spinner.py +0 -101
  213. code_puppy/state_management.py +0 -200
  214. code_puppy/tui/__init__.py +0 -10
  215. code_puppy/tui/app.py +0 -986
  216. code_puppy/tui/components/__init__.py +0 -21
  217. code_puppy/tui/components/chat_view.py +0 -550
  218. code_puppy/tui/components/command_history_modal.py +0 -218
  219. code_puppy/tui/components/copy_button.py +0 -139
  220. code_puppy/tui/components/custom_widgets.py +0 -63
  221. code_puppy/tui/components/human_input_modal.py +0 -175
  222. code_puppy/tui/components/input_area.py +0 -167
  223. code_puppy/tui/components/sidebar.py +0 -309
  224. code_puppy/tui/components/status_bar.py +0 -182
  225. code_puppy/tui/messages.py +0 -27
  226. code_puppy/tui/models/__init__.py +0 -8
  227. code_puppy/tui/models/chat_message.py +0 -25
  228. code_puppy/tui/models/command_history.py +0 -89
  229. code_puppy/tui/models/enums.py +0 -24
  230. code_puppy/tui/screens/__init__.py +0 -15
  231. code_puppy/tui/screens/help.py +0 -130
  232. code_puppy/tui/screens/mcp_install_wizard.py +0 -803
  233. code_puppy/tui/screens/settings.py +0 -290
  234. code_puppy/tui/screens/tools.py +0 -74
  235. code_puppy-0.0.169.data/data/code_puppy/models.json +0 -128
  236. code_puppy-0.0.169.dist-info/RECORD +0 -112
  237. /code_puppy/{mcp → mcp_}/circuit_breaker.py +0 -0
  238. /code_puppy/{mcp → mcp_}/error_isolation.py +0 -0
  239. /code_puppy/{mcp → mcp_}/health_monitor.py +0 -0
  240. /code_puppy/{mcp → mcp_}/retry_manager.py +0 -0
  241. /code_puppy/{mcp → mcp_}/status_tracker.py +0 -0
  242. /code_puppy/{mcp → mcp_}/system_tools.py +0 -0
  243. {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,715 @@
1
+ """Command handlers for Code Puppy - CONFIG commands.
2
+
3
+ This module contains @register_command decorated handlers that are automatically
4
+ discovered by the command registry system.
5
+ """
6
+
7
+ import json
8
+
9
+ from code_puppy.command_line.command_registry import register_command
10
+ from code_puppy.config import get_config_keys
11
+
12
+
13
+ # Import get_commands_help from command_handler to avoid circular imports
14
+ # This will be defined in command_handler.py
15
+ def get_commands_help():
16
+ """Lazy import to avoid circular dependency."""
17
+ from code_puppy.command_line.command_handler import get_commands_help as _gch
18
+
19
+ return _gch()
20
+
21
+
22
+ @register_command(
23
+ name="show",
24
+ description="Show puppy config key-values",
25
+ usage="/show",
26
+ category="config",
27
+ )
28
+ def handle_show_command(command: str) -> bool:
29
+ """Show current puppy configuration."""
30
+ from rich.text import Text
31
+
32
+ from code_puppy.agents import get_current_agent
33
+ from code_puppy.command_line.model_picker_completion import get_active_model
34
+ from code_puppy.config import (
35
+ get_auto_save_session,
36
+ get_compaction_strategy,
37
+ get_compaction_threshold,
38
+ get_default_agent,
39
+ get_effective_temperature,
40
+ get_openai_reasoning_effort,
41
+ get_openai_verbosity,
42
+ get_owner_name,
43
+ get_protected_token_count,
44
+ get_puppy_name,
45
+ get_temperature,
46
+ get_use_dbos,
47
+ get_yolo_mode,
48
+ )
49
+ from code_puppy.keymap import get_cancel_agent_display_name
50
+ from code_puppy.messaging import emit_info
51
+
52
+ puppy_name = get_puppy_name()
53
+ owner_name = get_owner_name()
54
+ model = get_active_model()
55
+ yolo_mode = get_yolo_mode()
56
+ auto_save = get_auto_save_session()
57
+ protected_tokens = get_protected_token_count()
58
+ compaction_threshold = get_compaction_threshold()
59
+ compaction_strategy = get_compaction_strategy()
60
+ global_temperature = get_temperature()
61
+ effective_temperature = get_effective_temperature(model)
62
+
63
+ # Get current agent info
64
+ current_agent = get_current_agent()
65
+ default_agent = get_default_agent()
66
+
67
+ status_msg = f"""[bold magenta]🐶 Puppy Status[/bold magenta]
68
+
69
+ [bold]puppy_name:[/bold] [cyan]{puppy_name}[/cyan]
70
+ [bold]owner_name:[/bold] [cyan]{owner_name}[/cyan]
71
+ [bold]current_agent:[/bold] [magenta]{current_agent.display_name}[/magenta]
72
+ [bold]default_agent:[/bold] [cyan]{default_agent}[/cyan]
73
+ [bold]model:[/bold] [green]{model}[/green]
74
+ [bold]YOLO_MODE:[/bold] {"[red]ON[/red]" if yolo_mode else "[yellow]off[/yellow]"}
75
+ [bold]DBOS:[/bold] {"[green]enabled[/green]" if get_use_dbos() else "[yellow]disabled[/yellow]"} (toggle: /set enable_dbos true|false)
76
+ [bold]auto_save_session:[/bold] {"[green]enabled[/green]" if auto_save else "[yellow]disabled[/yellow]"}
77
+ [bold]protected_tokens:[/bold] [cyan]{protected_tokens:,}[/cyan] recent tokens preserved
78
+ [bold]compaction_threshold:[/bold] [cyan]{compaction_threshold:.1%}[/cyan] context usage triggers compaction
79
+ [bold]compaction_strategy:[/bold] [cyan]{compaction_strategy}[/cyan] (summarization or truncation)
80
+ [bold]reasoning_effort:[/bold] [cyan]{get_openai_reasoning_effort()}[/cyan]
81
+ [bold]verbosity:[/bold] [cyan]{get_openai_verbosity()}[/cyan]
82
+ [bold]temperature:[/bold] [cyan]{effective_temperature if effective_temperature is not None else "(model default)"}[/cyan]{" (per-model)" if effective_temperature != global_temperature and effective_temperature is not None else ""}
83
+ [bold]cancel_agent_key:[/bold] [cyan]{get_cancel_agent_display_name()}[/cyan] (options: ctrl+c, ctrl+k, ctrl+q)
84
+
85
+ """
86
+ emit_info(Text.from_markup(status_msg))
87
+ return True
88
+
89
+
90
+ @register_command(
91
+ name="reasoning",
92
+ description="Set OpenAI reasoning effort for GPT-5 models (e.g., /reasoning high)",
93
+ usage="/reasoning <minimal|low|medium|high|xhigh>",
94
+ category="config",
95
+ )
96
+ def handle_reasoning_command(command: str) -> bool:
97
+ """Set OpenAI reasoning effort level."""
98
+ from code_puppy.messaging import emit_error, emit_success, emit_warning
99
+
100
+ tokens = command.split()
101
+ if len(tokens) != 2:
102
+ emit_warning("Usage: /reasoning <minimal|low|medium|high|xhigh>")
103
+ return True
104
+
105
+ effort = tokens[1]
106
+ try:
107
+ from code_puppy.config import set_openai_reasoning_effort
108
+
109
+ set_openai_reasoning_effort(effort)
110
+ except ValueError as exc:
111
+ emit_error(str(exc))
112
+ return True
113
+
114
+ from code_puppy.config import get_openai_reasoning_effort
115
+
116
+ normalized_effort = get_openai_reasoning_effort()
117
+
118
+ from code_puppy.agents.agent_manager import get_current_agent
119
+
120
+ agent = get_current_agent()
121
+ agent.reload_code_generation_agent()
122
+ emit_success(
123
+ f"Reasoning effort set to '{normalized_effort}' and active agent reloaded"
124
+ )
125
+ return True
126
+
127
+
128
+ @register_command(
129
+ name="verbosity",
130
+ description="Set OpenAI verbosity for GPT-5 models (e.g., /verbosity high)",
131
+ usage="/verbosity <low|medium|high>",
132
+ category="config",
133
+ )
134
+ def handle_verbosity_command(command: str) -> bool:
135
+ """Set OpenAI verbosity level.
136
+
137
+ Controls how concise vs. verbose the model's responses are:
138
+ - low: more concise responses
139
+ - medium: balanced (default)
140
+ - high: more verbose responses
141
+ """
142
+ from code_puppy.messaging import emit_error, emit_success, emit_warning
143
+
144
+ tokens = command.split()
145
+ if len(tokens) != 2:
146
+ emit_warning("Usage: /verbosity <low|medium|high>")
147
+ return True
148
+
149
+ verbosity = tokens[1]
150
+ try:
151
+ from code_puppy.config import set_openai_verbosity
152
+
153
+ set_openai_verbosity(verbosity)
154
+ except ValueError as exc:
155
+ emit_error(str(exc))
156
+ return True
157
+
158
+ from code_puppy.config import get_openai_verbosity
159
+
160
+ normalized_verbosity = get_openai_verbosity()
161
+
162
+ from code_puppy.agents.agent_manager import get_current_agent
163
+
164
+ agent = get_current_agent()
165
+ agent.reload_code_generation_agent()
166
+ emit_success(f"Verbosity set to '{normalized_verbosity}' and active agent reloaded")
167
+ return True
168
+
169
+
170
+ @register_command(
171
+ name="set",
172
+ description="Set puppy config (e.g., /set yolo_mode true)",
173
+ usage="/set <key> <value>",
174
+ category="config",
175
+ )
176
+ def handle_set_command(command: str) -> bool:
177
+ """Set configuration values."""
178
+ from rich.text import Text
179
+
180
+ from code_puppy.config import set_config_value
181
+ from code_puppy.messaging import emit_error, emit_info, emit_success, emit_warning
182
+
183
+ tokens = command.split(None, 2)
184
+ argstr = command[len("/set") :].strip()
185
+ key = None
186
+ value = None
187
+ if "=" in argstr:
188
+ key, value = argstr.split("=", 1)
189
+ key = key.strip()
190
+ value = value.strip()
191
+ elif len(tokens) >= 3:
192
+ key = tokens[1]
193
+ value = tokens[2]
194
+ elif len(tokens) == 2:
195
+ key = tokens[1]
196
+ value = ""
197
+ else:
198
+ config_keys = get_config_keys()
199
+ if "compaction_strategy" not in config_keys:
200
+ config_keys.append("compaction_strategy")
201
+ session_help = (
202
+ "\n[yellow]Session Management[/yellow]"
203
+ "\n [cyan]auto_save_session[/cyan] Auto-save chat after every response (true/false)"
204
+ )
205
+ keymap_help = (
206
+ "\n[yellow]Keyboard Shortcuts[/yellow]"
207
+ "\n [cyan]cancel_agent_key[/cyan] Key to cancel agent tasks (ctrl+c, ctrl+k, or ctrl+q)"
208
+ )
209
+ emit_warning(
210
+ Text.from_markup(
211
+ f"Usage: /set KEY=VALUE or /set KEY VALUE\nConfig keys: {', '.join(config_keys)}\n[dim]Note: compaction_strategy can be 'summarization' or 'truncation'[/dim]{session_help}{keymap_help}"
212
+ )
213
+ )
214
+ return True
215
+ if key:
216
+ # Check if we're toggling DBOS enablement
217
+ if key == "enable_dbos":
218
+ emit_info(
219
+ Text.from_markup(
220
+ "[yellow]āš ļø DBOS configuration changed. Please restart Code Puppy for this change to take effect.[/yellow]"
221
+ )
222
+ )
223
+
224
+ # Validate cancel_agent_key before setting
225
+ if key == "cancel_agent_key":
226
+ from code_puppy.keymap import VALID_CANCEL_KEYS
227
+
228
+ normalized_value = value.strip().lower()
229
+ if normalized_value not in VALID_CANCEL_KEYS:
230
+ emit_error(
231
+ f"Invalid cancel_agent_key '{value}'. Valid options: {', '.join(sorted(VALID_CANCEL_KEYS))}"
232
+ )
233
+ return True
234
+ value = normalized_value # Use normalized value
235
+ emit_info(
236
+ Text.from_markup(
237
+ "[yellow]āš ļø cancel_agent_key changed. Please restart Code Puppy for this change to take effect.[/yellow]"
238
+ )
239
+ )
240
+
241
+ set_config_value(key, value)
242
+ emit_success(f'Set {key} = "{value}" in puppy.cfg!')
243
+
244
+ # Reload the current agent to pick up the new config
245
+ from code_puppy.agents import get_current_agent
246
+
247
+ try:
248
+ current_agent = get_current_agent()
249
+ current_agent.reload_code_generation_agent()
250
+ emit_info("Agent reloaded with updated config")
251
+ except Exception as reload_error:
252
+ emit_warning(f"Config saved but agent reload failed: {reload_error}")
253
+ else:
254
+ emit_error("You must supply a key.")
255
+ return True
256
+
257
+
258
+ def _get_json_agents_pinned_to_model(model_name: str) -> list:
259
+ """Get JSON agents that have this model pinned in their JSON file."""
260
+ from code_puppy.agents.json_agent import discover_json_agents
261
+
262
+ pinned = []
263
+ json_agents = discover_json_agents()
264
+ for agent_name, agent_path in json_agents.items():
265
+ try:
266
+ with open(agent_path, "r") as f:
267
+ agent_data = json.load(f)
268
+ if agent_data.get("model") == model_name:
269
+ pinned.append(agent_name)
270
+ except Exception:
271
+ continue
272
+ return pinned
273
+
274
+
275
+ @register_command(
276
+ name="pin_model",
277
+ description="Pin a specific model to an agent",
278
+ usage="/pin_model <agent> <model>",
279
+ category="config",
280
+ )
281
+ def handle_pin_model_command(command: str) -> bool:
282
+ """Pin a specific model to an agent."""
283
+ from code_puppy.agents.json_agent import discover_json_agents
284
+ from code_puppy.command_line.model_picker_completion import load_model_names
285
+ from code_puppy.messaging import emit_error, emit_info, emit_success, emit_warning
286
+
287
+ tokens = command.split()
288
+
289
+ if len(tokens) != 3:
290
+ emit_warning("Usage: /pin_model <agent-name> <model-name>")
291
+
292
+ # Show available models and agents
293
+ available_models = load_model_names()
294
+ json_agents = discover_json_agents()
295
+
296
+ # Get built-in agents
297
+ from code_puppy.agents.agent_manager import get_agent_descriptions
298
+
299
+ builtin_agents = get_agent_descriptions()
300
+
301
+ emit_info("Available models:")
302
+ for model in available_models:
303
+ emit_info(f" {model}")
304
+
305
+ if builtin_agents:
306
+ emit_info("\nAvailable built-in agents:")
307
+ for agent_name, description in builtin_agents.items():
308
+ emit_info(f" {agent_name} - {description}")
309
+
310
+ if json_agents:
311
+ emit_info("\nAvailable JSON agents:")
312
+ for agent_name, agent_path in json_agents.items():
313
+ emit_info(f" {agent_name} ({agent_path})")
314
+ return True
315
+
316
+ agent_name = tokens[1].lower()
317
+ model_name = tokens[2]
318
+
319
+ # Handle special case: (unpin) option (case-insensitive)
320
+ if model_name.lower() == "(unpin)":
321
+ # Delegate to unpin command
322
+ return handle_unpin_command(f"/unpin {agent_name}")
323
+
324
+ # Check if model exists
325
+ available_models = load_model_names()
326
+ if model_name not in available_models:
327
+ emit_error(f"Model '{model_name}' not found")
328
+ emit_warning(f"Available models: {', '.join(available_models)}")
329
+ return True
330
+
331
+ # Check if this is a JSON agent or a built-in Python agent
332
+ json_agents = discover_json_agents()
333
+
334
+ # Get list of available built-in agents
335
+ from code_puppy.agents.agent_manager import get_agent_descriptions
336
+
337
+ builtin_agents = get_agent_descriptions()
338
+
339
+ is_json_agent = agent_name in json_agents
340
+ is_builtin_agent = agent_name in builtin_agents
341
+
342
+ if not is_json_agent and not is_builtin_agent:
343
+ emit_error(f"Agent '{agent_name}' not found")
344
+
345
+ # Show available agents
346
+ if builtin_agents:
347
+ emit_info("Available built-in agents:")
348
+ for name, desc in builtin_agents.items():
349
+ emit_info(f" {name} - {desc}")
350
+
351
+ if json_agents:
352
+ emit_info("\nAvailable JSON agents:")
353
+ for name, path in json_agents.items():
354
+ emit_info(f" {name} ({path})")
355
+ return True
356
+
357
+ # Handle different agent types
358
+ try:
359
+ if is_json_agent:
360
+ # Handle JSON agent - modify the JSON file
361
+ agent_file_path = json_agents[agent_name]
362
+
363
+ with open(agent_file_path, "r", encoding="utf-8") as f:
364
+ agent_config = json.load(f)
365
+
366
+ # Set the model
367
+ agent_config["model"] = model_name
368
+
369
+ # Save the updated configuration
370
+ with open(agent_file_path, "w", encoding="utf-8") as f:
371
+ json.dump(agent_config, f, indent=2, ensure_ascii=False)
372
+
373
+ else:
374
+ # Handle built-in Python agent - store in config
375
+ from code_puppy.config import set_agent_pinned_model
376
+
377
+ set_agent_pinned_model(agent_name, model_name)
378
+
379
+ emit_success(f"Model '{model_name}' pinned to agent '{agent_name}'")
380
+
381
+ # If this is the current agent, refresh it so the prompt updates immediately
382
+ from code_puppy.agents import get_current_agent
383
+
384
+ current_agent = get_current_agent()
385
+ if current_agent.name == agent_name:
386
+ try:
387
+ if is_json_agent and hasattr(current_agent, "refresh_config"):
388
+ current_agent.refresh_config()
389
+ current_agent.reload_code_generation_agent()
390
+ emit_info(f"Active agent reloaded with pinned model '{model_name}'")
391
+ except Exception as reload_error:
392
+ emit_warning(f"Pinned model applied but reload failed: {reload_error}")
393
+
394
+ return True
395
+
396
+ except Exception as e:
397
+ emit_error(f"Failed to pin model to agent '{agent_name}': {e}")
398
+ return True
399
+
400
+
401
+ @register_command(
402
+ name="unpin",
403
+ description="Unpin a model from an agent (resets to default)",
404
+ usage="/unpin <agent>",
405
+ category="config",
406
+ )
407
+ def handle_unpin_command(command: str) -> bool:
408
+ """Unpin a model from an agent (resets to default)."""
409
+ from code_puppy.agents.json_agent import discover_json_agents
410
+ from code_puppy.config import get_agent_pinned_model
411
+ from code_puppy.messaging import emit_error, emit_info, emit_success, emit_warning
412
+
413
+ tokens = command.split()
414
+
415
+ if len(tokens) != 2:
416
+ emit_warning("Usage: /unpin <agent-name>")
417
+
418
+ # Show available agents
419
+ json_agents = discover_json_agents()
420
+
421
+ # Get built-in agents
422
+ from code_puppy.agents.agent_manager import get_agent_descriptions
423
+
424
+ builtin_agents = get_agent_descriptions()
425
+
426
+ if builtin_agents:
427
+ emit_info("Available built-in agents:")
428
+ for agent_name, description in builtin_agents.items():
429
+ pinned_model = get_agent_pinned_model(agent_name)
430
+ if pinned_model:
431
+ emit_info(f" {agent_name} - {description} [→ {pinned_model}]")
432
+ else:
433
+ emit_info(f" {agent_name} - {description}")
434
+
435
+ if json_agents:
436
+ emit_info("\nAvailable JSON agents:")
437
+ for agent_name, agent_path in json_agents.items():
438
+ # Read the JSON file to check for pinned model
439
+ try:
440
+ with open(agent_path, "r") as f:
441
+ agent_config = json.load(f)
442
+ pinned_model = agent_config.get("model")
443
+ if pinned_model:
444
+ emit_info(f" {agent_name} ({agent_path}) [→ {pinned_model}]")
445
+ else:
446
+ emit_info(f" {agent_name} ({agent_path})")
447
+ except Exception:
448
+ emit_info(f" {agent_name} ({agent_path})")
449
+ return True
450
+
451
+ agent_name_input = tokens[1].lower()
452
+
453
+ # Check if this is a JSON agent or a built-in Python agent
454
+ json_agents = discover_json_agents()
455
+
456
+ # Get list of available built-in agents
457
+ from code_puppy.agents.agent_manager import get_agent_descriptions
458
+
459
+ builtin_agents = get_agent_descriptions()
460
+
461
+ # Find matching agent (case-insensitive)
462
+ agent_name = None
463
+ is_json_agent = False
464
+ is_builtin_agent = False
465
+
466
+ # Check JSON agents (case-insensitive)
467
+ for json_agent_name in json_agents:
468
+ if json_agent_name.lower() == agent_name_input:
469
+ agent_name = json_agent_name
470
+ is_json_agent = True
471
+ break
472
+
473
+ # Check built-in agents (case-insensitive)
474
+ if not is_json_agent:
475
+ for builtin_agent_name in builtin_agents:
476
+ if builtin_agent_name.lower() == agent_name_input:
477
+ agent_name = builtin_agent_name
478
+ is_builtin_agent = True
479
+ break
480
+
481
+ if not is_json_agent and not is_builtin_agent:
482
+ emit_error(f"Agent '{agent_name_input}' not found")
483
+
484
+ # Show available agents
485
+ if builtin_agents:
486
+ emit_info("Available built-in agents:")
487
+ for name, desc in builtin_agents.items():
488
+ emit_info(f" {name} - {desc}")
489
+
490
+ if json_agents:
491
+ emit_info("\nAvailable JSON agents:")
492
+ for name, path in json_agents.items():
493
+ emit_info(f" {name} ({path})")
494
+ return True
495
+
496
+ try:
497
+ if is_json_agent:
498
+ # Handle JSON agent - remove the model from JSON file
499
+ agent_file_path = json_agents[agent_name]
500
+
501
+ with open(agent_file_path, "r", encoding="utf-8") as f:
502
+ agent_config = json.load(f)
503
+
504
+ # Remove the model key if it exists
505
+ if "model" in agent_config:
506
+ del agent_config["model"]
507
+
508
+ # Save the updated configuration
509
+ with open(agent_file_path, "w", encoding="utf-8") as f:
510
+ json.dump(agent_config, f, indent=2, ensure_ascii=False)
511
+
512
+ else:
513
+ # Handle built-in Python agent - clear from config
514
+ from code_puppy.config import clear_agent_pinned_model
515
+
516
+ clear_agent_pinned_model(agent_name)
517
+
518
+ emit_success(f"Model unpinned from agent '{agent_name}' (reset to default)")
519
+
520
+ # If this is the current agent, refresh it so the prompt updates immediately
521
+ from code_puppy.agents import get_current_agent
522
+
523
+ current_agent = get_current_agent()
524
+ if current_agent.name == agent_name:
525
+ try:
526
+ if is_json_agent and hasattr(current_agent, "refresh_config"):
527
+ current_agent.refresh_config()
528
+ current_agent.reload_code_generation_agent()
529
+ emit_info("Active agent reloaded with default model")
530
+ except Exception as reload_error:
531
+ emit_warning(f"Model unpinned but reload failed: {reload_error}")
532
+
533
+ return True
534
+
535
+ except Exception as e:
536
+ emit_error(f"Failed to unpin model from agent '{agent_name}': {e}")
537
+ return True
538
+
539
+
540
+ @register_command(
541
+ name="diff",
542
+ description="Configure diff highlighting colors (additions, deletions)",
543
+ usage="/diff",
544
+ category="config",
545
+ )
546
+ def handle_diff_command(command: str) -> bool:
547
+ """Configure diff highlighting colors."""
548
+ import asyncio
549
+ import concurrent.futures
550
+
551
+ from code_puppy.command_line.diff_menu import interactive_diff_picker
552
+ from code_puppy.config import (
553
+ set_diff_addition_color,
554
+ set_diff_deletion_color,
555
+ )
556
+ from code_puppy.messaging import emit_error
557
+
558
+ # Show interactive picker for diff configuration
559
+ with concurrent.futures.ThreadPoolExecutor() as executor:
560
+ future = executor.submit(lambda: asyncio.run(interactive_diff_picker()))
561
+ result = future.result(timeout=300) # 5 min timeout
562
+
563
+ if result:
564
+ # Apply the changes silently (no console output)
565
+ try:
566
+ set_diff_addition_color(result["add_color"])
567
+ set_diff_deletion_color(result["del_color"])
568
+ except Exception as e:
569
+ emit_error(f"Failed to apply diff settings: {e}")
570
+ return True
571
+
572
+
573
+ @register_command(
574
+ name="colors",
575
+ description="Configure banner colors for tool outputs (THINKING, SHELL COMMAND, etc.)",
576
+ usage="/colors",
577
+ category="config",
578
+ )
579
+ def handle_colors_command(command: str) -> bool:
580
+ """Configure banner colors via interactive TUI."""
581
+ import asyncio
582
+ import concurrent.futures
583
+
584
+ from code_puppy.command_line.colors_menu import interactive_colors_picker
585
+ from code_puppy.config import set_banner_color
586
+ from code_puppy.messaging import emit_error, emit_success
587
+
588
+ # Show interactive picker for banner color configuration
589
+ with concurrent.futures.ThreadPoolExecutor() as executor:
590
+ future = executor.submit(lambda: asyncio.run(interactive_colors_picker()))
591
+ result = future.result(timeout=300) # 5 min timeout
592
+
593
+ if result:
594
+ # Apply the changes
595
+ try:
596
+ for banner_name, color in result.items():
597
+ set_banner_color(banner_name, color)
598
+ emit_success("Banner colors saved! šŸŽØ")
599
+ except Exception as e:
600
+ emit_error(f"Failed to apply banner color settings: {e}")
601
+ return True
602
+
603
+
604
+ # ============================================================================
605
+ # UTILITY FUNCTIONS
606
+ # ============================================================================
607
+
608
+
609
+ def _show_color_options(color_type: str):
610
+ # ============================================================================
611
+ # UTILITY FUNCTIONS
612
+ # ============================================================================
613
+
614
+ """Show available Rich color options organized by category."""
615
+ from rich.text import Text
616
+
617
+ from code_puppy.messaging import emit_info
618
+
619
+ # Standard Rich colors organized by category
620
+ color_categories = {
621
+ "Basic Colors": [
622
+ ("black", "⚫"),
623
+ ("red", "šŸ”“"),
624
+ ("green", "🟢"),
625
+ ("yellow", "🟔"),
626
+ ("blue", "šŸ”µ"),
627
+ ("magenta", "🟣"),
628
+ ("cyan", "šŸ”·"),
629
+ ("white", "⚪"),
630
+ ],
631
+ "Bright Colors": [
632
+ ("bright_black", "⚫"),
633
+ ("bright_red", "šŸ”“"),
634
+ ("bright_green", "🟢"),
635
+ ("bright_yellow", "🟔"),
636
+ ("bright_blue", "šŸ”µ"),
637
+ ("bright_magenta", "🟣"),
638
+ ("bright_cyan", "šŸ”·"),
639
+ ("bright_white", "⚪"),
640
+ ],
641
+ "Special Colors": [
642
+ ("orange1", "🟠"),
643
+ ("orange3", "🟠"),
644
+ ("orange4", "🟠"),
645
+ ("deep_sky_blue1", "šŸ”·"),
646
+ ("deep_sky_blue2", "šŸ”·"),
647
+ ("deep_sky_blue3", "šŸ”·"),
648
+ ("deep_sky_blue4", "šŸ”·"),
649
+ ("turquoise2", "šŸ”·"),
650
+ ("turquoise4", "šŸ”·"),
651
+ ("steel_blue1", "šŸ”·"),
652
+ ("steel_blue3", "šŸ”·"),
653
+ ("chartreuse1", "🟢"),
654
+ ("chartreuse2", "🟢"),
655
+ ("chartreuse3", "🟢"),
656
+ ("chartreuse4", "🟢"),
657
+ ("gold1", "🟔"),
658
+ ("gold3", "🟔"),
659
+ ("rosy_brown", "šŸ”“"),
660
+ ("indian_red", "šŸ”“"),
661
+ ],
662
+ }
663
+
664
+ # Suggested colors for each type
665
+ if color_type == "additions":
666
+ suggestions = [
667
+ ("green", "🟢"),
668
+ ("bright_green", "🟢"),
669
+ ("chartreuse1", "🟢"),
670
+ ("green3", "🟢"),
671
+ ("sea_green1", "🟢"),
672
+ ]
673
+ emit_info(
674
+ Text.from_markup(
675
+ "[bold white on green]šŸŽØ Recommended Colors for Additions:[/bold white on green]"
676
+ )
677
+ )
678
+ for color, emoji in suggestions:
679
+ emit_info(
680
+ Text.from_markup(
681
+ f" [cyan]{color:<16}[/cyan] [white on {color}]ā– ā– ā– ā– ā– ā– ā– ā– ā– ā– [/white on {color}] {emoji}"
682
+ )
683
+ )
684
+ elif color_type == "deletions":
685
+ suggestions = [
686
+ ("orange1", "🟠"),
687
+ ("red", "šŸ”“"),
688
+ ("bright_red", "šŸ”“"),
689
+ ("indian_red", "šŸ”“"),
690
+ ("dark_red", "šŸ”“"),
691
+ ]
692
+ emit_info(
693
+ Text.from_markup(
694
+ "[bold white on orange1]šŸŽØ Recommended Colors for Deletions:[/bold white on orange1]"
695
+ )
696
+ )
697
+ for color, emoji in suggestions:
698
+ emit_info(
699
+ Text.from_markup(
700
+ f" [cyan]{color:<16}[/cyan] [white on {color}]ā– ā– ā– ā– ā– ā– ā– ā– ā– ā– [/white on {color}] {emoji}"
701
+ )
702
+ )
703
+
704
+ emit_info("\nšŸŽØ All Available Rich Colors:")
705
+ for category, colors in color_categories.items():
706
+ emit_info(f"\n{category}:")
707
+ # Display in columns for better readability
708
+ for i in range(0, len(colors), 4):
709
+ row = colors[i : i + 4]
710
+ row_text = " ".join([f"[{color}]ā– [/{color}] {color}" for color, _ in row])
711
+ emit_info(Text.from_markup(f" {row_text}"))
712
+
713
+ emit_info("\nUsage: /diff {color_type} <color_name>")
714
+ emit_info("All diffs use white text on your chosen background colors")
715
+ emit_info("You can also use hex colors like #ff0000 or rgb(255,0,0)")