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