tunacode-cli 0.0.70__py3-none-any.whl → 0.0.78.6__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.
Potentially problematic release.
This version of tunacode-cli might be problematic. Click here for more details.
- tunacode/cli/commands/__init__.py +0 -2
- tunacode/cli/commands/implementations/__init__.py +0 -3
- tunacode/cli/commands/implementations/debug.py +2 -2
- tunacode/cli/commands/implementations/development.py +10 -8
- tunacode/cli/commands/implementations/model.py +357 -29
- tunacode/cli/commands/implementations/system.py +3 -2
- tunacode/cli/commands/implementations/template.py +0 -2
- tunacode/cli/commands/registry.py +8 -7
- tunacode/cli/commands/slash/loader.py +2 -1
- tunacode/cli/commands/slash/validator.py +2 -1
- tunacode/cli/main.py +19 -1
- tunacode/cli/repl.py +90 -229
- tunacode/cli/repl_components/command_parser.py +2 -1
- tunacode/cli/repl_components/error_recovery.py +8 -5
- tunacode/cli/repl_components/output_display.py +1 -10
- tunacode/cli/repl_components/tool_executor.py +1 -13
- tunacode/configuration/defaults.py +2 -2
- tunacode/configuration/key_descriptions.py +284 -0
- tunacode/configuration/settings.py +0 -1
- tunacode/constants.py +6 -42
- tunacode/core/agents/__init__.py +43 -2
- tunacode/core/agents/agent_components/__init__.py +7 -0
- tunacode/core/agents/agent_components/agent_config.py +162 -158
- tunacode/core/agents/agent_components/agent_helpers.py +31 -2
- tunacode/core/agents/agent_components/node_processor.py +180 -146
- tunacode/core/agents/agent_components/response_state.py +123 -6
- tunacode/core/agents/agent_components/state_transition.py +116 -0
- tunacode/core/agents/agent_components/streaming.py +296 -0
- tunacode/core/agents/agent_components/task_completion.py +19 -6
- tunacode/core/agents/agent_components/tool_buffer.py +21 -1
- tunacode/core/agents/agent_components/tool_executor.py +10 -0
- tunacode/core/agents/main.py +522 -370
- tunacode/core/agents/main_legact.py +538 -0
- tunacode/core/agents/prompts.py +66 -0
- tunacode/core/agents/utils.py +29 -122
- tunacode/core/setup/__init__.py +0 -2
- tunacode/core/setup/config_setup.py +88 -227
- tunacode/core/setup/config_wizard.py +230 -0
- tunacode/core/setup/coordinator.py +2 -1
- tunacode/core/state.py +16 -64
- tunacode/core/token_usage/usage_tracker.py +3 -1
- tunacode/core/tool_authorization.py +352 -0
- tunacode/core/tool_handler.py +67 -60
- tunacode/prompts/system.xml +751 -0
- tunacode/services/mcp.py +97 -1
- tunacode/setup.py +0 -23
- tunacode/tools/base.py +54 -1
- tunacode/tools/bash.py +14 -0
- tunacode/tools/glob.py +4 -2
- tunacode/tools/grep.py +7 -17
- tunacode/tools/prompts/glob_prompt.xml +1 -1
- tunacode/tools/prompts/grep_prompt.xml +1 -0
- tunacode/tools/prompts/list_dir_prompt.xml +1 -1
- tunacode/tools/prompts/react_prompt.xml +23 -0
- tunacode/tools/prompts/read_file_prompt.xml +1 -1
- tunacode/tools/react.py +153 -0
- tunacode/tools/run_command.py +15 -0
- tunacode/types.py +14 -79
- tunacode/ui/completers.py +434 -50
- tunacode/ui/config_dashboard.py +585 -0
- tunacode/ui/console.py +63 -11
- tunacode/ui/input.py +8 -3
- tunacode/ui/keybindings.py +0 -18
- tunacode/ui/model_selector.py +395 -0
- tunacode/ui/output.py +40 -19
- tunacode/ui/panels.py +173 -49
- tunacode/ui/path_heuristics.py +91 -0
- tunacode/ui/prompt_manager.py +1 -20
- tunacode/ui/tool_ui.py +30 -8
- tunacode/utils/api_key_validation.py +93 -0
- tunacode/utils/config_comparator.py +340 -0
- tunacode/utils/models_registry.py +593 -0
- tunacode/utils/text_utils.py +18 -1
- {tunacode_cli-0.0.70.dist-info → tunacode_cli-0.0.78.6.dist-info}/METADATA +80 -12
- {tunacode_cli-0.0.70.dist-info → tunacode_cli-0.0.78.6.dist-info}/RECORD +78 -74
- tunacode/cli/commands/implementations/plan.py +0 -50
- tunacode/cli/commands/implementations/todo.py +0 -217
- tunacode/context.py +0 -71
- tunacode/core/setup/git_safety_setup.py +0 -186
- tunacode/prompts/system.md +0 -359
- tunacode/prompts/system.md.bak +0 -487
- tunacode/tools/exit_plan_mode.py +0 -273
- tunacode/tools/present_plan.py +0 -288
- tunacode/tools/prompts/exit_plan_mode_prompt.xml +0 -25
- tunacode/tools/prompts/present_plan_prompt.xml +0 -20
- tunacode/tools/prompts/todo_prompt.xml +0 -96
- tunacode/tools/todo.py +0 -456
- {tunacode_cli-0.0.70.dist-info → tunacode_cli-0.0.78.6.dist-info}/WHEEL +0 -0
- {tunacode_cli-0.0.70.dist-info → tunacode_cli-0.0.78.6.dist-info}/entry_points.txt +0 -0
- {tunacode_cli-0.0.70.dist-info → tunacode_cli-0.0.78.6.dist-info}/licenses/LICENSE +0 -0
tunacode/ui/input.py
CHANGED
|
@@ -89,20 +89,25 @@ async def multiline_input(
|
|
|
89
89
|
"<bold>Enter</bold> to submit • "
|
|
90
90
|
"<bold>Esc + Enter</bold> for new line • "
|
|
91
91
|
"<bold>Esc twice</bold> to cancel • "
|
|
92
|
-
"<bold>Shift + Tab</bold> toggle plan mode • "
|
|
93
92
|
"<bold>/help</bold> for commands"
|
|
94
93
|
"</darkgrey>"
|
|
95
94
|
)
|
|
96
95
|
)
|
|
97
96
|
|
|
98
|
-
#
|
|
97
|
+
# Create models registry for auto-completion (lazy loaded)
|
|
98
|
+
from ..utils.models_registry import ModelsRegistry
|
|
99
|
+
|
|
100
|
+
models_registry = ModelsRegistry()
|
|
101
|
+
# Note: Registry will be loaded lazily by the completer when needed
|
|
102
|
+
|
|
103
|
+
# Display input area
|
|
99
104
|
result = await input(
|
|
100
105
|
"multiline",
|
|
101
106
|
pretext="> ",
|
|
102
107
|
key_bindings=kb,
|
|
103
108
|
multiline=True,
|
|
104
109
|
placeholder=placeholder,
|
|
105
|
-
completer=create_completer(command_registry),
|
|
110
|
+
completer=create_completer(command_registry, models_registry),
|
|
106
111
|
lexer=FileReferenceLexer(),
|
|
107
112
|
state_manager=state_manager,
|
|
108
113
|
)
|
tunacode/ui/keybindings.py
CHANGED
|
@@ -50,22 +50,4 @@ def create_key_bindings(state_manager: StateManager = None) -> KeyBindings:
|
|
|
50
50
|
|
|
51
51
|
os.kill(os.getpid(), signal.SIGINT)
|
|
52
52
|
|
|
53
|
-
@kb.add("s-tab") # shift+tab
|
|
54
|
-
def _toggle_plan_mode(event):
|
|
55
|
-
"""Toggle between Plan Mode and normal mode."""
|
|
56
|
-
if state_manager:
|
|
57
|
-
# Toggle the state
|
|
58
|
-
if state_manager.is_plan_mode():
|
|
59
|
-
state_manager.exit_plan_mode()
|
|
60
|
-
logger.debug("Toggled to normal mode via Shift+Tab")
|
|
61
|
-
else:
|
|
62
|
-
state_manager.enter_plan_mode()
|
|
63
|
-
logger.debug("Toggled to Plan Mode via Shift+Tab")
|
|
64
|
-
|
|
65
|
-
# Clear the current buffer and refresh the display
|
|
66
|
-
event.current_buffer.reset()
|
|
67
|
-
|
|
68
|
-
# Force a refresh of the application without exiting
|
|
69
|
-
event.app.invalidate()
|
|
70
|
-
|
|
71
53
|
return kb
|
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
"""Interactive model selector UI component."""
|
|
2
|
+
|
|
3
|
+
from typing import List, Optional
|
|
4
|
+
|
|
5
|
+
from prompt_toolkit.application import Application
|
|
6
|
+
from prompt_toolkit.buffer import Buffer
|
|
7
|
+
from prompt_toolkit.formatted_text import HTML, StyleAndTextTuples
|
|
8
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
9
|
+
from prompt_toolkit.layout import (
|
|
10
|
+
FormattedTextControl,
|
|
11
|
+
HSplit,
|
|
12
|
+
Layout,
|
|
13
|
+
VSplit,
|
|
14
|
+
Window,
|
|
15
|
+
WindowAlign,
|
|
16
|
+
)
|
|
17
|
+
from prompt_toolkit.layout.controls import BufferControl
|
|
18
|
+
from prompt_toolkit.layout.dimension import Dimension
|
|
19
|
+
from prompt_toolkit.search import SearchState
|
|
20
|
+
from prompt_toolkit.styles import Style
|
|
21
|
+
from prompt_toolkit.widgets import Frame
|
|
22
|
+
|
|
23
|
+
from ..utils.models_registry import ModelInfo, ModelsRegistry
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ModelSelector:
|
|
27
|
+
"""Interactive model selector with search and navigation."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, registry: ModelsRegistry):
|
|
30
|
+
"""Initialize the model selector."""
|
|
31
|
+
self.registry = registry
|
|
32
|
+
self.models: List[ModelInfo] = []
|
|
33
|
+
self.filtered_models: List[ModelInfo] = []
|
|
34
|
+
self.selected_index = 0
|
|
35
|
+
self.search_text = ""
|
|
36
|
+
self.selected_model: Optional[ModelInfo] = None
|
|
37
|
+
|
|
38
|
+
# Create key bindings
|
|
39
|
+
self.kb = self._create_key_bindings()
|
|
40
|
+
|
|
41
|
+
# Create search buffer
|
|
42
|
+
self.search_buffer = Buffer(on_text_changed=self._on_search_changed)
|
|
43
|
+
|
|
44
|
+
# Search state
|
|
45
|
+
self.search_state = SearchState()
|
|
46
|
+
|
|
47
|
+
def _create_key_bindings(self) -> KeyBindings:
|
|
48
|
+
"""Create key bindings for the selector."""
|
|
49
|
+
kb = KeyBindings()
|
|
50
|
+
|
|
51
|
+
@kb.add("up", "k")
|
|
52
|
+
def move_up(event):
|
|
53
|
+
"""Move selection up."""
|
|
54
|
+
if self.selected_index > 0:
|
|
55
|
+
self.selected_index -= 1
|
|
56
|
+
self._update_display()
|
|
57
|
+
|
|
58
|
+
@kb.add("down", "j")
|
|
59
|
+
def move_down(event):
|
|
60
|
+
"""Move selection down."""
|
|
61
|
+
if self.selected_index < len(self.filtered_models) - 1:
|
|
62
|
+
self.selected_index += 1
|
|
63
|
+
self._update_display()
|
|
64
|
+
|
|
65
|
+
@kb.add("pageup")
|
|
66
|
+
def page_up(event):
|
|
67
|
+
"""Move selection up by page."""
|
|
68
|
+
self.selected_index = max(0, self.selected_index - 10)
|
|
69
|
+
self._update_display()
|
|
70
|
+
|
|
71
|
+
@kb.add("pagedown")
|
|
72
|
+
def page_down(event):
|
|
73
|
+
"""Move selection down by page."""
|
|
74
|
+
self.selected_index = min(len(self.filtered_models) - 1, self.selected_index + 10)
|
|
75
|
+
self._update_display()
|
|
76
|
+
|
|
77
|
+
@kb.add("enter")
|
|
78
|
+
def select_model(event):
|
|
79
|
+
"""Select the current model."""
|
|
80
|
+
if 0 <= self.selected_index < len(self.filtered_models):
|
|
81
|
+
self.selected_model = self.filtered_models[self.selected_index]
|
|
82
|
+
event.app.exit(result=self.selected_model)
|
|
83
|
+
|
|
84
|
+
@kb.add("c-c", "escape", "q")
|
|
85
|
+
def cancel(event):
|
|
86
|
+
"""Cancel selection."""
|
|
87
|
+
event.app.exit(result=None)
|
|
88
|
+
|
|
89
|
+
@kb.add("/")
|
|
90
|
+
def focus_search(event):
|
|
91
|
+
"""Focus the search input."""
|
|
92
|
+
event.app.layout.focus(self.search_buffer)
|
|
93
|
+
|
|
94
|
+
@kb.add("tab")
|
|
95
|
+
def next_provider(event):
|
|
96
|
+
"""Jump to next provider."""
|
|
97
|
+
if not self.filtered_models:
|
|
98
|
+
return
|
|
99
|
+
|
|
100
|
+
current_provider = self.filtered_models[self.selected_index].provider
|
|
101
|
+
for i in range(self.selected_index + 1, len(self.filtered_models)):
|
|
102
|
+
if self.filtered_models[i].provider != current_provider:
|
|
103
|
+
self.selected_index = i
|
|
104
|
+
self._update_display()
|
|
105
|
+
break
|
|
106
|
+
|
|
107
|
+
@kb.add("s-tab")
|
|
108
|
+
def prev_provider(event):
|
|
109
|
+
"""Jump to previous provider."""
|
|
110
|
+
if not self.filtered_models:
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
current_provider = self.filtered_models[self.selected_index].provider
|
|
114
|
+
for i in range(self.selected_index - 1, -1, -1):
|
|
115
|
+
if self.filtered_models[i].provider != current_provider:
|
|
116
|
+
self.selected_index = i
|
|
117
|
+
self._update_display()
|
|
118
|
+
break
|
|
119
|
+
|
|
120
|
+
return kb
|
|
121
|
+
|
|
122
|
+
def _on_search_changed(self, buffer: Buffer) -> None:
|
|
123
|
+
"""Handle search text changes."""
|
|
124
|
+
self.search_text = buffer.text
|
|
125
|
+
self._filter_models()
|
|
126
|
+
self._update_display()
|
|
127
|
+
|
|
128
|
+
def _filter_models(self) -> None:
|
|
129
|
+
"""Filter models based on search text."""
|
|
130
|
+
if not self.search_text:
|
|
131
|
+
self.filtered_models = self.models.copy()
|
|
132
|
+
else:
|
|
133
|
+
# Search and sort by relevance
|
|
134
|
+
self.filtered_models = self.registry.search_models(self.search_text)
|
|
135
|
+
|
|
136
|
+
# Reset selection
|
|
137
|
+
self.selected_index = 0 if self.filtered_models else -1
|
|
138
|
+
|
|
139
|
+
def _get_model_lines(self) -> List[StyleAndTextTuples]:
|
|
140
|
+
"""Get formatted lines for model display."""
|
|
141
|
+
lines = []
|
|
142
|
+
|
|
143
|
+
if not self.filtered_models:
|
|
144
|
+
lines.append([("class:muted", "No models found")])
|
|
145
|
+
return lines
|
|
146
|
+
|
|
147
|
+
# Group models by provider
|
|
148
|
+
current_provider = None
|
|
149
|
+
for i, model in enumerate(self.filtered_models):
|
|
150
|
+
# Add provider header if changed
|
|
151
|
+
if model.provider != current_provider:
|
|
152
|
+
if current_provider is not None:
|
|
153
|
+
lines.append([]) # Empty line between providers
|
|
154
|
+
|
|
155
|
+
provider_info = self.registry.providers.get(model.provider)
|
|
156
|
+
provider_name = provider_info.name if provider_info else model.provider
|
|
157
|
+
lines.append([("class:provider", f"▼ {provider_name}")])
|
|
158
|
+
current_provider = model.provider
|
|
159
|
+
|
|
160
|
+
# Model line
|
|
161
|
+
is_selected = i == self.selected_index
|
|
162
|
+
|
|
163
|
+
# Build model display
|
|
164
|
+
parts = []
|
|
165
|
+
|
|
166
|
+
# Selection indicator
|
|
167
|
+
if is_selected:
|
|
168
|
+
parts.append(("class:selected", "→ "))
|
|
169
|
+
else:
|
|
170
|
+
parts.append(("", " "))
|
|
171
|
+
|
|
172
|
+
# Model ID and name
|
|
173
|
+
parts.append(
|
|
174
|
+
("class:model-id" if not is_selected else "class:selected-id", f"{model.id}")
|
|
175
|
+
)
|
|
176
|
+
parts.append(("class:muted", " - "))
|
|
177
|
+
parts.append(
|
|
178
|
+
("class:model-name" if not is_selected else "class:selected-name", model.name)
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
# Cost and limits
|
|
182
|
+
details = []
|
|
183
|
+
if model.cost.input is not None:
|
|
184
|
+
details.append(f"${model.cost.input}/{model.cost.output}")
|
|
185
|
+
if model.limits.context:
|
|
186
|
+
details.append(f"{model.limits.context // 1000}k")
|
|
187
|
+
|
|
188
|
+
if details:
|
|
189
|
+
parts.append(("class:muted", f" ({', '.join(details)})"))
|
|
190
|
+
|
|
191
|
+
# Capabilities badges
|
|
192
|
+
badges = []
|
|
193
|
+
if model.capabilities.attachment:
|
|
194
|
+
badges.append("📎")
|
|
195
|
+
if model.capabilities.reasoning:
|
|
196
|
+
badges.append("🧠")
|
|
197
|
+
if model.capabilities.tool_call:
|
|
198
|
+
badges.append("🔧")
|
|
199
|
+
|
|
200
|
+
if badges:
|
|
201
|
+
parts.append(("class:badges", " " + "".join(badges)))
|
|
202
|
+
|
|
203
|
+
lines.append(parts)
|
|
204
|
+
|
|
205
|
+
return lines
|
|
206
|
+
|
|
207
|
+
def _get_details_panel(self) -> StyleAndTextTuples:
|
|
208
|
+
"""Get the details panel content for selected model."""
|
|
209
|
+
if not self.filtered_models or self.selected_index < 0:
|
|
210
|
+
return [("", "Select a model to see details")]
|
|
211
|
+
|
|
212
|
+
model = self.filtered_models[self.selected_index]
|
|
213
|
+
lines = []
|
|
214
|
+
|
|
215
|
+
# Model name and ID
|
|
216
|
+
lines.append([("class:title", model.name)])
|
|
217
|
+
lines.append([("class:muted", f"{model.full_id}")])
|
|
218
|
+
lines.append([])
|
|
219
|
+
|
|
220
|
+
# Pricing
|
|
221
|
+
lines.append([("class:section", "Pricing:")])
|
|
222
|
+
if model.cost.input is not None:
|
|
223
|
+
lines.append([("", f" Input: ${model.cost.input} per 1M tokens")])
|
|
224
|
+
lines.append([("", f" Output: ${model.cost.output} per 1M tokens")])
|
|
225
|
+
else:
|
|
226
|
+
lines.append([("class:muted", " Not available")])
|
|
227
|
+
lines.append([])
|
|
228
|
+
|
|
229
|
+
# Limits
|
|
230
|
+
lines.append([("class:section", "Limits:")])
|
|
231
|
+
if model.limits.context:
|
|
232
|
+
lines.append([("", f" Context: {model.limits.context:,} tokens")])
|
|
233
|
+
if model.limits.output:
|
|
234
|
+
lines.append([("", f" Output: {model.limits.output:,} tokens")])
|
|
235
|
+
if not model.limits.context and not model.limits.output:
|
|
236
|
+
lines.append([("class:muted", " Not specified")])
|
|
237
|
+
lines.append([])
|
|
238
|
+
|
|
239
|
+
# Capabilities
|
|
240
|
+
lines.append([("class:section", "Capabilities:")])
|
|
241
|
+
caps = []
|
|
242
|
+
if model.capabilities.attachment:
|
|
243
|
+
caps.append("Attachments")
|
|
244
|
+
if model.capabilities.reasoning:
|
|
245
|
+
caps.append("Reasoning")
|
|
246
|
+
if model.capabilities.tool_call:
|
|
247
|
+
caps.append("Tool calling")
|
|
248
|
+
if model.capabilities.temperature:
|
|
249
|
+
caps.append("Temperature control")
|
|
250
|
+
|
|
251
|
+
if caps:
|
|
252
|
+
for cap in caps:
|
|
253
|
+
lines.append([("", f" ✓ {cap}")])
|
|
254
|
+
else:
|
|
255
|
+
lines.append([("class:muted", " Basic text generation")])
|
|
256
|
+
|
|
257
|
+
if model.capabilities.knowledge:
|
|
258
|
+
lines.append([])
|
|
259
|
+
lines.append([("class:section", "Knowledge cutoff:")])
|
|
260
|
+
lines.append([("", f" {model.capabilities.knowledge}")])
|
|
261
|
+
|
|
262
|
+
# Modalities
|
|
263
|
+
if model.modalities:
|
|
264
|
+
lines.append([])
|
|
265
|
+
lines.append([("class:section", "Modalities:")])
|
|
266
|
+
if "input" in model.modalities:
|
|
267
|
+
lines.append([("", f" Input: {', '.join(model.modalities['input'])}")])
|
|
268
|
+
if "output" in model.modalities:
|
|
269
|
+
lines.append([("", f" Output: {', '.join(model.modalities['output'])}")])
|
|
270
|
+
|
|
271
|
+
return lines
|
|
272
|
+
|
|
273
|
+
def _update_display(self) -> None:
|
|
274
|
+
"""Update the display (called on changes)."""
|
|
275
|
+
# This will trigger a redraw through prompt_toolkit's event system
|
|
276
|
+
if hasattr(self, "app"):
|
|
277
|
+
self.app.invalidate()
|
|
278
|
+
|
|
279
|
+
def _create_layout(self) -> Layout:
|
|
280
|
+
"""Create the application layout."""
|
|
281
|
+
# Model list
|
|
282
|
+
model_list = FormattedTextControl(self._get_model_lines, focusable=False, show_cursor=False)
|
|
283
|
+
|
|
284
|
+
model_window = Window(
|
|
285
|
+
content=model_list,
|
|
286
|
+
width=Dimension(min=40, preferred=60),
|
|
287
|
+
height=Dimension(min=10, preferred=20),
|
|
288
|
+
scroll_offsets=True,
|
|
289
|
+
wrap_lines=False,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
# Details panel
|
|
293
|
+
details_control = FormattedTextControl(
|
|
294
|
+
self._get_details_panel, focusable=False, show_cursor=False
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
details_window = Window(
|
|
298
|
+
content=details_control, width=Dimension(min=30, preferred=40), wrap_lines=True
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
# Search bar
|
|
302
|
+
search_field = Window(
|
|
303
|
+
BufferControl(buffer=self.search_buffer, focus_on_click=True), height=1
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
search_label = Window(
|
|
307
|
+
FormattedTextControl(HTML("<b>Search:</b> ")), width=8, height=1, dont_extend_width=True
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
search_bar = VSplit([search_label, search_field])
|
|
311
|
+
|
|
312
|
+
# Help text
|
|
313
|
+
help_text = Window(
|
|
314
|
+
FormattedTextControl(
|
|
315
|
+
HTML(
|
|
316
|
+
"<muted>↑↓: Navigate | Enter: Select | /: Search | Tab: Next provider | "
|
|
317
|
+
"Esc: Cancel</muted>"
|
|
318
|
+
)
|
|
319
|
+
),
|
|
320
|
+
height=1,
|
|
321
|
+
align=WindowAlign.CENTER,
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
# Main content
|
|
325
|
+
content = VSplit(
|
|
326
|
+
[Frame(model_window, title="Select Model"), Frame(details_window, title="Details")]
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
# Root layout
|
|
330
|
+
root = HSplit(
|
|
331
|
+
[
|
|
332
|
+
search_bar,
|
|
333
|
+
Window(height=1), # Spacer
|
|
334
|
+
content,
|
|
335
|
+
Window(height=1), # Spacer
|
|
336
|
+
help_text,
|
|
337
|
+
]
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
return Layout(root)
|
|
341
|
+
|
|
342
|
+
async def select_model(self, initial_query: str = "") -> Optional[ModelInfo]:
|
|
343
|
+
"""Show the model selector and return selected model."""
|
|
344
|
+
# Load all models
|
|
345
|
+
self.models = list(self.registry.models.values())
|
|
346
|
+
self.search_buffer.text = initial_query
|
|
347
|
+
|
|
348
|
+
# Filter initially
|
|
349
|
+
self._filter_models()
|
|
350
|
+
|
|
351
|
+
# Create application
|
|
352
|
+
self.app = Application(
|
|
353
|
+
layout=self._create_layout(),
|
|
354
|
+
key_bindings=self.kb,
|
|
355
|
+
mouse_support=True,
|
|
356
|
+
full_screen=False,
|
|
357
|
+
style=self._get_style(),
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
# Run the selector
|
|
361
|
+
result = await self.app.run_async()
|
|
362
|
+
return result
|
|
363
|
+
|
|
364
|
+
def _get_style(self) -> Style:
|
|
365
|
+
"""Get the style for the selector."""
|
|
366
|
+
return Style.from_dict(
|
|
367
|
+
{
|
|
368
|
+
"provider": "bold cyan",
|
|
369
|
+
"model-id": "white",
|
|
370
|
+
"model-name": "ansiwhite",
|
|
371
|
+
"selected": "reverse bold",
|
|
372
|
+
"selected-id": "reverse bold white",
|
|
373
|
+
"selected-name": "reverse bold ansiwhite",
|
|
374
|
+
"muted": "gray",
|
|
375
|
+
"badges": "yellow",
|
|
376
|
+
"title": "bold ansiwhite",
|
|
377
|
+
"section": "bold cyan",
|
|
378
|
+
}
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
async def select_model_interactive(
|
|
383
|
+
registry: Optional[ModelsRegistry] = None, initial_query: str = ""
|
|
384
|
+
) -> Optional[str]:
|
|
385
|
+
"""Show interactive model selector and return selected model ID."""
|
|
386
|
+
if registry is None:
|
|
387
|
+
registry = ModelsRegistry()
|
|
388
|
+
await registry.load()
|
|
389
|
+
|
|
390
|
+
selector = ModelSelector(registry)
|
|
391
|
+
model = await selector.select_model(initial_query)
|
|
392
|
+
|
|
393
|
+
if model:
|
|
394
|
+
return model.full_id
|
|
395
|
+
return None
|
tunacode/ui/output.py
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
"""Output and display functions for TunaCode UI."""
|
|
2
2
|
|
|
3
|
-
from typing import Optional
|
|
3
|
+
from typing import TYPE_CHECKING, Any, Optional
|
|
4
4
|
|
|
5
5
|
from prompt_toolkit.application import run_in_terminal
|
|
6
|
-
from rich.console import Console
|
|
7
6
|
from rich.padding import Padding
|
|
8
7
|
|
|
9
|
-
from tunacode.configuration.settings import ApplicationSettings
|
|
10
8
|
from tunacode.constants import (
|
|
11
9
|
MSG_UPDATE_AVAILABLE,
|
|
12
10
|
MSG_UPDATE_INSTRUCTION,
|
|
@@ -22,27 +20,48 @@ from .constants import SPINNER_TYPE
|
|
|
22
20
|
from .decorators import create_sync_wrapper
|
|
23
21
|
from .logging_compat import ui_logger
|
|
24
22
|
|
|
25
|
-
#
|
|
26
|
-
|
|
23
|
+
# Lazy console initialization
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from rich.console import Console
|
|
26
|
+
|
|
27
|
+
_console: Optional["Console"] = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_console() -> "Console":
|
|
31
|
+
"""Get console instance lazily."""
|
|
32
|
+
from rich.console import Console
|
|
33
|
+
|
|
34
|
+
global _console
|
|
35
|
+
if _console is None:
|
|
36
|
+
_console = Console()
|
|
37
|
+
return _console
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class _LazyConsole:
|
|
41
|
+
"""Lightweight proxy that defers Console creation."""
|
|
42
|
+
|
|
43
|
+
def __getattr__(self, name: str) -> Any:
|
|
44
|
+
return getattr(get_console(), name)
|
|
45
|
+
|
|
46
|
+
def __str__(self) -> str:
|
|
47
|
+
return str(get_console())
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
console = _LazyConsole()
|
|
27
51
|
colors = DotDict(UI_COLORS)
|
|
28
52
|
|
|
29
53
|
BANNER = """[bold cyan]
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
██╔════╝██╔═══██╗██╔══██╗██╔════╝
|
|
39
|
-
██║ ██║ ██║██║ ██║█████╗
|
|
40
|
-
██║ ██║ ██║██║ ██║██╔══╝
|
|
41
|
-
╚██████╗╚██████╔╝██████╔╝███████╗
|
|
42
|
-
╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝
|
|
54
|
+
▐█████▌
|
|
55
|
+
▐█▛█▛█▛█▛█▛█▌
|
|
56
|
+
▐█████████████▌
|
|
57
|
+
▐██ ⬤ ██▌
|
|
58
|
+
▐█████████████▌
|
|
59
|
+
▐█▛█▛█▛█▛█▛█▌
|
|
60
|
+
▐█████▌
|
|
61
|
+
TunaCode
|
|
43
62
|
[/bold cyan]
|
|
44
63
|
|
|
45
|
-
|
|
64
|
+
"""
|
|
46
65
|
|
|
47
66
|
|
|
48
67
|
@create_sync_wrapper
|
|
@@ -83,6 +102,8 @@ async def usage(usage: str) -> None:
|
|
|
83
102
|
|
|
84
103
|
async def version() -> None:
|
|
85
104
|
"""Print version information."""
|
|
105
|
+
from tunacode.configuration.settings import ApplicationSettings
|
|
106
|
+
|
|
86
107
|
app_settings = ApplicationSettings()
|
|
87
108
|
await info(MSG_VERSION_DISPLAY.format(version=app_settings.version))
|
|
88
109
|
|