codepp 0.0.437__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 (288) hide show
  1. code_puppy/__init__.py +10 -0
  2. code_puppy/__main__.py +10 -0
  3. code_puppy/agents/__init__.py +31 -0
  4. code_puppy/agents/agent_c_reviewer.py +155 -0
  5. code_puppy/agents/agent_code_puppy.py +117 -0
  6. code_puppy/agents/agent_code_reviewer.py +90 -0
  7. code_puppy/agents/agent_cpp_reviewer.py +132 -0
  8. code_puppy/agents/agent_creator_agent.py +638 -0
  9. code_puppy/agents/agent_golang_reviewer.py +151 -0
  10. code_puppy/agents/agent_helios.py +124 -0
  11. code_puppy/agents/agent_javascript_reviewer.py +160 -0
  12. code_puppy/agents/agent_manager.py +742 -0
  13. code_puppy/agents/agent_pack_leader.py +385 -0
  14. code_puppy/agents/agent_planning.py +165 -0
  15. code_puppy/agents/agent_python_programmer.py +169 -0
  16. code_puppy/agents/agent_python_reviewer.py +90 -0
  17. code_puppy/agents/agent_qa_expert.py +163 -0
  18. code_puppy/agents/agent_qa_kitten.py +208 -0
  19. code_puppy/agents/agent_scheduler.py +121 -0
  20. code_puppy/agents/agent_security_auditor.py +181 -0
  21. code_puppy/agents/agent_terminal_qa.py +323 -0
  22. code_puppy/agents/agent_typescript_reviewer.py +166 -0
  23. code_puppy/agents/base_agent.py +2156 -0
  24. code_puppy/agents/event_stream_handler.py +348 -0
  25. code_puppy/agents/json_agent.py +202 -0
  26. code_puppy/agents/pack/__init__.py +34 -0
  27. code_puppy/agents/pack/bloodhound.py +304 -0
  28. code_puppy/agents/pack/husky.py +327 -0
  29. code_puppy/agents/pack/retriever.py +393 -0
  30. code_puppy/agents/pack/shepherd.py +348 -0
  31. code_puppy/agents/pack/terrier.py +287 -0
  32. code_puppy/agents/pack/watchdog.py +367 -0
  33. code_puppy/agents/prompt_reviewer.py +145 -0
  34. code_puppy/agents/subagent_stream_handler.py +276 -0
  35. code_puppy/api/__init__.py +13 -0
  36. code_puppy/api/app.py +169 -0
  37. code_puppy/api/main.py +21 -0
  38. code_puppy/api/pty_manager.py +453 -0
  39. code_puppy/api/routers/__init__.py +12 -0
  40. code_puppy/api/routers/agents.py +36 -0
  41. code_puppy/api/routers/commands.py +217 -0
  42. code_puppy/api/routers/config.py +75 -0
  43. code_puppy/api/routers/sessions.py +234 -0
  44. code_puppy/api/templates/terminal.html +361 -0
  45. code_puppy/api/websocket.py +154 -0
  46. code_puppy/callbacks.py +692 -0
  47. code_puppy/chatgpt_codex_client.py +338 -0
  48. code_puppy/claude_cache_client.py +672 -0
  49. code_puppy/cli_runner.py +1073 -0
  50. code_puppy/command_line/__init__.py +1 -0
  51. code_puppy/command_line/add_model_menu.py +1092 -0
  52. code_puppy/command_line/agent_menu.py +662 -0
  53. code_puppy/command_line/attachments.py +395 -0
  54. code_puppy/command_line/autosave_menu.py +704 -0
  55. code_puppy/command_line/clipboard.py +527 -0
  56. code_puppy/command_line/colors_menu.py +532 -0
  57. code_puppy/command_line/command_handler.py +293 -0
  58. code_puppy/command_line/command_registry.py +150 -0
  59. code_puppy/command_line/config_commands.py +719 -0
  60. code_puppy/command_line/core_commands.py +867 -0
  61. code_puppy/command_line/diff_menu.py +865 -0
  62. code_puppy/command_line/file_path_completion.py +73 -0
  63. code_puppy/command_line/load_context_completion.py +52 -0
  64. code_puppy/command_line/mcp/__init__.py +10 -0
  65. code_puppy/command_line/mcp/base.py +32 -0
  66. code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
  67. code_puppy/command_line/mcp/custom_server_form.py +688 -0
  68. code_puppy/command_line/mcp/custom_server_installer.py +195 -0
  69. code_puppy/command_line/mcp/edit_command.py +148 -0
  70. code_puppy/command_line/mcp/handler.py +138 -0
  71. code_puppy/command_line/mcp/help_command.py +147 -0
  72. code_puppy/command_line/mcp/install_command.py +214 -0
  73. code_puppy/command_line/mcp/install_menu.py +705 -0
  74. code_puppy/command_line/mcp/list_command.py +94 -0
  75. code_puppy/command_line/mcp/logs_command.py +235 -0
  76. code_puppy/command_line/mcp/remove_command.py +82 -0
  77. code_puppy/command_line/mcp/restart_command.py +100 -0
  78. code_puppy/command_line/mcp/search_command.py +123 -0
  79. code_puppy/command_line/mcp/start_all_command.py +135 -0
  80. code_puppy/command_line/mcp/start_command.py +117 -0
  81. code_puppy/command_line/mcp/status_command.py +184 -0
  82. code_puppy/command_line/mcp/stop_all_command.py +112 -0
  83. code_puppy/command_line/mcp/stop_command.py +80 -0
  84. code_puppy/command_line/mcp/test_command.py +107 -0
  85. code_puppy/command_line/mcp/utils.py +129 -0
  86. code_puppy/command_line/mcp/wizard_utils.py +334 -0
  87. code_puppy/command_line/mcp_completion.py +174 -0
  88. code_puppy/command_line/model_picker_completion.py +197 -0
  89. code_puppy/command_line/model_settings_menu.py +932 -0
  90. code_puppy/command_line/motd.py +96 -0
  91. code_puppy/command_line/onboarding_slides.py +179 -0
  92. code_puppy/command_line/onboarding_wizard.py +342 -0
  93. code_puppy/command_line/pin_command_completion.py +329 -0
  94. code_puppy/command_line/prompt_toolkit_completion.py +846 -0
  95. code_puppy/command_line/session_commands.py +302 -0
  96. code_puppy/command_line/shell_passthrough.py +145 -0
  97. code_puppy/command_line/skills_completion.py +160 -0
  98. code_puppy/command_line/uc_menu.py +893 -0
  99. code_puppy/command_line/utils.py +93 -0
  100. code_puppy/command_line/wiggum_state.py +78 -0
  101. code_puppy/config.py +1770 -0
  102. code_puppy/error_logging.py +134 -0
  103. code_puppy/gemini_code_assist.py +385 -0
  104. code_puppy/gemini_model.py +754 -0
  105. code_puppy/hook_engine/README.md +105 -0
  106. code_puppy/hook_engine/__init__.py +21 -0
  107. code_puppy/hook_engine/aliases.py +155 -0
  108. code_puppy/hook_engine/engine.py +221 -0
  109. code_puppy/hook_engine/executor.py +296 -0
  110. code_puppy/hook_engine/matcher.py +156 -0
  111. code_puppy/hook_engine/models.py +240 -0
  112. code_puppy/hook_engine/registry.py +106 -0
  113. code_puppy/hook_engine/validator.py +144 -0
  114. code_puppy/http_utils.py +361 -0
  115. code_puppy/keymap.py +128 -0
  116. code_puppy/main.py +10 -0
  117. code_puppy/mcp_/__init__.py +66 -0
  118. code_puppy/mcp_/async_lifecycle.py +286 -0
  119. code_puppy/mcp_/blocking_startup.py +469 -0
  120. code_puppy/mcp_/captured_stdio_server.py +275 -0
  121. code_puppy/mcp_/circuit_breaker.py +290 -0
  122. code_puppy/mcp_/config_wizard.py +507 -0
  123. code_puppy/mcp_/dashboard.py +308 -0
  124. code_puppy/mcp_/error_isolation.py +407 -0
  125. code_puppy/mcp_/examples/retry_example.py +226 -0
  126. code_puppy/mcp_/health_monitor.py +589 -0
  127. code_puppy/mcp_/managed_server.py +428 -0
  128. code_puppy/mcp_/manager.py +807 -0
  129. code_puppy/mcp_/mcp_logs.py +224 -0
  130. code_puppy/mcp_/registry.py +451 -0
  131. code_puppy/mcp_/retry_manager.py +337 -0
  132. code_puppy/mcp_/server_registry_catalog.py +1126 -0
  133. code_puppy/mcp_/status_tracker.py +355 -0
  134. code_puppy/mcp_/system_tools.py +209 -0
  135. code_puppy/mcp_prompts/__init__.py +1 -0
  136. code_puppy/mcp_prompts/hook_creator.py +103 -0
  137. code_puppy/messaging/__init__.py +255 -0
  138. code_puppy/messaging/bus.py +613 -0
  139. code_puppy/messaging/commands.py +167 -0
  140. code_puppy/messaging/markdown_patches.py +57 -0
  141. code_puppy/messaging/message_queue.py +361 -0
  142. code_puppy/messaging/messages.py +569 -0
  143. code_puppy/messaging/queue_console.py +271 -0
  144. code_puppy/messaging/renderers.py +311 -0
  145. code_puppy/messaging/rich_renderer.py +1158 -0
  146. code_puppy/messaging/spinner/__init__.py +83 -0
  147. code_puppy/messaging/spinner/console_spinner.py +240 -0
  148. code_puppy/messaging/spinner/spinner_base.py +95 -0
  149. code_puppy/messaging/subagent_console.py +460 -0
  150. code_puppy/model_factory.py +848 -0
  151. code_puppy/model_switching.py +63 -0
  152. code_puppy/model_utils.py +168 -0
  153. code_puppy/models.json +174 -0
  154. code_puppy/models_dev_api.json +1 -0
  155. code_puppy/models_dev_parser.py +592 -0
  156. code_puppy/plugins/__init__.py +186 -0
  157. code_puppy/plugins/agent_skills/__init__.py +22 -0
  158. code_puppy/plugins/agent_skills/config.py +175 -0
  159. code_puppy/plugins/agent_skills/discovery.py +136 -0
  160. code_puppy/plugins/agent_skills/downloader.py +392 -0
  161. code_puppy/plugins/agent_skills/installer.py +22 -0
  162. code_puppy/plugins/agent_skills/metadata.py +219 -0
  163. code_puppy/plugins/agent_skills/prompt_builder.py +60 -0
  164. code_puppy/plugins/agent_skills/register_callbacks.py +241 -0
  165. code_puppy/plugins/agent_skills/remote_catalog.py +322 -0
  166. code_puppy/plugins/agent_skills/skill_catalog.py +257 -0
  167. code_puppy/plugins/agent_skills/skills_install_menu.py +664 -0
  168. code_puppy/plugins/agent_skills/skills_menu.py +781 -0
  169. code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
  170. code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
  171. code_puppy/plugins/antigravity_oauth/antigravity_model.py +706 -0
  172. code_puppy/plugins/antigravity_oauth/config.py +42 -0
  173. code_puppy/plugins/antigravity_oauth/constants.py +133 -0
  174. code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
  175. code_puppy/plugins/antigravity_oauth/register_callbacks.py +518 -0
  176. code_puppy/plugins/antigravity_oauth/storage.py +288 -0
  177. code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
  178. code_puppy/plugins/antigravity_oauth/token.py +167 -0
  179. code_puppy/plugins/antigravity_oauth/transport.py +863 -0
  180. code_puppy/plugins/antigravity_oauth/utils.py +168 -0
  181. code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
  182. code_puppy/plugins/chatgpt_oauth/config.py +52 -0
  183. code_puppy/plugins/chatgpt_oauth/oauth_flow.py +329 -0
  184. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +176 -0
  185. code_puppy/plugins/chatgpt_oauth/test_plugin.py +301 -0
  186. code_puppy/plugins/chatgpt_oauth/utils.py +523 -0
  187. code_puppy/plugins/claude_code_hooks/__init__.py +1 -0
  188. code_puppy/plugins/claude_code_hooks/config.py +137 -0
  189. code_puppy/plugins/claude_code_hooks/register_callbacks.py +175 -0
  190. code_puppy/plugins/claude_code_oauth/README.md +167 -0
  191. code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
  192. code_puppy/plugins/claude_code_oauth/__init__.py +25 -0
  193. code_puppy/plugins/claude_code_oauth/config.py +52 -0
  194. code_puppy/plugins/claude_code_oauth/register_callbacks.py +453 -0
  195. code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
  196. code_puppy/plugins/claude_code_oauth/token_refresh_heartbeat.py +241 -0
  197. code_puppy/plugins/claude_code_oauth/utils.py +640 -0
  198. code_puppy/plugins/customizable_commands/__init__.py +0 -0
  199. code_puppy/plugins/customizable_commands/register_callbacks.py +152 -0
  200. code_puppy/plugins/example_custom_command/README.md +280 -0
  201. code_puppy/plugins/example_custom_command/register_callbacks.py +51 -0
  202. code_puppy/plugins/file_permission_handler/__init__.py +4 -0
  203. code_puppy/plugins/file_permission_handler/register_callbacks.py +470 -0
  204. code_puppy/plugins/frontend_emitter/__init__.py +25 -0
  205. code_puppy/plugins/frontend_emitter/emitter.py +121 -0
  206. code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
  207. code_puppy/plugins/hook_creator/__init__.py +1 -0
  208. code_puppy/plugins/hook_creator/register_callbacks.py +33 -0
  209. code_puppy/plugins/hook_manager/__init__.py +1 -0
  210. code_puppy/plugins/hook_manager/config.py +290 -0
  211. code_puppy/plugins/hook_manager/hooks_menu.py +564 -0
  212. code_puppy/plugins/hook_manager/register_callbacks.py +227 -0
  213. code_puppy/plugins/oauth_puppy_html.py +228 -0
  214. code_puppy/plugins/scheduler/__init__.py +1 -0
  215. code_puppy/plugins/scheduler/register_callbacks.py +88 -0
  216. code_puppy/plugins/scheduler/scheduler_menu.py +522 -0
  217. code_puppy/plugins/scheduler/scheduler_wizard.py +341 -0
  218. code_puppy/plugins/shell_safety/__init__.py +6 -0
  219. code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
  220. code_puppy/plugins/shell_safety/command_cache.py +156 -0
  221. code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
  222. code_puppy/plugins/synthetic_status/__init__.py +1 -0
  223. code_puppy/plugins/synthetic_status/register_callbacks.py +132 -0
  224. code_puppy/plugins/synthetic_status/status_api.py +147 -0
  225. code_puppy/plugins/universal_constructor/__init__.py +13 -0
  226. code_puppy/plugins/universal_constructor/models.py +138 -0
  227. code_puppy/plugins/universal_constructor/register_callbacks.py +47 -0
  228. code_puppy/plugins/universal_constructor/registry.py +302 -0
  229. code_puppy/plugins/universal_constructor/sandbox.py +584 -0
  230. code_puppy/prompts/antigravity_system_prompt.md +1 -0
  231. code_puppy/pydantic_patches.py +356 -0
  232. code_puppy/reopenable_async_client.py +232 -0
  233. code_puppy/round_robin_model.py +150 -0
  234. code_puppy/scheduler/__init__.py +41 -0
  235. code_puppy/scheduler/__main__.py +9 -0
  236. code_puppy/scheduler/cli.py +118 -0
  237. code_puppy/scheduler/config.py +126 -0
  238. code_puppy/scheduler/daemon.py +280 -0
  239. code_puppy/scheduler/executor.py +155 -0
  240. code_puppy/scheduler/platform.py +19 -0
  241. code_puppy/scheduler/platform_unix.py +22 -0
  242. code_puppy/scheduler/platform_win.py +32 -0
  243. code_puppy/session_storage.py +338 -0
  244. code_puppy/status_display.py +257 -0
  245. code_puppy/summarization_agent.py +176 -0
  246. code_puppy/terminal_utils.py +418 -0
  247. code_puppy/tools/__init__.py +501 -0
  248. code_puppy/tools/agent_tools.py +603 -0
  249. code_puppy/tools/ask_user_question/__init__.py +26 -0
  250. code_puppy/tools/ask_user_question/constants.py +73 -0
  251. code_puppy/tools/ask_user_question/demo_tui.py +55 -0
  252. code_puppy/tools/ask_user_question/handler.py +232 -0
  253. code_puppy/tools/ask_user_question/models.py +304 -0
  254. code_puppy/tools/ask_user_question/registration.py +26 -0
  255. code_puppy/tools/ask_user_question/renderers.py +309 -0
  256. code_puppy/tools/ask_user_question/terminal_ui.py +329 -0
  257. code_puppy/tools/ask_user_question/theme.py +155 -0
  258. code_puppy/tools/ask_user_question/tui_loop.py +423 -0
  259. code_puppy/tools/browser/__init__.py +37 -0
  260. code_puppy/tools/browser/browser_control.py +289 -0
  261. code_puppy/tools/browser/browser_interactions.py +545 -0
  262. code_puppy/tools/browser/browser_locators.py +640 -0
  263. code_puppy/tools/browser/browser_manager.py +378 -0
  264. code_puppy/tools/browser/browser_navigation.py +251 -0
  265. code_puppy/tools/browser/browser_screenshot.py +179 -0
  266. code_puppy/tools/browser/browser_scripts.py +462 -0
  267. code_puppy/tools/browser/browser_workflows.py +221 -0
  268. code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
  269. code_puppy/tools/browser/terminal_command_tools.py +534 -0
  270. code_puppy/tools/browser/terminal_screenshot_tools.py +552 -0
  271. code_puppy/tools/browser/terminal_tools.py +525 -0
  272. code_puppy/tools/command_runner.py +1346 -0
  273. code_puppy/tools/common.py +1409 -0
  274. code_puppy/tools/display.py +84 -0
  275. code_puppy/tools/file_modifications.py +886 -0
  276. code_puppy/tools/file_operations.py +802 -0
  277. code_puppy/tools/scheduler_tools.py +412 -0
  278. code_puppy/tools/skills_tools.py +244 -0
  279. code_puppy/tools/subagent_context.py +158 -0
  280. code_puppy/tools/tools_content.py +51 -0
  281. code_puppy/tools/universal_constructor.py +889 -0
  282. code_puppy/uvx_detection.py +242 -0
  283. code_puppy/version_checker.py +82 -0
  284. codepp-0.0.437.dist-info/METADATA +766 -0
  285. codepp-0.0.437.dist-info/RECORD +288 -0
  286. codepp-0.0.437.dist-info/WHEEL +4 -0
  287. codepp-0.0.437.dist-info/entry_points.txt +3 -0
  288. codepp-0.0.437.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,932 @@
1
+ """Interactive TUI for configuring per-model settings.
2
+
3
+ Provides a beautiful interface for viewing and modifying model-specific
4
+ settings like temperature and seed on a per-model basis.
5
+ """
6
+
7
+ import sys
8
+ import time
9
+ from typing import Dict, List, Optional
10
+
11
+ from prompt_toolkit import Application
12
+ from prompt_toolkit.key_binding import KeyBindings
13
+ from prompt_toolkit.layout import Dimension, Layout, VSplit, Window
14
+ from prompt_toolkit.layout.controls import FormattedTextControl
15
+ from prompt_toolkit.widgets import Frame
16
+
17
+ from code_puppy.config import (
18
+ get_all_model_settings,
19
+ get_global_model_name,
20
+ get_openai_reasoning_effort,
21
+ get_openai_verbosity,
22
+ model_supports_setting,
23
+ set_model_setting,
24
+ set_openai_reasoning_effort,
25
+ set_openai_verbosity,
26
+ )
27
+ from code_puppy.messaging import emit_info
28
+ from code_puppy.model_factory import ModelFactory
29
+ from code_puppy.tools.command_runner import set_awaiting_user_input
30
+
31
+ # Pagination config
32
+ MODELS_PER_PAGE = 15
33
+
34
+ # Setting definitions with metadata
35
+ # Numeric settings have min/max/step, choice settings have choices list
36
+ SETTING_DEFINITIONS: Dict[str, Dict] = {
37
+ "temperature": {
38
+ "name": "Temperature",
39
+ "description": "Controls randomness (0.0-1.0). Lower = more deterministic, higher = more creative.",
40
+ "type": "numeric",
41
+ "min": 0.0,
42
+ "max": 1.0,
43
+ "step": 0.05,
44
+ "default": None, # None means use model default
45
+ "format": "{:.2f}",
46
+ },
47
+ "seed": {
48
+ "name": "Seed",
49
+ "description": "Random seed for reproducible outputs. Set to same value for consistent results.",
50
+ "type": "numeric",
51
+ "min": 0,
52
+ "max": 999999,
53
+ "step": 1,
54
+ "default": None,
55
+ "format": "{:.0f}",
56
+ },
57
+ "top_p": {
58
+ "name": "Top-P (Nucleus Sampling)",
59
+ "description": "Controls token diversity. 0.0 = least random (only most likely tokens), 1.0 = most random (sample from all tokens).",
60
+ "type": "numeric",
61
+ "min": 0.0,
62
+ "max": 1.0,
63
+ "step": 0.05,
64
+ "default": None,
65
+ "format": "{:.2f}",
66
+ },
67
+ "reasoning_effort": {
68
+ "name": "Reasoning Effort",
69
+ "description": "Controls how much effort GPT-5 models spend on reasoning. Higher = more thorough but slower.",
70
+ "type": "choice",
71
+ "choices": ["minimal", "low", "medium", "high", "xhigh"],
72
+ "default": "medium",
73
+ },
74
+ "verbosity": {
75
+ "name": "Verbosity",
76
+ "description": "Controls response length. Low = concise, Medium = balanced, High = verbose.",
77
+ "type": "choice",
78
+ "choices": ["low", "medium", "high", "max"],
79
+ "default": "medium",
80
+ },
81
+ "extended_thinking": {
82
+ "name": "Extended Thinking",
83
+ "description": "Controls extended thinking mode. 'enabled' = classic thinking with budget_tokens, 'adaptive' = model decides when/how much to think (no budget), 'off' = disabled.",
84
+ "type": "choice",
85
+ "choices": ["enabled", "adaptive", "off"],
86
+ "default": "enabled",
87
+ },
88
+ "budget_tokens": {
89
+ "name": "Thinking Budget (tokens)",
90
+ "description": "Max tokens for extended thinking. Only used when extended_thinking is 'enabled'.",
91
+ "type": "numeric",
92
+ "min": 1024,
93
+ "max": 131072,
94
+ "step": 1024,
95
+ "default": 10000,
96
+ "format": "{:.0f}",
97
+ },
98
+ "interleaved_thinking": {
99
+ "name": "Interleaved Thinking",
100
+ "description": "Enable thinking between tool calls (Claude 4 only: Opus 4.5, Opus 4.1, Opus 4, Sonnet 4). Adds beta header. WARNING: On Vertex/Bedrock, this FAILS for non-Claude 4 models!",
101
+ "type": "boolean",
102
+ "default": False,
103
+ },
104
+ "clear_thinking": {
105
+ "name": "Clear Thinking",
106
+ "description": "False = Preserved Thinking (keep <think> blocks visible). True = strip thinking from responses.",
107
+ "type": "boolean",
108
+ "default": False,
109
+ },
110
+ "thinking_enabled": {
111
+ "name": "Thinking Enabled",
112
+ "description": "Enable thinking mode for Gemini 3 Pro models. When enabled, the model will show its reasoning process.",
113
+ "type": "boolean",
114
+ "default": True,
115
+ },
116
+ "thinking_level": {
117
+ "name": "Thinking Level",
118
+ "description": "Controls the depth of thinking for Gemini 3 Pro models. Low = faster responses, High = more thorough reasoning.",
119
+ "type": "choice",
120
+ "choices": ["low", "high"],
121
+ "default": "low",
122
+ },
123
+ "effort": {
124
+ "name": "Effort",
125
+ "description": "Controls how much effort the model spends on its response (Opus 4-6 only). Low = fast, Max = most thorough.",
126
+ "type": "choice",
127
+ "choices": ["low", "medium", "high", "max"],
128
+ "default": "high",
129
+ },
130
+ }
131
+
132
+
133
+ def _load_all_model_names() -> List[str]:
134
+ """Load all available model names from config."""
135
+ models_config = ModelFactory.load_config()
136
+ return list(models_config.keys())
137
+
138
+
139
+ def _get_setting_choices(
140
+ setting_key: str, model_name: Optional[str] = None
141
+ ) -> List[str]:
142
+ """Get the available choices for a setting, filtered by model capabilities.
143
+
144
+ For reasoning_effort, only codex models support 'xhigh' - regular GPT-5.2
145
+ models are capped at 'high'.
146
+
147
+ Args:
148
+ setting_key: The setting name (e.g., 'reasoning_effort', 'verbosity')
149
+ model_name: Optional model name to filter choices for
150
+
151
+ Returns:
152
+ List of valid choices for this setting and model combination.
153
+ """
154
+ setting_def = SETTING_DEFINITIONS.get(setting_key, {})
155
+ if setting_def.get("type") != "choice":
156
+ return []
157
+
158
+ base_choices = setting_def.get("choices", [])
159
+
160
+ # For reasoning_effort, filter 'xhigh' based on model support
161
+ if setting_key == "reasoning_effort" and model_name:
162
+ models_config = ModelFactory.load_config()
163
+ model_config = models_config.get(model_name, {})
164
+
165
+ # Check if model supports xhigh reasoning
166
+ supports_xhigh = model_config.get("supports_xhigh_reasoning", False)
167
+
168
+ if not supports_xhigh:
169
+ # Remove xhigh from choices for non-codex models
170
+ return [c for c in base_choices if c != "xhigh"]
171
+
172
+ return base_choices
173
+
174
+
175
+ class ModelSettingsMenu:
176
+ """Interactive TUI for model settings configuration.
177
+
178
+ Two-level navigation:
179
+ - Level 1: List of all available models (paginated)
180
+ - Level 2: Settings for the selected model
181
+ """
182
+
183
+ def __init__(self):
184
+ """Initialize the settings menu."""
185
+ self.all_models = _load_all_model_names()
186
+ self.current_model_name = get_global_model_name()
187
+
188
+ # Navigation state
189
+ self.view_mode = "models" # "models" or "settings"
190
+ self.model_index = 0 # Index in model list (absolute)
191
+ self.setting_index = 0 # Index in settings list
192
+
193
+ # Pagination state
194
+ self.page = 0
195
+ self.page_size = MODELS_PER_PAGE
196
+
197
+ # Try to pre-select the current model and set correct page
198
+ if self.current_model_name in self.all_models:
199
+ self.model_index = self.all_models.index(self.current_model_name)
200
+ self.page = self.model_index // self.page_size
201
+
202
+ # Editing state
203
+ self.editing_mode = False
204
+ self.edit_value: Optional[float] = None
205
+ self.result_changed = False
206
+
207
+ # Cache for selected model's settings
208
+ self.selected_model: Optional[str] = None
209
+ self.supported_settings: List[str] = []
210
+ self.current_settings: Dict = {}
211
+
212
+ @property
213
+ def total_pages(self) -> int:
214
+ """Calculate total number of pages."""
215
+ if not self.all_models:
216
+ return 1
217
+ return (len(self.all_models) + self.page_size - 1) // self.page_size
218
+
219
+ @property
220
+ def page_start(self) -> int:
221
+ """Get the starting index for the current page."""
222
+ return self.page * self.page_size
223
+
224
+ @property
225
+ def page_end(self) -> int:
226
+ """Get the ending index (exclusive) for the current page."""
227
+ return min(self.page_start + self.page_size, len(self.all_models))
228
+
229
+ @property
230
+ def models_on_page(self) -> List[str]:
231
+ """Get the models visible on the current page."""
232
+ return self.all_models[self.page_start : self.page_end]
233
+
234
+ def _ensure_selection_visible(self):
235
+ """Ensure the current selection is on the visible page."""
236
+ if self.model_index < self.page_start:
237
+ self.page = self.model_index // self.page_size
238
+ elif self.model_index >= self.page_end:
239
+ self.page = self.model_index // self.page_size
240
+
241
+ def _get_supported_settings(self, model_name: str) -> List[str]:
242
+ """Get list of settings supported by a model."""
243
+ supported = []
244
+ for setting_key in SETTING_DEFINITIONS:
245
+ if model_supports_setting(model_name, setting_key):
246
+ supported.append(setting_key)
247
+ return supported
248
+
249
+ def _load_model_settings(self, model_name: str):
250
+ """Load settings for a specific model."""
251
+ self.selected_model = model_name
252
+ self.supported_settings = self._get_supported_settings(model_name)
253
+ self.current_settings = get_all_model_settings(model_name)
254
+
255
+ # Add global OpenAI settings if model supports them
256
+ if model_supports_setting(model_name, "reasoning_effort"):
257
+ self.current_settings["reasoning_effort"] = get_openai_reasoning_effort()
258
+ if model_supports_setting(model_name, "verbosity"):
259
+ self.current_settings["verbosity"] = get_openai_verbosity()
260
+
261
+ self.setting_index = 0
262
+
263
+ def _get_current_value(self, setting: str):
264
+ """Get the current value for a setting."""
265
+ return self.current_settings.get(setting)
266
+
267
+ def _format_value(self, setting: str, value) -> str:
268
+ """Format a setting value for display."""
269
+ setting_def = SETTING_DEFINITIONS.get(setting)
270
+ if setting_def is None:
271
+ # Unknown/stale setting from saved config — just stringify it
272
+ return str(value) if value is not None else "(unknown)"
273
+
274
+ if value is None:
275
+ default = setting_def.get("default")
276
+ if default is not None:
277
+ return f"(default: {default})"
278
+ return "(model default)"
279
+
280
+ if setting_def.get("type") == "choice":
281
+ return str(value)
282
+
283
+ if setting_def.get("type") == "boolean":
284
+ return "Enabled" if value else "Disabled"
285
+
286
+ fmt = setting_def.get("format", "{:.2f}")
287
+ return fmt.format(value)
288
+
289
+ def _render_main_list(self) -> List:
290
+ """Render the main list panel (models or settings)."""
291
+ lines = []
292
+
293
+ if self.view_mode == "models":
294
+ # Header with page indicator
295
+ lines.append(("bold cyan", " 🐕 Select a Model to Configure"))
296
+ if self.total_pages > 1:
297
+ lines.append(
298
+ (
299
+ "fg:ansibrightblack",
300
+ f" (Page {self.page + 1}/{self.total_pages})",
301
+ )
302
+ )
303
+ lines.append(("", "\n\n"))
304
+
305
+ if not self.all_models:
306
+ lines.append(("fg:ansiyellow", " No models available."))
307
+ lines.append(("", "\n\n"))
308
+ self._add_model_nav_hints(lines)
309
+ return lines
310
+
311
+ # Only render models on the current page
312
+ for i, model_name in enumerate(self.models_on_page):
313
+ absolute_index = self.page_start + i
314
+ is_selected = absolute_index == self.model_index
315
+ is_current = model_name == self.current_model_name
316
+
317
+ prefix = " › " if is_selected else " "
318
+ style = "fg:ansiwhite bold" if is_selected else "fg:ansibrightblack"
319
+
320
+ # Check if model has any custom settings
321
+ model_settings = get_all_model_settings(model_name)
322
+ has_settings = len(model_settings) > 0
323
+
324
+ lines.append((style, f"{prefix}{model_name}"))
325
+
326
+ # Show indicators
327
+ if is_current:
328
+ lines.append(("fg:ansigreen", " (active)"))
329
+ if has_settings:
330
+ lines.append(("fg:ansicyan", " ⚙"))
331
+
332
+ lines.append(("", "\n"))
333
+
334
+ lines.append(("", "\n"))
335
+ self._add_model_nav_hints(lines)
336
+ else:
337
+ # Settings view
338
+ lines.append(("bold cyan", f" ⚙ Settings for {self.selected_model}"))
339
+ lines.append(("", "\n\n"))
340
+
341
+ if not self.supported_settings:
342
+ lines.append(
343
+ ("fg:ansiyellow", " No configurable settings for this model.")
344
+ )
345
+ lines.append(("", "\n\n"))
346
+ self._add_settings_nav_hints(lines)
347
+ return lines
348
+
349
+ for i, setting_key in enumerate(self.supported_settings):
350
+ setting_def = SETTING_DEFINITIONS[setting_key]
351
+ is_selected = i == self.setting_index
352
+ current_value = self._get_current_value(setting_key)
353
+
354
+ # Show editing state if in edit mode for this setting
355
+ if is_selected and self.editing_mode:
356
+ display_value = self._format_value(setting_key, self.edit_value)
357
+ prefix = " ✏️ "
358
+ style = "fg:ansigreen bold"
359
+ else:
360
+ display_value = self._format_value(setting_key, current_value)
361
+ prefix = " › " if is_selected else " "
362
+ style = "fg:ansiwhite" if is_selected else "fg:ansibrightblack"
363
+
364
+ # Setting name and value
365
+ lines.append((style, f"{prefix}{setting_def['name']}: "))
366
+ if current_value is not None or (is_selected and self.editing_mode):
367
+ lines.append(("fg:ansicyan", display_value))
368
+ else:
369
+ lines.append(("fg:ansibrightblack dim", display_value))
370
+ lines.append(("", "\n"))
371
+
372
+ lines.append(("", "\n"))
373
+ self._add_settings_nav_hints(lines)
374
+
375
+ return lines
376
+
377
+ def _add_model_nav_hints(self, lines: List):
378
+ """Add navigation hints for model list view."""
379
+ lines.append(("", "\n"))
380
+ lines.append(("fg:ansibrightblack", " ↑/↓ "))
381
+ lines.append(("", "Navigate models\n"))
382
+ if self.total_pages > 1:
383
+ lines.append(("fg:ansibrightblack", " PgUp/PgDn "))
384
+ lines.append(("", "Change page\n"))
385
+ lines.append(("fg:ansigreen", " Enter "))
386
+ lines.append(("", "Configure model\n"))
387
+ lines.append(("fg:ansiyellow", " Esc "))
388
+ lines.append(("", "Exit\n"))
389
+
390
+ def _add_settings_nav_hints(self, lines: List):
391
+ """Add navigation hints for settings view."""
392
+ lines.append(("", "\n"))
393
+
394
+ if self.editing_mode:
395
+ lines.append(("fg:ansibrightblack", " ←/→ "))
396
+ lines.append(("", "Adjust value\n"))
397
+ lines.append(("fg:ansigreen", " Enter "))
398
+ lines.append(("", "Save\n"))
399
+ lines.append(("fg:ansiyellow", " Esc "))
400
+ lines.append(("", "Cancel edit\n"))
401
+ lines.append(("fg:ansired", " d "))
402
+ lines.append(("", "Reset to default\n"))
403
+ else:
404
+ lines.append(("fg:ansibrightblack", " ↑/↓ "))
405
+ lines.append(("", "Navigate settings\n"))
406
+ lines.append(("fg:ansigreen", " Enter "))
407
+ lines.append(("", "Edit setting\n"))
408
+ lines.append(("fg:ansired", " d "))
409
+ lines.append(("", "Reset to default\n"))
410
+ lines.append(("fg:ansiyellow", " Esc "))
411
+ lines.append(("", "Back to models\n"))
412
+
413
+ def _render_details_panel(self) -> List:
414
+ """Render the details/help panel."""
415
+ lines = []
416
+
417
+ if self.view_mode == "models":
418
+ lines.append(("bold cyan", " Model Info"))
419
+ lines.append(("", "\n\n"))
420
+
421
+ if not self.all_models:
422
+ lines.append(("fg:ansibrightblack", " No models available."))
423
+ return lines
424
+
425
+ model_name = self.all_models[self.model_index]
426
+ is_current = model_name == self.current_model_name
427
+
428
+ lines.append(("bold", f" {model_name}"))
429
+ lines.append(("", "\n\n"))
430
+
431
+ if is_current:
432
+ lines.append(("fg:ansigreen", " ✓ Currently active model"))
433
+ lines.append(("", "\n\n"))
434
+
435
+ # Show current settings for this model
436
+ model_settings = get_all_model_settings(model_name)
437
+ if model_settings:
438
+ lines.append(("bold", " Custom Settings:"))
439
+ lines.append(("", "\n"))
440
+ for setting_key, value in model_settings.items():
441
+ setting_def = SETTING_DEFINITIONS.get(setting_key, {})
442
+ name = setting_def.get("name", setting_key)
443
+ display = self._format_value(setting_key, value)
444
+ lines.append(("fg:ansicyan", f" {name}: {display}"))
445
+ lines.append(("", "\n"))
446
+ else:
447
+ lines.append(("fg:ansibrightblack", " Using all default settings"))
448
+ lines.append(("", "\n"))
449
+
450
+ # Show supported settings
451
+ supported = self._get_supported_settings(model_name)
452
+ lines.append(("", "\n"))
453
+ lines.append(("bold", " Configurable Settings:"))
454
+ lines.append(("", "\n"))
455
+ if supported:
456
+ for s in supported:
457
+ setting_def = SETTING_DEFINITIONS.get(s, {})
458
+ name = setting_def.get("name", s)
459
+ lines.append(("fg:ansibrightblack", f" • {name}"))
460
+ lines.append(("", "\n"))
461
+ else:
462
+ lines.append(("fg:ansibrightblack dim", " None"))
463
+ lines.append(("", "\n"))
464
+
465
+ # Show pagination info at the bottom of details
466
+ if self.total_pages > 1:
467
+ lines.append(("", "\n"))
468
+ lines.append(
469
+ (
470
+ "fg:ansibrightblack dim",
471
+ f" Model {self.model_index + 1} of {len(self.all_models)}",
472
+ )
473
+ )
474
+ lines.append(("", "\n"))
475
+
476
+ else:
477
+ # Settings detail view
478
+ lines.append(("bold cyan", " Setting Details"))
479
+ lines.append(("", "\n\n"))
480
+
481
+ if not self.supported_settings:
482
+ lines.append(
483
+ ("fg:ansibrightblack", " This model doesn't expose any settings.")
484
+ )
485
+ return lines
486
+
487
+ setting_key = self.supported_settings[self.setting_index]
488
+ setting_def = SETTING_DEFINITIONS[setting_key]
489
+ current_value = self._get_current_value(setting_key)
490
+
491
+ # Setting name
492
+ lines.append(("bold", f" {setting_def['name']}"))
493
+ lines.append(("", "\n"))
494
+
495
+ # Show if this is a global setting
496
+ if setting_key in ("reasoning_effort", "verbosity"):
497
+ lines.append(
498
+ (
499
+ "fg:ansiyellow",
500
+ " ⚠ Global setting (applies to all GPT-5 models)",
501
+ )
502
+ )
503
+ lines.append(("", "\n\n"))
504
+
505
+ # Description
506
+ lines.append(("fg:ansibrightblack", f" {setting_def['description']}"))
507
+ lines.append(("", "\n\n"))
508
+
509
+ # Range/choices info
510
+ if setting_def.get("type") == "choice":
511
+ lines.append(("bold", " Options:"))
512
+ lines.append(("", "\n"))
513
+ # Get filtered choices based on model capabilities
514
+ choices = _get_setting_choices(setting_key, self.selected_model)
515
+ lines.append(
516
+ (
517
+ "fg:ansibrightblack",
518
+ f" {' | '.join(choices)}",
519
+ )
520
+ )
521
+ elif setting_def.get("type") == "boolean":
522
+ lines.append(("bold", " Options:"))
523
+ lines.append(("", "\n"))
524
+ lines.append(
525
+ (
526
+ "fg:ansibrightblack",
527
+ " Enabled | Disabled",
528
+ )
529
+ )
530
+ else:
531
+ lines.append(("bold", " Range:"))
532
+ lines.append(("", "\n"))
533
+ lines.append(
534
+ (
535
+ "fg:ansibrightblack",
536
+ f" Min: {setting_def['min']} Max: {setting_def['max']} Step: {setting_def['step']}",
537
+ )
538
+ )
539
+ lines.append(("", "\n\n"))
540
+
541
+ # Current value
542
+ lines.append(("bold", " Current Value:"))
543
+ lines.append(("", "\n"))
544
+ if current_value is not None:
545
+ lines.append(
546
+ (
547
+ "fg:ansicyan",
548
+ f" {self._format_value(setting_key, current_value)}",
549
+ )
550
+ )
551
+ else:
552
+ lines.append(("fg:ansibrightblack dim", " (using model default)"))
553
+ lines.append(("", "\n\n"))
554
+
555
+ # Editing hint
556
+ if self.editing_mode:
557
+ lines.append(("fg:ansigreen bold", " ✏️ EDITING MODE"))
558
+ lines.append(("", "\n"))
559
+ if self.edit_value is not None:
560
+ lines.append(
561
+ (
562
+ "fg:ansicyan",
563
+ f" New value: {self._format_value(setting_key, self.edit_value)}",
564
+ )
565
+ )
566
+ else:
567
+ lines.append(
568
+ ("fg:ansibrightblack", " New value: (model default)")
569
+ )
570
+ lines.append(("", "\n"))
571
+
572
+ return lines
573
+
574
+ def _enter_settings_view(self):
575
+ """Enter settings view for the selected model."""
576
+ if not self.all_models:
577
+ return
578
+ model_name = self.all_models[self.model_index]
579
+ self._load_model_settings(model_name)
580
+ self.view_mode = "settings"
581
+
582
+ def _back_to_models(self):
583
+ """Go back to model list view."""
584
+ self.view_mode = "models"
585
+ self.editing_mode = False
586
+ self.edit_value = None
587
+
588
+ def _start_editing(self):
589
+ """Enter editing mode for the selected setting."""
590
+ if not self.supported_settings:
591
+ return
592
+
593
+ setting_key = self.supported_settings[self.setting_index]
594
+ setting_def = SETTING_DEFINITIONS[setting_key]
595
+ current = self._get_current_value(setting_key)
596
+
597
+ # Start with current value, or default if not set
598
+ if current is not None:
599
+ self.edit_value = current
600
+ elif setting_def.get("type") == "choice":
601
+ # For choice settings, start with the default (using filtered choices)
602
+ choices = _get_setting_choices(setting_key, self.selected_model)
603
+ self.edit_value = setting_def.get(
604
+ "default", choices[0] if choices else None
605
+ )
606
+ elif setting_def.get("type") == "boolean":
607
+ # For boolean settings, start with the default
608
+ self.edit_value = setting_def.get("default", False)
609
+ else:
610
+ # Default to a sensible starting point for numeric
611
+ if setting_key == "temperature":
612
+ self.edit_value = 0.7
613
+ elif setting_key == "top_p":
614
+ self.edit_value = 0.9 # Common default for top_p
615
+ elif setting_key == "seed":
616
+ self.edit_value = 42
617
+ elif setting_key == "budget_tokens":
618
+ self.edit_value = 10000
619
+ else:
620
+ self.edit_value = (setting_def["min"] + setting_def["max"]) / 2
621
+
622
+ self.editing_mode = True
623
+
624
+ def _adjust_value(self, direction: int):
625
+ """Adjust the current edit value."""
626
+ if not self.editing_mode or self.edit_value is None:
627
+ return
628
+
629
+ setting_key = self.supported_settings[self.setting_index]
630
+ setting_def = SETTING_DEFINITIONS[setting_key]
631
+
632
+ if setting_def.get("type") == "choice":
633
+ # Cycle through filtered choices based on model capabilities
634
+ choices = _get_setting_choices(setting_key, self.selected_model)
635
+ current_idx = (
636
+ choices.index(self.edit_value) if self.edit_value in choices else 0
637
+ )
638
+ new_idx = (current_idx + direction) % len(choices)
639
+ self.edit_value = choices[new_idx]
640
+ elif setting_def.get("type") == "boolean":
641
+ # Toggle boolean
642
+ self.edit_value = not self.edit_value
643
+ else:
644
+ # Numeric adjustment
645
+ step = setting_def["step"]
646
+ new_value = self.edit_value + (direction * step)
647
+ # Clamp to range
648
+ new_value = max(setting_def["min"], min(setting_def["max"], new_value))
649
+ self.edit_value = new_value
650
+
651
+ def _save_edit(self):
652
+ """Save the current edit value."""
653
+ if not self.editing_mode or self.selected_model is None:
654
+ return
655
+
656
+ setting_key = self.supported_settings[self.setting_index]
657
+
658
+ # Handle global OpenAI settings specially
659
+ if setting_key == "reasoning_effort":
660
+ if self.edit_value is not None:
661
+ set_openai_reasoning_effort(self.edit_value)
662
+ elif setting_key == "verbosity":
663
+ if self.edit_value is not None:
664
+ set_openai_verbosity(self.edit_value)
665
+ else:
666
+ # Standard per-model setting
667
+ set_model_setting(self.selected_model, setting_key, self.edit_value)
668
+
669
+ # Update local cache
670
+ if self.edit_value is not None:
671
+ self.current_settings[setting_key] = self.edit_value
672
+ elif setting_key in self.current_settings:
673
+ del self.current_settings[setting_key]
674
+
675
+ self.result_changed = True
676
+ self.editing_mode = False
677
+ self.edit_value = None
678
+
679
+ def _cancel_edit(self):
680
+ """Cancel the current edit."""
681
+ self.editing_mode = False
682
+ self.edit_value = None
683
+
684
+ def _reset_to_default(self):
685
+ """Reset the current setting to model default."""
686
+ if not self.supported_settings or self.selected_model is None:
687
+ return
688
+
689
+ setting_key = self.supported_settings[self.setting_index]
690
+ setting_def = SETTING_DEFINITIONS.get(setting_key, {})
691
+
692
+ if self.editing_mode:
693
+ # Reset edit value to default
694
+ default = setting_def.get("default")
695
+ self.edit_value = default
696
+ else:
697
+ # Handle global OpenAI settings - reset to their defaults
698
+ if setting_key == "reasoning_effort":
699
+ set_openai_reasoning_effort("medium") # Default
700
+ self.current_settings[setting_key] = "medium"
701
+ elif setting_key == "verbosity":
702
+ set_openai_verbosity("medium") # Default
703
+ self.current_settings[setting_key] = "medium"
704
+ else:
705
+ # Standard per-model setting
706
+ set_model_setting(self.selected_model, setting_key, None)
707
+ if setting_key in self.current_settings:
708
+ del self.current_settings[setting_key]
709
+ self.result_changed = True
710
+
711
+ def _page_up(self):
712
+ """Go to previous page."""
713
+ if self.page > 0:
714
+ self.page -= 1
715
+ # Move selection to first item on new page
716
+ self.model_index = self.page_start
717
+
718
+ def _page_down(self):
719
+ """Go to next page."""
720
+ if self.page < self.total_pages - 1:
721
+ self.page += 1
722
+ # Move selection to first item on new page
723
+ self.model_index = self.page_start
724
+
725
+ def update_display(self):
726
+ """Update the display."""
727
+ self.menu_control.text = self._render_main_list()
728
+ self.details_control.text = self._render_details_panel()
729
+
730
+ def run(self) -> bool:
731
+ """Run the interactive settings menu.
732
+
733
+ Returns:
734
+ True if settings were changed, False otherwise.
735
+ """
736
+ # Build UI
737
+ self.menu_control = FormattedTextControl(text="")
738
+ self.details_control = FormattedTextControl(text="")
739
+
740
+ menu_window = Window(
741
+ content=self.menu_control, wrap_lines=True, width=Dimension(weight=40)
742
+ )
743
+ details_window = Window(
744
+ content=self.details_control, wrap_lines=True, width=Dimension(weight=60)
745
+ )
746
+
747
+ menu_frame = Frame(menu_window, width=Dimension(weight=40), title="Models")
748
+ details_frame = Frame(
749
+ details_window, width=Dimension(weight=60), title="Details"
750
+ )
751
+
752
+ root_container = VSplit([menu_frame, details_frame])
753
+
754
+ # Key bindings
755
+ kb = KeyBindings()
756
+
757
+ @kb.add("up")
758
+ @kb.add("c-p") # Ctrl+P = previous (Emacs-style)
759
+ def _(event):
760
+ if self.view_mode == "models":
761
+ if self.model_index > 0:
762
+ self.model_index -= 1
763
+ self._ensure_selection_visible()
764
+ self.update_display()
765
+ else:
766
+ if not self.editing_mode and self.setting_index > 0:
767
+ self.setting_index -= 1
768
+ self.update_display()
769
+
770
+ @kb.add("down")
771
+ @kb.add("c-n") # Ctrl+N = next (Emacs-style)
772
+ def _(event):
773
+ if self.view_mode == "models":
774
+ if self.model_index < len(self.all_models) - 1:
775
+ self.model_index += 1
776
+ self._ensure_selection_visible()
777
+ self.update_display()
778
+ else:
779
+ if (
780
+ not self.editing_mode
781
+ and self.setting_index < len(self.supported_settings) - 1
782
+ ):
783
+ self.setting_index += 1
784
+ self.update_display()
785
+
786
+ @kb.add("pageup")
787
+ def _(event):
788
+ if self.view_mode == "models":
789
+ self._page_up()
790
+ self.update_display()
791
+
792
+ @kb.add("pagedown")
793
+ def _(event):
794
+ if self.view_mode == "models":
795
+ self._page_down()
796
+ self.update_display()
797
+
798
+ @kb.add("left")
799
+ def _(event):
800
+ if self.view_mode == "settings" and self.editing_mode:
801
+ self._adjust_value(-1)
802
+ self.update_display()
803
+ elif self.view_mode == "models":
804
+ # Left arrow also goes to previous page
805
+ self._page_up()
806
+ self.update_display()
807
+
808
+ @kb.add("right")
809
+ def _(event):
810
+ if self.view_mode == "settings" and self.editing_mode:
811
+ self._adjust_value(1)
812
+ self.update_display()
813
+ elif self.view_mode == "models":
814
+ # Right arrow also goes to next page
815
+ self._page_down()
816
+ self.update_display()
817
+
818
+ @kb.add("enter")
819
+ def _(event):
820
+ if self.view_mode == "models":
821
+ self._enter_settings_view()
822
+ self.update_display()
823
+ else:
824
+ if self.editing_mode:
825
+ self._save_edit()
826
+ else:
827
+ self._start_editing()
828
+ self.update_display()
829
+
830
+ @kb.add("escape")
831
+ def _(event):
832
+ if self.view_mode == "settings":
833
+ if self.editing_mode:
834
+ self._cancel_edit()
835
+ self.update_display()
836
+ else:
837
+ self._back_to_models()
838
+ self.update_display()
839
+ else:
840
+ # At model list level, ESC closes the TUI
841
+ event.app.exit()
842
+
843
+ @kb.add("d")
844
+ def _(event):
845
+ if self.view_mode == "settings":
846
+ self._reset_to_default()
847
+ self.update_display()
848
+
849
+ @kb.add("c-c")
850
+ def _(event):
851
+ if self.editing_mode:
852
+ self._cancel_edit()
853
+ event.app.exit()
854
+
855
+ layout = Layout(root_container)
856
+ app = Application(
857
+ layout=layout,
858
+ key_bindings=kb,
859
+ full_screen=False,
860
+ mouse_support=False,
861
+ )
862
+
863
+ set_awaiting_user_input(True)
864
+
865
+ # Enter alternate screen buffer
866
+ sys.stdout.write("\033[?1049h")
867
+ sys.stdout.write("\033[2J\033[H")
868
+ sys.stdout.flush()
869
+ time.sleep(0.05)
870
+
871
+ try:
872
+ self.update_display()
873
+ sys.stdout.write("\033[2J\033[H")
874
+ sys.stdout.flush()
875
+
876
+ app.run(in_thread=True)
877
+
878
+ finally:
879
+ sys.stdout.write("\033[?1049l")
880
+ sys.stdout.flush()
881
+ set_awaiting_user_input(False)
882
+
883
+ # Clear exit message
884
+ from code_puppy.messaging import emit_info
885
+
886
+ emit_info("✓ Exited model settings")
887
+
888
+ return self.result_changed
889
+
890
+
891
+ def interactive_model_settings(model_name: Optional[str] = None) -> bool:
892
+ """Show interactive TUI to configure model settings.
893
+
894
+ Args:
895
+ model_name: Deprecated - the TUI now shows all models.
896
+ This parameter is ignored.
897
+
898
+ Returns:
899
+ True if settings were changed, False otherwise.
900
+ """
901
+ menu = ModelSettingsMenu()
902
+ return menu.run()
903
+
904
+
905
+ def show_model_settings_summary(model_name: Optional[str] = None) -> None:
906
+ """Print a summary of current model settings to the console.
907
+
908
+ Args:
909
+ model_name: Model to show settings for. If None, uses current global model.
910
+ """
911
+ model = model_name or get_global_model_name()
912
+ settings = get_all_model_settings(model)
913
+
914
+ if not settings:
915
+ emit_info(f"No custom settings configured for {model} (using model defaults)")
916
+ return
917
+
918
+ emit_info(f"Settings for {model}:")
919
+ for setting_key, value in settings.items():
920
+ setting_def = SETTING_DEFINITIONS.get(setting_key, {})
921
+ name = setting_def.get("name", setting_key)
922
+ setting_type = setting_def.get("type")
923
+ if setting_type in ("choice", "boolean"):
924
+ display = (
925
+ str(value)
926
+ if setting_type == "choice"
927
+ else ("Enabled" if value else "Disabled")
928
+ )
929
+ else:
930
+ fmt = setting_def.get("format", "{:.2f}")
931
+ display = fmt.format(value)
932
+ emit_info(f" {name}: {display}")