titan-cli 0.1.0__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.
- titan_cli/__init__.py +3 -0
- titan_cli/__main__.py +4 -0
- titan_cli/ai/__init__.py +0 -0
- titan_cli/ai/agents/__init__.py +15 -0
- titan_cli/ai/agents/base.py +152 -0
- titan_cli/ai/client.py +170 -0
- titan_cli/ai/constants.py +56 -0
- titan_cli/ai/exceptions.py +48 -0
- titan_cli/ai/models.py +34 -0
- titan_cli/ai/oauth_helper.py +120 -0
- titan_cli/ai/providers/__init__.py +9 -0
- titan_cli/ai/providers/anthropic.py +117 -0
- titan_cli/ai/providers/base.py +75 -0
- titan_cli/ai/providers/gemini.py +278 -0
- titan_cli/cli.py +59 -0
- titan_cli/clients/__init__.py +1 -0
- titan_cli/clients/gcloud_client.py +52 -0
- titan_cli/core/__init__.py +3 -0
- titan_cli/core/config.py +274 -0
- titan_cli/core/discovery.py +51 -0
- titan_cli/core/errors.py +81 -0
- titan_cli/core/models.py +52 -0
- titan_cli/core/plugins/available.py +36 -0
- titan_cli/core/plugins/models.py +67 -0
- titan_cli/core/plugins/plugin_base.py +108 -0
- titan_cli/core/plugins/plugin_registry.py +163 -0
- titan_cli/core/secrets.py +141 -0
- titan_cli/core/workflows/__init__.py +22 -0
- titan_cli/core/workflows/models.py +88 -0
- titan_cli/core/workflows/project_step_source.py +86 -0
- titan_cli/core/workflows/workflow_exceptions.py +17 -0
- titan_cli/core/workflows/workflow_filter_service.py +137 -0
- titan_cli/core/workflows/workflow_registry.py +419 -0
- titan_cli/core/workflows/workflow_sources.py +307 -0
- titan_cli/engine/__init__.py +39 -0
- titan_cli/engine/builder.py +159 -0
- titan_cli/engine/context.py +82 -0
- titan_cli/engine/mock_context.py +176 -0
- titan_cli/engine/results.py +91 -0
- titan_cli/engine/steps/ai_assistant_step.py +185 -0
- titan_cli/engine/steps/command_step.py +93 -0
- titan_cli/engine/utils/__init__.py +3 -0
- titan_cli/engine/utils/venv.py +31 -0
- titan_cli/engine/workflow_executor.py +187 -0
- titan_cli/external_cli/__init__.py +0 -0
- titan_cli/external_cli/configs.py +17 -0
- titan_cli/external_cli/launcher.py +65 -0
- titan_cli/messages.py +121 -0
- titan_cli/ui/tui/__init__.py +205 -0
- titan_cli/ui/tui/__previews__/statusbar_preview.py +88 -0
- titan_cli/ui/tui/app.py +113 -0
- titan_cli/ui/tui/icons.py +70 -0
- titan_cli/ui/tui/screens/__init__.py +24 -0
- titan_cli/ui/tui/screens/ai_config.py +498 -0
- titan_cli/ui/tui/screens/ai_config_wizard.py +882 -0
- titan_cli/ui/tui/screens/base.py +110 -0
- titan_cli/ui/tui/screens/cli_launcher.py +151 -0
- titan_cli/ui/tui/screens/global_setup_wizard.py +363 -0
- titan_cli/ui/tui/screens/main_menu.py +162 -0
- titan_cli/ui/tui/screens/plugin_config_wizard.py +550 -0
- titan_cli/ui/tui/screens/plugin_management.py +377 -0
- titan_cli/ui/tui/screens/project_setup_wizard.py +686 -0
- titan_cli/ui/tui/screens/workflow_execution.py +592 -0
- titan_cli/ui/tui/screens/workflows.py +249 -0
- titan_cli/ui/tui/textual_components.py +537 -0
- titan_cli/ui/tui/textual_workflow_executor.py +405 -0
- titan_cli/ui/tui/theme.py +102 -0
- titan_cli/ui/tui/widgets/__init__.py +40 -0
- titan_cli/ui/tui/widgets/button.py +108 -0
- titan_cli/ui/tui/widgets/header.py +116 -0
- titan_cli/ui/tui/widgets/panel.py +81 -0
- titan_cli/ui/tui/widgets/status_bar.py +115 -0
- titan_cli/ui/tui/widgets/table.py +77 -0
- titan_cli/ui/tui/widgets/text.py +177 -0
- titan_cli/utils/__init__.py +0 -0
- titan_cli/utils/autoupdate.py +155 -0
- titan_cli-0.1.0.dist-info/METADATA +149 -0
- titan_cli-0.1.0.dist-info/RECORD +146 -0
- titan_cli-0.1.0.dist-info/WHEEL +4 -0
- titan_cli-0.1.0.dist-info/entry_points.txt +9 -0
- titan_cli-0.1.0.dist-info/licenses/LICENSE +201 -0
- titan_plugin_git/__init__.py +1 -0
- titan_plugin_git/clients/__init__.py +8 -0
- titan_plugin_git/clients/git_client.py +772 -0
- titan_plugin_git/exceptions.py +40 -0
- titan_plugin_git/messages.py +112 -0
- titan_plugin_git/models.py +39 -0
- titan_plugin_git/plugin.py +118 -0
- titan_plugin_git/steps/__init__.py +1 -0
- titan_plugin_git/steps/ai_commit_message_step.py +171 -0
- titan_plugin_git/steps/branch_steps.py +104 -0
- titan_plugin_git/steps/commit_step.py +80 -0
- titan_plugin_git/steps/push_step.py +63 -0
- titan_plugin_git/steps/status_step.py +59 -0
- titan_plugin_git/workflows/__previews__/__init__.py +1 -0
- titan_plugin_git/workflows/__previews__/commit_ai_preview.py +124 -0
- titan_plugin_git/workflows/commit-ai.yaml +28 -0
- titan_plugin_github/__init__.py +11 -0
- titan_plugin_github/agents/__init__.py +6 -0
- titan_plugin_github/agents/config_loader.py +130 -0
- titan_plugin_github/agents/issue_generator.py +353 -0
- titan_plugin_github/agents/pr_agent.py +528 -0
- titan_plugin_github/clients/__init__.py +8 -0
- titan_plugin_github/clients/github_client.py +1105 -0
- titan_plugin_github/config/__init__.py +0 -0
- titan_plugin_github/config/pr_agent.toml +85 -0
- titan_plugin_github/exceptions.py +28 -0
- titan_plugin_github/messages.py +88 -0
- titan_plugin_github/models.py +330 -0
- titan_plugin_github/plugin.py +131 -0
- titan_plugin_github/steps/__init__.py +12 -0
- titan_plugin_github/steps/ai_pr_step.py +172 -0
- titan_plugin_github/steps/create_pr_step.py +86 -0
- titan_plugin_github/steps/github_prompt_steps.py +171 -0
- titan_plugin_github/steps/issue_steps.py +143 -0
- titan_plugin_github/steps/preview_step.py +40 -0
- titan_plugin_github/utils.py +82 -0
- titan_plugin_github/workflows/__previews__/__init__.py +1 -0
- titan_plugin_github/workflows/__previews__/create_pr_ai_preview.py +140 -0
- titan_plugin_github/workflows/create-issue-ai.yaml +32 -0
- titan_plugin_github/workflows/create-pr-ai.yaml +49 -0
- titan_plugin_jira/__init__.py +8 -0
- titan_plugin_jira/agents/__init__.py +6 -0
- titan_plugin_jira/agents/config_loader.py +154 -0
- titan_plugin_jira/agents/jira_agent.py +553 -0
- titan_plugin_jira/agents/prompts.py +364 -0
- titan_plugin_jira/agents/response_parser.py +435 -0
- titan_plugin_jira/agents/token_tracker.py +223 -0
- titan_plugin_jira/agents/validators.py +246 -0
- titan_plugin_jira/clients/jira_client.py +745 -0
- titan_plugin_jira/config/jira_agent.toml +92 -0
- titan_plugin_jira/config/templates/issue_analysis.md.j2 +78 -0
- titan_plugin_jira/exceptions.py +37 -0
- titan_plugin_jira/formatters/__init__.py +6 -0
- titan_plugin_jira/formatters/markdown_formatter.py +245 -0
- titan_plugin_jira/messages.py +115 -0
- titan_plugin_jira/models.py +89 -0
- titan_plugin_jira/plugin.py +264 -0
- titan_plugin_jira/steps/ai_analyze_issue_step.py +105 -0
- titan_plugin_jira/steps/get_issue_step.py +82 -0
- titan_plugin_jira/steps/prompt_select_issue_step.py +80 -0
- titan_plugin_jira/steps/search_saved_query_step.py +238 -0
- titan_plugin_jira/utils/__init__.py +13 -0
- titan_plugin_jira/utils/issue_sorter.py +140 -0
- titan_plugin_jira/utils/saved_queries.py +150 -0
- titan_plugin_jira/workflows/analyze-jira-issues.yaml +34 -0
|
@@ -0,0 +1,882 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AI Configuration Wizard Screen
|
|
3
|
+
|
|
4
|
+
Step-by-step wizard for configuring AI providers with visual progress tracking.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from textual.app import ComposeResult
|
|
8
|
+
from textual.widgets import Static, OptionList, Input
|
|
9
|
+
from textual.widgets.option_list import Option
|
|
10
|
+
from textual.containers import Container, Horizontal, VerticalScroll
|
|
11
|
+
from textual.binding import Binding
|
|
12
|
+
|
|
13
|
+
from titan_cli.ui.tui.icons import Icons
|
|
14
|
+
from titan_cli.ui.tui.widgets import Text, DimText, Button
|
|
15
|
+
from .base import BaseScreen
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class StepIndicator(Static):
|
|
19
|
+
"""Widget showing a single step with status indicator."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, step_number: int, title: str, status: str = "pending"):
|
|
22
|
+
self.step_number = step_number
|
|
23
|
+
self.title = title
|
|
24
|
+
self.status = status
|
|
25
|
+
super().__init__()
|
|
26
|
+
|
|
27
|
+
def render(self) -> str:
|
|
28
|
+
"""Render the step with appropriate icon."""
|
|
29
|
+
if self.status == "completed":
|
|
30
|
+
icon = Icons.SUCCESS
|
|
31
|
+
style = "dim"
|
|
32
|
+
elif self.status == "in_progress":
|
|
33
|
+
icon = Icons.RUNNING
|
|
34
|
+
style = "bold cyan"
|
|
35
|
+
else: # pending
|
|
36
|
+
icon = Icons.PENDING
|
|
37
|
+
style = "dim"
|
|
38
|
+
|
|
39
|
+
return f"[{style}]{icon} {self.step_number}. {self.title}[/{style}]"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class AIConfigWizardScreen(BaseScreen):
|
|
43
|
+
"""
|
|
44
|
+
Wizard screen for AI provider configuration.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
BINDINGS = [
|
|
48
|
+
Binding("escape", "back", "Back"),
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
CSS = """
|
|
52
|
+
AIConfigWizardScreen {
|
|
53
|
+
align: center middle;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
#wizard-container {
|
|
57
|
+
width: 100%;
|
|
58
|
+
height: 1fr;
|
|
59
|
+
background: $surface-lighten-1;
|
|
60
|
+
padding: 0 2 1 2;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
#steps-panel {
|
|
64
|
+
width: 20%;
|
|
65
|
+
height: 100%;
|
|
66
|
+
border: round $primary;
|
|
67
|
+
border-title-align: center;
|
|
68
|
+
background: $surface-lighten-1;
|
|
69
|
+
padding: 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
#steps-content {
|
|
73
|
+
padding: 1;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
StepIndicator {
|
|
77
|
+
height: auto;
|
|
78
|
+
margin-bottom: 1;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
#content-panel {
|
|
82
|
+
width: 80%;
|
|
83
|
+
height: 100%;
|
|
84
|
+
border: round $primary;
|
|
85
|
+
border-title-align: center;
|
|
86
|
+
background: $surface-lighten-1;
|
|
87
|
+
padding: 0;
|
|
88
|
+
layout: vertical;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
#content-scroll {
|
|
92
|
+
height: 1fr;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
#content-area {
|
|
96
|
+
padding: 1;
|
|
97
|
+
height: auto;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
#content-title {
|
|
101
|
+
color: $accent;
|
|
102
|
+
text-style: bold;
|
|
103
|
+
margin-bottom: 2;
|
|
104
|
+
height: auto;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
#content-body {
|
|
108
|
+
height: auto;
|
|
109
|
+
margin-bottom: 2;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
#steps-content {
|
|
113
|
+
height: auto;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
#button-container {
|
|
117
|
+
height: auto;
|
|
118
|
+
padding: 1 2;
|
|
119
|
+
background: $surface-lighten-1;
|
|
120
|
+
border-top: solid $primary;
|
|
121
|
+
align: right middle;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
#type-options-list, #provider-options-list {
|
|
125
|
+
height: auto;
|
|
126
|
+
margin-top: 1;
|
|
127
|
+
margin-bottom: 2;
|
|
128
|
+
border: solid $accent;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
#type-options-list > .option-list--option,
|
|
132
|
+
#provider-options-list > .option-list--option {
|
|
133
|
+
padding: 1;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
#type-options-list > .option-list--option-highlighted,
|
|
137
|
+
#provider-options-list > .option-list--option-highlighted {
|
|
138
|
+
padding: 1;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
Input {
|
|
142
|
+
width: 100%;
|
|
143
|
+
margin-top: 1;
|
|
144
|
+
border: solid $accent;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
Input:focus {
|
|
148
|
+
border: solid $primary;
|
|
149
|
+
}
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
def __init__(self, config):
|
|
153
|
+
super().__init__(
|
|
154
|
+
config,
|
|
155
|
+
title=f"{Icons.SETTINGS} Configure AI Provider",
|
|
156
|
+
show_back=True,
|
|
157
|
+
show_status_bar=False
|
|
158
|
+
)
|
|
159
|
+
self.current_step = 0
|
|
160
|
+
self.wizard_data = {}
|
|
161
|
+
|
|
162
|
+
# Define all wizard steps
|
|
163
|
+
self.steps = [
|
|
164
|
+
{"id": "type", "title": "Configuration Type"},
|
|
165
|
+
{"id": "base_url", "title": "Base URL"},
|
|
166
|
+
{"id": "provider", "title": "Select Provider"},
|
|
167
|
+
{"id": "api_key", "title": "API Key"},
|
|
168
|
+
{"id": "model", "title": "Select Model"},
|
|
169
|
+
{"id": "name", "title": "Provider Name"},
|
|
170
|
+
{"id": "advanced", "title": "Advanced Options"},
|
|
171
|
+
{"id": "review", "title": "Review & Save"},
|
|
172
|
+
]
|
|
173
|
+
|
|
174
|
+
def compose_content(self) -> ComposeResult:
|
|
175
|
+
"""Compose the wizard screen with two panels."""
|
|
176
|
+
with Container(id="wizard-container"):
|
|
177
|
+
with Horizontal():
|
|
178
|
+
# Left panel: Steps
|
|
179
|
+
left_panel = VerticalScroll(id="steps-panel")
|
|
180
|
+
left_panel.border_title = "Configuration Steps"
|
|
181
|
+
with left_panel:
|
|
182
|
+
with Container(id="steps-content"):
|
|
183
|
+
for i, step in enumerate(self.steps, 1):
|
|
184
|
+
status = "in_progress" if i == 1 else "pending"
|
|
185
|
+
yield StepIndicator(i, step["title"], status=status)
|
|
186
|
+
|
|
187
|
+
# Right panel: Content
|
|
188
|
+
right_panel = Container(id="content-panel")
|
|
189
|
+
right_panel.border_title = "Step Configuration"
|
|
190
|
+
with right_panel:
|
|
191
|
+
with VerticalScroll(id="content-scroll"):
|
|
192
|
+
with Container(id="content-area"):
|
|
193
|
+
yield Static("", id="content-title")
|
|
194
|
+
yield Container(id="content-body")
|
|
195
|
+
|
|
196
|
+
# Bottom buttons
|
|
197
|
+
with Horizontal(id="button-container"):
|
|
198
|
+
yield Button("Back", variant="default", id="back-button", disabled=True)
|
|
199
|
+
yield Button("Next", variant="primary", id="next-button")
|
|
200
|
+
yield Button("Cancel", variant="default", id="cancel-button")
|
|
201
|
+
|
|
202
|
+
def on_mount(self) -> None:
|
|
203
|
+
"""Load the first step when mounted."""
|
|
204
|
+
self.load_step(0)
|
|
205
|
+
|
|
206
|
+
def load_step(self, step_index: int) -> None:
|
|
207
|
+
"""Load content for the given step."""
|
|
208
|
+
self.current_step = step_index
|
|
209
|
+
step = self.steps[step_index]
|
|
210
|
+
|
|
211
|
+
# Update step indicators
|
|
212
|
+
for i, indicator in enumerate(self.query(StepIndicator)):
|
|
213
|
+
if i < step_index:
|
|
214
|
+
indicator.status = "completed"
|
|
215
|
+
elif i == step_index:
|
|
216
|
+
indicator.status = "in_progress"
|
|
217
|
+
else:
|
|
218
|
+
indicator.status = "pending"
|
|
219
|
+
indicator.refresh()
|
|
220
|
+
|
|
221
|
+
# Update buttons
|
|
222
|
+
back_button = self.query_one("#back-button", Button)
|
|
223
|
+
back_button.disabled = (step_index == 0)
|
|
224
|
+
|
|
225
|
+
# Change Next button to Save on last step
|
|
226
|
+
next_button = self.query_one("#next-button", Button)
|
|
227
|
+
if step_index == len(self.steps) - 1:
|
|
228
|
+
next_button.label = "Save"
|
|
229
|
+
else:
|
|
230
|
+
next_button.label = "Next"
|
|
231
|
+
|
|
232
|
+
# Load step content
|
|
233
|
+
content_title = self.query_one("#content-title", Static)
|
|
234
|
+
content_body = self.query_one("#content-body", Container)
|
|
235
|
+
|
|
236
|
+
if step["id"] == "type":
|
|
237
|
+
self.load_type_step(content_title, content_body)
|
|
238
|
+
elif step["id"] == "base_url":
|
|
239
|
+
self.load_base_url_step(content_title, content_body)
|
|
240
|
+
elif step["id"] == "provider":
|
|
241
|
+
self.load_provider_step(content_title, content_body)
|
|
242
|
+
elif step["id"] == "api_key":
|
|
243
|
+
self.load_api_key_step(content_title, content_body)
|
|
244
|
+
elif step["id"] == "model":
|
|
245
|
+
self.load_model_step(content_title, content_body)
|
|
246
|
+
elif step["id"] == "name":
|
|
247
|
+
self.load_name_step(content_title, content_body)
|
|
248
|
+
elif step["id"] == "advanced":
|
|
249
|
+
self.load_advanced_step(content_title, content_body)
|
|
250
|
+
elif step["id"] == "review":
|
|
251
|
+
self.load_review_step(content_title, content_body)
|
|
252
|
+
|
|
253
|
+
def load_type_step(self, title_widget: Static, body_widget: Container) -> None:
|
|
254
|
+
"""Load Configuration Type step."""
|
|
255
|
+
title_widget.update("Select Configuration Type")
|
|
256
|
+
|
|
257
|
+
# Clear previous content
|
|
258
|
+
body_widget.remove_children()
|
|
259
|
+
|
|
260
|
+
# Add description
|
|
261
|
+
description = Static(
|
|
262
|
+
"Choose the type of AI configuration:\n\n"
|
|
263
|
+
"• Corporate: Use your company's AI endpoint\n"
|
|
264
|
+
"• Individual: Use your personal API key"
|
|
265
|
+
)
|
|
266
|
+
body_widget.mount(description)
|
|
267
|
+
|
|
268
|
+
# Add options
|
|
269
|
+
options = OptionList(
|
|
270
|
+
Option("Corporate Configuration", id="corporate"),
|
|
271
|
+
Option("Individual Configuration", id="individual"),
|
|
272
|
+
id="type-options-list"
|
|
273
|
+
)
|
|
274
|
+
body_widget.mount(options)
|
|
275
|
+
|
|
276
|
+
# Focus the options list
|
|
277
|
+
self.call_after_refresh(lambda: options.focus())
|
|
278
|
+
|
|
279
|
+
def load_base_url_step(self, title_widget: Static, body_widget: Container) -> None:
|
|
280
|
+
"""Load Base URL step (only for corporate)."""
|
|
281
|
+
title_widget.update("Base URL Configuration")
|
|
282
|
+
body_widget.remove_children()
|
|
283
|
+
|
|
284
|
+
# Add description
|
|
285
|
+
description = Text(
|
|
286
|
+
"Configure your corporate AI endpoint.\n\n"
|
|
287
|
+
"Enter the base URL for your organization's AI service."
|
|
288
|
+
)
|
|
289
|
+
body_widget.mount(description)
|
|
290
|
+
|
|
291
|
+
# Add examples
|
|
292
|
+
examples = DimText(
|
|
293
|
+
"\nExamples:\n"
|
|
294
|
+
" • https://ai.yourcompany.com\n"
|
|
295
|
+
" • https://api.internal.corp/ai\n"
|
|
296
|
+
" • https://llm-gateway.enterprise.local"
|
|
297
|
+
)
|
|
298
|
+
body_widget.mount(examples)
|
|
299
|
+
|
|
300
|
+
# Add input field with default value
|
|
301
|
+
default_url = self.wizard_data.get("base_url", "https://")
|
|
302
|
+
input_widget = Input(
|
|
303
|
+
value=default_url,
|
|
304
|
+
placeholder="Enter base URL...",
|
|
305
|
+
id="base-url-input"
|
|
306
|
+
)
|
|
307
|
+
input_widget.styles.margin = (2, 0, 0, 0)
|
|
308
|
+
body_widget.mount(input_widget)
|
|
309
|
+
|
|
310
|
+
# Focus the input
|
|
311
|
+
self.call_after_refresh(lambda: input_widget.focus())
|
|
312
|
+
|
|
313
|
+
def load_provider_step(self, title_widget: Static, body_widget: Container) -> None:
|
|
314
|
+
"""Load Provider Selection step."""
|
|
315
|
+
title_widget.update("Select AI Provider")
|
|
316
|
+
body_widget.remove_children()
|
|
317
|
+
|
|
318
|
+
# Add description
|
|
319
|
+
description = Text(
|
|
320
|
+
"Choose your AI provider.\n\n"
|
|
321
|
+
"Select the AI service you want to use for this configuration."
|
|
322
|
+
)
|
|
323
|
+
body_widget.mount(description)
|
|
324
|
+
|
|
325
|
+
# Add provider options (only Anthropic and Gemini are supported)
|
|
326
|
+
options = OptionList(
|
|
327
|
+
Option("Anthropic (Claude)", id="anthropic"),
|
|
328
|
+
Option("Google (Gemini)", id="gemini"),
|
|
329
|
+
id="provider-options-list"
|
|
330
|
+
)
|
|
331
|
+
body_widget.mount(options)
|
|
332
|
+
|
|
333
|
+
# Focus the options list
|
|
334
|
+
self.call_after_refresh(lambda: options.focus())
|
|
335
|
+
|
|
336
|
+
def load_api_key_step(self, title_widget: Static, body_widget: Container) -> None:
|
|
337
|
+
"""Load API Key step."""
|
|
338
|
+
title_widget.update("Enter API Key")
|
|
339
|
+
body_widget.remove_children()
|
|
340
|
+
|
|
341
|
+
# Get provider name for context
|
|
342
|
+
provider = self.wizard_data.get("provider", "")
|
|
343
|
+
provider_name = "Anthropic" if provider == "anthropic" else "Google" if provider == "gemini" else "AI"
|
|
344
|
+
|
|
345
|
+
# Add description
|
|
346
|
+
description = Text(
|
|
347
|
+
f"Enter your {provider_name} API key.\n\n"
|
|
348
|
+
f"This key will be securely stored in your system's keyring."
|
|
349
|
+
)
|
|
350
|
+
body_widget.mount(description)
|
|
351
|
+
|
|
352
|
+
# Add info about getting the key
|
|
353
|
+
info = DimText(
|
|
354
|
+
"\nWhere to get your API key:\n"
|
|
355
|
+
" • Anthropic: https://console.anthropic.com/settings/keys\n"
|
|
356
|
+
" • Google: https://aistudio.google.com/app/apikey"
|
|
357
|
+
)
|
|
358
|
+
body_widget.mount(info)
|
|
359
|
+
|
|
360
|
+
# Add input field (password type to hide the key)
|
|
361
|
+
default_key = self.wizard_data.get("api_key", "")
|
|
362
|
+
input_widget = Input(
|
|
363
|
+
value=default_key,
|
|
364
|
+
placeholder="Enter API key...",
|
|
365
|
+
password=True,
|
|
366
|
+
id="api-key-input"
|
|
367
|
+
)
|
|
368
|
+
input_widget.styles.margin = (2, 0, 0, 0)
|
|
369
|
+
body_widget.mount(input_widget)
|
|
370
|
+
|
|
371
|
+
# Focus the input
|
|
372
|
+
self.call_after_refresh(lambda: input_widget.focus())
|
|
373
|
+
|
|
374
|
+
def load_model_step(self, title_widget: Static, body_widget: Container) -> None:
|
|
375
|
+
"""Load Model Selection step."""
|
|
376
|
+
title_widget.update("Select Model")
|
|
377
|
+
body_widget.remove_children()
|
|
378
|
+
|
|
379
|
+
# Get provider to show relevant models
|
|
380
|
+
provider = self.wizard_data.get("provider", "")
|
|
381
|
+
|
|
382
|
+
# Add description
|
|
383
|
+
description = Text(
|
|
384
|
+
"Select or enter the model to use.\n\n"
|
|
385
|
+
"You can choose from popular models or enter a custom model name."
|
|
386
|
+
)
|
|
387
|
+
body_widget.mount(description)
|
|
388
|
+
|
|
389
|
+
# Show popular models based on provider
|
|
390
|
+
if provider == "anthropic":
|
|
391
|
+
models_info = DimText(
|
|
392
|
+
"\nPopular Claude models:\n"
|
|
393
|
+
" • claude-3-5-sonnet-20241022\n"
|
|
394
|
+
" • claude-3-opus-20240229\n"
|
|
395
|
+
" • claude-3-sonnet-20240229\n"
|
|
396
|
+
" • claude-3-haiku-20240307\n"
|
|
397
|
+
" • claude-3-5-haiku-20241022"
|
|
398
|
+
)
|
|
399
|
+
elif provider == "gemini":
|
|
400
|
+
models_info = DimText(
|
|
401
|
+
"\nPopular Gemini models:\n"
|
|
402
|
+
" • gemini-1.5-pro\n"
|
|
403
|
+
" • gemini-1.5-flash\n"
|
|
404
|
+
" • gemini-pro"
|
|
405
|
+
)
|
|
406
|
+
else:
|
|
407
|
+
models_info = DimText("\nEnter the model name for your provider.")
|
|
408
|
+
|
|
409
|
+
body_widget.mount(models_info)
|
|
410
|
+
|
|
411
|
+
# Add input field with default model
|
|
412
|
+
from titan_cli.ai.constants import get_default_model
|
|
413
|
+
default_model = self.wizard_data.get("model", get_default_model(provider) if provider else "")
|
|
414
|
+
|
|
415
|
+
input_widget = Input(
|
|
416
|
+
value=default_model,
|
|
417
|
+
placeholder="Enter model name...",
|
|
418
|
+
id="model-input"
|
|
419
|
+
)
|
|
420
|
+
input_widget.styles.margin = (2, 0, 0, 0)
|
|
421
|
+
body_widget.mount(input_widget)
|
|
422
|
+
|
|
423
|
+
# Focus the input
|
|
424
|
+
self.call_after_refresh(lambda: input_widget.focus())
|
|
425
|
+
|
|
426
|
+
def load_name_step(self, title_widget: Static, body_widget: Container) -> None:
|
|
427
|
+
"""Load Provider Name step."""
|
|
428
|
+
title_widget.update("Provider Name")
|
|
429
|
+
body_widget.remove_children()
|
|
430
|
+
|
|
431
|
+
# Add description
|
|
432
|
+
description = Text(
|
|
433
|
+
"Name this provider configuration.\n\n"
|
|
434
|
+
"This helps you identify this configuration when you have multiple providers."
|
|
435
|
+
)
|
|
436
|
+
body_widget.mount(description)
|
|
437
|
+
|
|
438
|
+
# Generate default name based on type and provider
|
|
439
|
+
config_type = self.wizard_data.get("config_type", "")
|
|
440
|
+
provider = self.wizard_data.get("provider", "")
|
|
441
|
+
|
|
442
|
+
config_type_label = "Corporate" if config_type == "corporate" else "Individual"
|
|
443
|
+
provider_name = "Anthropic" if provider == "anthropic" else "Google" if provider == "gemini" else "AI"
|
|
444
|
+
|
|
445
|
+
default_name = self.wizard_data.get("provider_name", f"{config_type_label} {provider_name}")
|
|
446
|
+
|
|
447
|
+
# Add example
|
|
448
|
+
example = DimText(
|
|
449
|
+
f"\nExamples:\n"
|
|
450
|
+
f" • {config_type_label} {provider_name}\n"
|
|
451
|
+
f" • My {provider_name} Account\n"
|
|
452
|
+
f" • Work Claude\n"
|
|
453
|
+
f" • Personal Gemini"
|
|
454
|
+
)
|
|
455
|
+
body_widget.mount(example)
|
|
456
|
+
|
|
457
|
+
# Add input field
|
|
458
|
+
input_widget = Input(
|
|
459
|
+
value=default_name,
|
|
460
|
+
placeholder="Enter provider name...",
|
|
461
|
+
id="name-input"
|
|
462
|
+
)
|
|
463
|
+
input_widget.styles.margin = (2, 0, 0, 0)
|
|
464
|
+
body_widget.mount(input_widget)
|
|
465
|
+
|
|
466
|
+
# Focus the input
|
|
467
|
+
self.call_after_refresh(lambda: input_widget.focus())
|
|
468
|
+
|
|
469
|
+
def load_advanced_step(self, title_widget: Static, body_widget: Container) -> None:
|
|
470
|
+
"""Load Advanced Options step."""
|
|
471
|
+
title_widget.update("Advanced Options")
|
|
472
|
+
body_widget.remove_children()
|
|
473
|
+
|
|
474
|
+
# Add description
|
|
475
|
+
description = Text(
|
|
476
|
+
"Configure advanced AI parameters (optional).\n\n"
|
|
477
|
+
"These settings control the AI's behavior. You can use the defaults or customize them."
|
|
478
|
+
)
|
|
479
|
+
body_widget.mount(description)
|
|
480
|
+
|
|
481
|
+
# Get provider to show correct temperature range
|
|
482
|
+
provider = self.wizard_data.get("provider", "")
|
|
483
|
+
max_temp = "1.0" if provider == "anthropic" else "2.0"
|
|
484
|
+
|
|
485
|
+
# Temperature input
|
|
486
|
+
temp_label = Text(f"\nTemperature (0.0 - {max_temp}):")
|
|
487
|
+
temp_label.styles.margin = (2, 0, 0, 0)
|
|
488
|
+
body_widget.mount(temp_label)
|
|
489
|
+
|
|
490
|
+
temp_info = DimText(
|
|
491
|
+
"Controls randomness. Lower = more focused, higher = more creative.\n"
|
|
492
|
+
"Default: 0.7"
|
|
493
|
+
)
|
|
494
|
+
body_widget.mount(temp_info)
|
|
495
|
+
|
|
496
|
+
default_temp = str(self.wizard_data.get("temperature", "0.7"))
|
|
497
|
+
temp_input = Input(
|
|
498
|
+
value=default_temp,
|
|
499
|
+
placeholder="0.7",
|
|
500
|
+
id="temperature-input"
|
|
501
|
+
)
|
|
502
|
+
temp_input.styles.margin = (1, 0, 0, 0)
|
|
503
|
+
body_widget.mount(temp_input)
|
|
504
|
+
|
|
505
|
+
# Max tokens input
|
|
506
|
+
tokens_label = Text("\nMax Tokens:")
|
|
507
|
+
tokens_label.styles.margin = (2, 0, 0, 0)
|
|
508
|
+
body_widget.mount(tokens_label)
|
|
509
|
+
|
|
510
|
+
tokens_info = DimText(
|
|
511
|
+
"Maximum length of AI responses.\n"
|
|
512
|
+
"Default: 4096"
|
|
513
|
+
)
|
|
514
|
+
body_widget.mount(tokens_info)
|
|
515
|
+
|
|
516
|
+
default_tokens = str(self.wizard_data.get("max_tokens", "4096"))
|
|
517
|
+
tokens_input = Input(
|
|
518
|
+
value=default_tokens,
|
|
519
|
+
placeholder="4096",
|
|
520
|
+
id="max-tokens-input"
|
|
521
|
+
)
|
|
522
|
+
tokens_input.styles.margin = (1, 0, 0, 0)
|
|
523
|
+
body_widget.mount(tokens_input)
|
|
524
|
+
|
|
525
|
+
# Focus the temperature input
|
|
526
|
+
self.call_after_refresh(lambda: temp_input.focus())
|
|
527
|
+
|
|
528
|
+
def load_review_step(self, title_widget: Static, body_widget: Container) -> None:
|
|
529
|
+
"""Load Review & Save step."""
|
|
530
|
+
title_widget.update("Review Configuration")
|
|
531
|
+
body_widget.remove_children()
|
|
532
|
+
|
|
533
|
+
# Add description
|
|
534
|
+
description = Text(
|
|
535
|
+
"Review your configuration before saving.\n\n"
|
|
536
|
+
"Please verify all settings are correct."
|
|
537
|
+
)
|
|
538
|
+
body_widget.mount(description)
|
|
539
|
+
|
|
540
|
+
# Build configuration summary
|
|
541
|
+
config_type = self.wizard_data.get("config_type", "")
|
|
542
|
+
base_url = self.wizard_data.get("base_url", "")
|
|
543
|
+
provider = self.wizard_data.get("provider", "")
|
|
544
|
+
model = self.wizard_data.get("model", "")
|
|
545
|
+
provider_name = self.wizard_data.get("provider_name", "")
|
|
546
|
+
temperature = self.wizard_data.get("temperature", 0.7)
|
|
547
|
+
max_tokens = self.wizard_data.get("max_tokens", 4096)
|
|
548
|
+
|
|
549
|
+
# Format provider name
|
|
550
|
+
provider_label = "Anthropic" if provider == "anthropic" else "Google" if provider == "gemini" else provider
|
|
551
|
+
config_type_label = "Corporate" if config_type == "corporate" else "Individual"
|
|
552
|
+
|
|
553
|
+
# Create summary text
|
|
554
|
+
summary = Text("\n")
|
|
555
|
+
summary.styles.margin = (2, 0, 0, 0)
|
|
556
|
+
body_widget.mount(summary)
|
|
557
|
+
|
|
558
|
+
# Configuration details
|
|
559
|
+
from titan_cli.ui.tui.widgets import BoldText
|
|
560
|
+
|
|
561
|
+
body_widget.mount(BoldText("Configuration Type:"))
|
|
562
|
+
body_widget.mount(DimText(f" {config_type_label}"))
|
|
563
|
+
body_widget.mount(Text(""))
|
|
564
|
+
|
|
565
|
+
if base_url:
|
|
566
|
+
body_widget.mount(BoldText("Base URL:"))
|
|
567
|
+
body_widget.mount(DimText(f" {base_url}"))
|
|
568
|
+
body_widget.mount(Text(""))
|
|
569
|
+
|
|
570
|
+
body_widget.mount(BoldText("Provider:"))
|
|
571
|
+
body_widget.mount(DimText(f" {provider_label}"))
|
|
572
|
+
body_widget.mount(Text(""))
|
|
573
|
+
|
|
574
|
+
body_widget.mount(BoldText("Model:"))
|
|
575
|
+
body_widget.mount(DimText(f" {model}"))
|
|
576
|
+
body_widget.mount(Text(""))
|
|
577
|
+
|
|
578
|
+
body_widget.mount(BoldText("Provider Name:"))
|
|
579
|
+
body_widget.mount(DimText(f" {provider_name}"))
|
|
580
|
+
body_widget.mount(Text(""))
|
|
581
|
+
|
|
582
|
+
body_widget.mount(BoldText("Temperature:"))
|
|
583
|
+
body_widget.mount(DimText(f" {temperature}"))
|
|
584
|
+
body_widget.mount(Text(""))
|
|
585
|
+
|
|
586
|
+
body_widget.mount(BoldText("Max Tokens:"))
|
|
587
|
+
body_widget.mount(DimText(f" {max_tokens}"))
|
|
588
|
+
body_widget.mount(Text(""))
|
|
589
|
+
|
|
590
|
+
body_widget.mount(BoldText("API Key:"))
|
|
591
|
+
body_widget.mount(DimText(" ••••••••••••••••••••"))
|
|
592
|
+
body_widget.mount(Text(""))
|
|
593
|
+
|
|
594
|
+
def on_option_list_option_selected(self, event: OptionList.OptionSelected) -> None:
|
|
595
|
+
"""Handle option selection in lists - auto-advance to next step."""
|
|
596
|
+
# Save the selection and move to next step
|
|
597
|
+
self.handle_next()
|
|
598
|
+
|
|
599
|
+
def on_input_submitted(self, event: Input.Submitted) -> None:
|
|
600
|
+
"""Handle Enter key in input fields - auto-advance to next step."""
|
|
601
|
+
self.handle_next()
|
|
602
|
+
|
|
603
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
604
|
+
"""Handle button presses."""
|
|
605
|
+
if event.button.id == "next-button":
|
|
606
|
+
self.handle_next()
|
|
607
|
+
elif event.button.id == "back-button":
|
|
608
|
+
self.handle_back()
|
|
609
|
+
elif event.button.id == "cancel-button":
|
|
610
|
+
self.action_back()
|
|
611
|
+
|
|
612
|
+
def handle_next(self) -> None:
|
|
613
|
+
"""Move to next step or save configuration."""
|
|
614
|
+
# Validate and save current step data
|
|
615
|
+
if not self.validate_and_save_step():
|
|
616
|
+
return
|
|
617
|
+
|
|
618
|
+
# If on last step, save configuration
|
|
619
|
+
if self.current_step == len(self.steps) - 1:
|
|
620
|
+
self.save_configuration()
|
|
621
|
+
return
|
|
622
|
+
|
|
623
|
+
# Move to next step
|
|
624
|
+
if self.current_step < len(self.steps) - 1:
|
|
625
|
+
next_step = self.current_step + 1
|
|
626
|
+
|
|
627
|
+
# Skip Base URL step if Individual was selected
|
|
628
|
+
if self.steps[next_step]["id"] == "base_url" and self.wizard_data.get("config_type") == "individual":
|
|
629
|
+
next_step += 1
|
|
630
|
+
|
|
631
|
+
self.load_step(next_step)
|
|
632
|
+
|
|
633
|
+
def validate_and_save_step(self) -> bool:
|
|
634
|
+
"""Validate and save data from current step."""
|
|
635
|
+
step = self.steps[self.current_step]
|
|
636
|
+
|
|
637
|
+
if step["id"] == "type":
|
|
638
|
+
# Get selected configuration type
|
|
639
|
+
try:
|
|
640
|
+
options_list = self.query_one("#type-options-list", OptionList)
|
|
641
|
+
if options_list.highlighted is None:
|
|
642
|
+
self.app.notify("Please select a configuration type", severity="warning")
|
|
643
|
+
return False
|
|
644
|
+
|
|
645
|
+
selected_option = options_list.get_option_at_index(options_list.highlighted)
|
|
646
|
+
self.wizard_data["config_type"] = selected_option.id
|
|
647
|
+
return True
|
|
648
|
+
except Exception:
|
|
649
|
+
self.app.notify("Please select a configuration type", severity="error")
|
|
650
|
+
return False
|
|
651
|
+
|
|
652
|
+
elif step["id"] == "base_url":
|
|
653
|
+
# Get base URL from input
|
|
654
|
+
try:
|
|
655
|
+
input_widget = self.query_one("#base-url-input", Input)
|
|
656
|
+
base_url = input_widget.value.strip()
|
|
657
|
+
|
|
658
|
+
# Validate URL
|
|
659
|
+
if not base_url:
|
|
660
|
+
self.app.notify("Please enter a base URL", severity="warning")
|
|
661
|
+
return False
|
|
662
|
+
|
|
663
|
+
if not base_url.startswith(("http://", "https://")):
|
|
664
|
+
self.app.notify("Base URL must start with http:// or https://", severity="warning")
|
|
665
|
+
return False
|
|
666
|
+
|
|
667
|
+
self.wizard_data["base_url"] = base_url
|
|
668
|
+
return True
|
|
669
|
+
except Exception:
|
|
670
|
+
self.app.notify("Please enter a valid base URL", severity="error")
|
|
671
|
+
return False
|
|
672
|
+
|
|
673
|
+
elif step["id"] == "provider":
|
|
674
|
+
# Get selected provider
|
|
675
|
+
try:
|
|
676
|
+
options_list = self.query_one("#provider-options-list", OptionList)
|
|
677
|
+
if options_list.highlighted is None:
|
|
678
|
+
self.app.notify("Please select a provider", severity="warning")
|
|
679
|
+
return False
|
|
680
|
+
|
|
681
|
+
selected_option = options_list.get_option_at_index(options_list.highlighted)
|
|
682
|
+
self.wizard_data["provider"] = selected_option.id
|
|
683
|
+
return True
|
|
684
|
+
except Exception:
|
|
685
|
+
self.app.notify("Please select a provider", severity="error")
|
|
686
|
+
return False
|
|
687
|
+
|
|
688
|
+
elif step["id"] == "api_key":
|
|
689
|
+
# Get API key from input
|
|
690
|
+
try:
|
|
691
|
+
input_widget = self.query_one("#api-key-input", Input)
|
|
692
|
+
api_key = input_widget.value.strip()
|
|
693
|
+
|
|
694
|
+
# Validate API key
|
|
695
|
+
if not api_key:
|
|
696
|
+
self.app.notify("Please enter an API key", severity="warning")
|
|
697
|
+
return False
|
|
698
|
+
|
|
699
|
+
# Basic validation: should be alphanumeric with possible hyphens/underscores
|
|
700
|
+
if len(api_key) < 10:
|
|
701
|
+
self.app.notify("API key seems too short", severity="warning")
|
|
702
|
+
return False
|
|
703
|
+
|
|
704
|
+
self.wizard_data["api_key"] = api_key
|
|
705
|
+
return True
|
|
706
|
+
except Exception:
|
|
707
|
+
self.app.notify("Please enter a valid API key", severity="error")
|
|
708
|
+
return False
|
|
709
|
+
|
|
710
|
+
elif step["id"] == "model":
|
|
711
|
+
# Get model from input
|
|
712
|
+
try:
|
|
713
|
+
input_widget = self.query_one("#model-input", Input)
|
|
714
|
+
model = input_widget.value.strip()
|
|
715
|
+
|
|
716
|
+
# Validate model
|
|
717
|
+
if not model:
|
|
718
|
+
self.app.notify("Please enter a model name", severity="warning")
|
|
719
|
+
return False
|
|
720
|
+
|
|
721
|
+
self.wizard_data["model"] = model
|
|
722
|
+
return True
|
|
723
|
+
except Exception:
|
|
724
|
+
self.app.notify("Please enter a valid model name", severity="error")
|
|
725
|
+
return False
|
|
726
|
+
|
|
727
|
+
elif step["id"] == "name":
|
|
728
|
+
# Get provider name from input
|
|
729
|
+
try:
|
|
730
|
+
input_widget = self.query_one("#name-input", Input)
|
|
731
|
+
provider_name = input_widget.value.strip()
|
|
732
|
+
|
|
733
|
+
# Validate name
|
|
734
|
+
if not provider_name:
|
|
735
|
+
self.app.notify("Please enter a provider name", severity="warning")
|
|
736
|
+
return False
|
|
737
|
+
|
|
738
|
+
self.wizard_data["provider_name"] = provider_name
|
|
739
|
+
return True
|
|
740
|
+
except Exception:
|
|
741
|
+
self.app.notify("Please enter a valid provider name", severity="error")
|
|
742
|
+
return False
|
|
743
|
+
|
|
744
|
+
elif step["id"] == "advanced":
|
|
745
|
+
# Get temperature and max_tokens
|
|
746
|
+
try:
|
|
747
|
+
temp_input = self.query_one("#temperature-input", Input)
|
|
748
|
+
tokens_input = self.query_one("#max-tokens-input", Input)
|
|
749
|
+
|
|
750
|
+
# Get provider to determine max temperature
|
|
751
|
+
provider = self.wizard_data.get("provider", "")
|
|
752
|
+
max_temp = 1.0 if provider == "anthropic" else 2.0
|
|
753
|
+
|
|
754
|
+
# Validate temperature
|
|
755
|
+
try:
|
|
756
|
+
temperature = float(temp_input.value.strip())
|
|
757
|
+
if temperature < 0.0 or temperature > max_temp:
|
|
758
|
+
self.app.notify(f"Temperature must be between 0.0 and {max_temp}", severity="warning")
|
|
759
|
+
return False
|
|
760
|
+
except ValueError:
|
|
761
|
+
self.app.notify("Temperature must be a number", severity="warning")
|
|
762
|
+
return False
|
|
763
|
+
|
|
764
|
+
# Validate max_tokens
|
|
765
|
+
try:
|
|
766
|
+
max_tokens = int(tokens_input.value.strip())
|
|
767
|
+
if max_tokens < 1:
|
|
768
|
+
self.app.notify("Max tokens must be at least 1", severity="warning")
|
|
769
|
+
return False
|
|
770
|
+
except ValueError:
|
|
771
|
+
self.app.notify("Max tokens must be a number", severity="warning")
|
|
772
|
+
return False
|
|
773
|
+
|
|
774
|
+
self.wizard_data["temperature"] = temperature
|
|
775
|
+
self.wizard_data["max_tokens"] = max_tokens
|
|
776
|
+
return True
|
|
777
|
+
except Exception:
|
|
778
|
+
self.app.notify("Please enter valid advanced options", severity="error")
|
|
779
|
+
return False
|
|
780
|
+
|
|
781
|
+
# TODO: Add validation for review step
|
|
782
|
+
return True
|
|
783
|
+
|
|
784
|
+
def handle_back(self) -> None:
|
|
785
|
+
"""Move to previous step."""
|
|
786
|
+
if self.current_step > 0:
|
|
787
|
+
prev_step = self.current_step - 1
|
|
788
|
+
|
|
789
|
+
# Skip Base URL step if going back and config type is Individual
|
|
790
|
+
if self.steps[prev_step]["id"] == "base_url" and self.wizard_data.get("config_type") == "individual":
|
|
791
|
+
prev_step -= 1
|
|
792
|
+
|
|
793
|
+
self.load_step(prev_step)
|
|
794
|
+
|
|
795
|
+
def save_configuration(self) -> None:
|
|
796
|
+
"""Save the AI provider configuration."""
|
|
797
|
+
import tomli
|
|
798
|
+
import tomli_w
|
|
799
|
+
import re
|
|
800
|
+
import logging
|
|
801
|
+
from titan_cli.core.config import TitanConfig
|
|
802
|
+
from titan_cli.core.secrets import SecretManager
|
|
803
|
+
|
|
804
|
+
logger = logging.getLogger('titan_cli.ui.tui.screens.project_setup_wizard')
|
|
805
|
+
logger.debug("AI wizard - save_configuration() called")
|
|
806
|
+
|
|
807
|
+
try:
|
|
808
|
+
# Generate provider ID from name (clean to only allow valid characters)
|
|
809
|
+
provider_name = self.wizard_data.get("provider_name", "")
|
|
810
|
+
# Replace spaces with hyphens, remove invalid characters
|
|
811
|
+
provider_id = provider_name.lower().replace(" ", "-")
|
|
812
|
+
provider_id = re.sub(r'[^a-z0-9_-]', '', provider_id)
|
|
813
|
+
|
|
814
|
+
# Load global config
|
|
815
|
+
global_config_path = TitanConfig.GLOBAL_CONFIG
|
|
816
|
+
global_config_data = {}
|
|
817
|
+
if global_config_path.exists():
|
|
818
|
+
with open(global_config_path, "rb") as f:
|
|
819
|
+
global_config_data = tomli.load(f)
|
|
820
|
+
|
|
821
|
+
# Initialize AI config structure if needed
|
|
822
|
+
if "ai" not in global_config_data:
|
|
823
|
+
global_config_data["ai"] = {}
|
|
824
|
+
if "providers" not in global_config_data["ai"]:
|
|
825
|
+
global_config_data["ai"]["providers"] = {}
|
|
826
|
+
|
|
827
|
+
# Check if provider ID already exists
|
|
828
|
+
if provider_id in global_config_data["ai"]["providers"]:
|
|
829
|
+
self.app.notify(f"Provider ID '{provider_id}' already exists", severity="error")
|
|
830
|
+
return
|
|
831
|
+
|
|
832
|
+
# Build provider configuration
|
|
833
|
+
provider_cfg = {
|
|
834
|
+
"name": self.wizard_data.get("provider_name"),
|
|
835
|
+
"type": self.wizard_data.get("config_type"),
|
|
836
|
+
"provider": self.wizard_data.get("provider"),
|
|
837
|
+
"model": self.wizard_data.get("model"),
|
|
838
|
+
"temperature": self.wizard_data.get("temperature", 0.7),
|
|
839
|
+
"max_tokens": self.wizard_data.get("max_tokens", 4096),
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
# Add base_url if it's a corporate configuration
|
|
843
|
+
if self.wizard_data.get("base_url"):
|
|
844
|
+
provider_cfg["base_url"] = self.wizard_data.get("base_url")
|
|
845
|
+
|
|
846
|
+
# Save provider configuration
|
|
847
|
+
global_config_data["ai"]["providers"][provider_id] = provider_cfg
|
|
848
|
+
|
|
849
|
+
# Set as default if it's the first provider
|
|
850
|
+
if len(global_config_data["ai"]["providers"]) == 1:
|
|
851
|
+
global_config_data["ai"]["default"] = provider_id
|
|
852
|
+
elif "default" not in global_config_data["ai"]:
|
|
853
|
+
global_config_data["ai"]["default"] = provider_id
|
|
854
|
+
|
|
855
|
+
# Save to disk
|
|
856
|
+
logger.debug(f"AI wizard - Saving to {global_config_path}")
|
|
857
|
+
logger.debug(f"AI wizard - Config data: {global_config_data}")
|
|
858
|
+
global_config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
859
|
+
with open(global_config_path, "wb") as f:
|
|
860
|
+
tomli_w.dump(global_config_data, f)
|
|
861
|
+
logger.debug("AI wizard - Config saved successfully")
|
|
862
|
+
|
|
863
|
+
# Save API key to secrets
|
|
864
|
+
secrets = SecretManager()
|
|
865
|
+
api_key = self.wizard_data.get("api_key")
|
|
866
|
+
secrets.set(f"{provider_id}_api_key", api_key, scope="user")
|
|
867
|
+
logger.debug("AI wizard - API key saved to secrets")
|
|
868
|
+
|
|
869
|
+
# Show success message
|
|
870
|
+
self.app.notify(f"AI provider '{provider_name}' configured successfully!", severity="information")
|
|
871
|
+
|
|
872
|
+
# Close wizard and trigger callback
|
|
873
|
+
logger.debug("AI wizard - Dismissing with result=True")
|
|
874
|
+
self.dismiss(result=True)
|
|
875
|
+
|
|
876
|
+
except Exception as e:
|
|
877
|
+
logger.error(f"AI wizard - Error saving: {e}", exc_info=True)
|
|
878
|
+
self.app.notify(f"Failed to save configuration: {e}", severity="error")
|
|
879
|
+
|
|
880
|
+
def action_back(self) -> None:
|
|
881
|
+
"""Cancel and go back."""
|
|
882
|
+
self.dismiss(result=False)
|