shotgun-sh 0.2.1.dev3__py3-none-any.whl → 0.2.1.dev5__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 shotgun-sh might be problematic. Click here for more details.
- shotgun/agents/config/constants.py +2 -1
- shotgun/agents/config/manager.py +90 -19
- shotgun/agents/config/models.py +11 -2
- shotgun/agents/config/provider.py +2 -1
- shotgun/cli/config.py +6 -6
- shotgun/cli/feedback.py +4 -2
- shotgun/codebase/core/ingestor.py +25 -5
- shotgun/posthog_telemetry.py +10 -8
- shotgun/sentry_telemetry.py +3 -3
- shotgun/shotgun_web/__init__.py +19 -0
- shotgun/shotgun_web/client.py +138 -0
- shotgun/shotgun_web/constants.py +17 -0
- shotgun/shotgun_web/models.py +47 -0
- shotgun/telemetry.py +7 -4
- shotgun/tui/app.py +24 -8
- shotgun/tui/screens/chat_screen/command_providers.py +6 -4
- shotgun/tui/screens/feedback.py +2 -2
- shotgun/tui/screens/model_picker.py +92 -17
- shotgun/tui/screens/provider_config.py +68 -2
- shotgun/tui/screens/shotgun_auth.py +295 -0
- shotgun/tui/screens/welcome.py +176 -0
- {shotgun_sh-0.2.1.dev3.dist-info → shotgun_sh-0.2.1.dev5.dist-info}/METADATA +1 -1
- {shotgun_sh-0.2.1.dev3.dist-info → shotgun_sh-0.2.1.dev5.dist-info}/RECORD +26 -20
- {shotgun_sh-0.2.1.dev3.dist-info → shotgun_sh-0.2.1.dev5.dist-info}/WHEEL +0 -0
- {shotgun_sh-0.2.1.dev3.dist-info → shotgun_sh-0.2.1.dev5.dist-info}/entry_points.txt +0 -0
- {shotgun_sh-0.2.1.dev3.dist-info → shotgun_sh-0.2.1.dev5.dist-info}/licenses/LICENSE +0 -0
shotgun/tui/app.py
CHANGED
|
@@ -8,6 +8,7 @@ from textual.screen import Screen
|
|
|
8
8
|
from shotgun.agents.config import ConfigManager, get_config_manager
|
|
9
9
|
from shotgun.logging_config import get_logger
|
|
10
10
|
from shotgun.tui.screens.splash import SplashScreen
|
|
11
|
+
from shotgun.utils.env_utils import is_shotgun_account_enabled
|
|
11
12
|
from shotgun.utils.file_system_utils import get_shotgun_base_path
|
|
12
13
|
from shotgun.utils.update_checker import perform_auto_update_async
|
|
13
14
|
|
|
@@ -16,6 +17,7 @@ from .screens.directory_setup import DirectorySetupScreen
|
|
|
16
17
|
from .screens.feedback import FeedbackScreen
|
|
17
18
|
from .screens.model_picker import ModelPickerScreen
|
|
18
19
|
from .screens.provider_config import ProviderConfigScreen
|
|
20
|
+
from .screens.welcome import WelcomeScreen
|
|
19
21
|
|
|
20
22
|
logger = get_logger(__name__)
|
|
21
23
|
|
|
@@ -60,20 +62,34 @@ class ShotgunApp(App[None]):
|
|
|
60
62
|
def refresh_startup_screen(self) -> None:
|
|
61
63
|
"""Push the appropriate screen based on configured providers."""
|
|
62
64
|
if not self.config_manager.has_any_provider_key():
|
|
63
|
-
|
|
65
|
+
# If Shotgun Account is enabled, show welcome screen with choice
|
|
66
|
+
# Otherwise, go directly to provider config (BYOK only)
|
|
67
|
+
if is_shotgun_account_enabled():
|
|
68
|
+
if isinstance(self.screen, WelcomeScreen):
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
self.push_screen(
|
|
72
|
+
WelcomeScreen(),
|
|
73
|
+
callback=lambda _arg: self.refresh_startup_screen(),
|
|
74
|
+
)
|
|
75
|
+
return
|
|
76
|
+
else:
|
|
77
|
+
if isinstance(self.screen, ProviderConfigScreen):
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
self.push_screen(
|
|
81
|
+
ProviderConfigScreen(),
|
|
82
|
+
callback=lambda _arg: self.refresh_startup_screen(),
|
|
83
|
+
)
|
|
64
84
|
return
|
|
65
|
-
|
|
66
|
-
self.push_screen(
|
|
67
|
-
"provider_config", callback=lambda _arg: self.refresh_startup_screen()
|
|
68
|
-
)
|
|
69
|
-
return
|
|
70
85
|
|
|
71
86
|
if not self.check_local_shotgun_directory_exists():
|
|
72
87
|
if isinstance(self.screen, DirectorySetupScreen):
|
|
73
88
|
return
|
|
74
89
|
|
|
75
90
|
self.push_screen(
|
|
76
|
-
|
|
91
|
+
DirectorySetupScreen(),
|
|
92
|
+
callback=lambda _arg: self.refresh_startup_screen(),
|
|
77
93
|
)
|
|
78
94
|
return
|
|
79
95
|
|
|
@@ -110,7 +126,7 @@ class ShotgunApp(App[None]):
|
|
|
110
126
|
submit_feedback_survey(feedback)
|
|
111
127
|
self.notify("Feedback sent. Thank you!")
|
|
112
128
|
|
|
113
|
-
self.push_screen(
|
|
129
|
+
self.push_screen(FeedbackScreen(), callback=handle_feedback)
|
|
114
130
|
|
|
115
131
|
|
|
116
132
|
def run(no_update_check: bool = False, continue_session: bool = False) -> None:
|
|
@@ -5,6 +5,8 @@ from textual.command import DiscoveryHit, Hit, Provider
|
|
|
5
5
|
|
|
6
6
|
from shotgun.agents.models import AgentType
|
|
7
7
|
from shotgun.codebase.models import CodebaseGraph
|
|
8
|
+
from shotgun.tui.screens.model_picker import ModelPickerScreen
|
|
9
|
+
from shotgun.tui.screens.provider_config import ProviderConfigScreen
|
|
8
10
|
|
|
9
11
|
if TYPE_CHECKING:
|
|
10
12
|
from shotgun.tui.screens.chat import ChatScreen
|
|
@@ -139,11 +141,11 @@ class ProviderSetupProvider(Provider):
|
|
|
139
141
|
|
|
140
142
|
def open_provider_config(self) -> None:
|
|
141
143
|
"""Show the provider configuration screen."""
|
|
142
|
-
self.chat_screen.app.push_screen(
|
|
144
|
+
self.chat_screen.app.push_screen(ProviderConfigScreen())
|
|
143
145
|
|
|
144
146
|
def open_model_picker(self) -> None:
|
|
145
147
|
"""Show the model picker screen."""
|
|
146
|
-
self.chat_screen.app.push_screen(
|
|
148
|
+
self.chat_screen.app.push_screen(ModelPickerScreen())
|
|
147
149
|
|
|
148
150
|
async def discover(self) -> AsyncGenerator[DiscoveryHit, None]:
|
|
149
151
|
yield DiscoveryHit(
|
|
@@ -282,11 +284,11 @@ class UnifiedCommandProvider(Provider):
|
|
|
282
284
|
|
|
283
285
|
def open_provider_config(self) -> None:
|
|
284
286
|
"""Show the provider configuration screen."""
|
|
285
|
-
self.chat_screen.app.push_screen(
|
|
287
|
+
self.chat_screen.app.push_screen(ProviderConfigScreen())
|
|
286
288
|
|
|
287
289
|
def open_model_picker(self) -> None:
|
|
288
290
|
"""Show the model picker screen."""
|
|
289
|
-
self.chat_screen.app.push_screen(
|
|
291
|
+
self.chat_screen.app.push_screen(ModelPickerScreen())
|
|
290
292
|
|
|
291
293
|
async def discover(self) -> AsyncGenerator[DiscoveryHit, None]:
|
|
292
294
|
"""Provide commands in alphabetical order when palette opens."""
|
shotgun/tui/screens/feedback.py
CHANGED
|
@@ -182,12 +182,12 @@ class FeedbackScreen(Screen[Feedback | None]):
|
|
|
182
182
|
return
|
|
183
183
|
|
|
184
184
|
app = cast("ShotgunApp", self.app)
|
|
185
|
-
|
|
185
|
+
shotgun_instance_id = app.config_manager.get_shotgun_instance_id()
|
|
186
186
|
|
|
187
187
|
feedback = Feedback(
|
|
188
188
|
kind=self.selected_kind,
|
|
189
189
|
description=description,
|
|
190
|
-
|
|
190
|
+
shotgun_instance_id=shotgun_instance_id,
|
|
191
191
|
)
|
|
192
192
|
|
|
193
193
|
self.dismiss(feedback)
|
|
@@ -12,11 +12,14 @@ from textual.screen import Screen
|
|
|
12
12
|
from textual.widgets import Button, Label, ListItem, ListView, Static
|
|
13
13
|
|
|
14
14
|
from shotgun.agents.config import ConfigManager
|
|
15
|
-
from shotgun.agents.config.models import MODEL_SPECS, ModelName
|
|
15
|
+
from shotgun.agents.config.models import MODEL_SPECS, ModelName, ShotgunConfig
|
|
16
|
+
from shotgun.logging_config import get_logger
|
|
16
17
|
|
|
17
18
|
if TYPE_CHECKING:
|
|
18
19
|
from ..app import ShotgunApp
|
|
19
20
|
|
|
21
|
+
logger = get_logger(__name__)
|
|
22
|
+
|
|
20
23
|
|
|
21
24
|
# Available models for selection
|
|
22
25
|
AVAILABLE_MODELS = list(ModelName)
|
|
@@ -82,20 +85,52 @@ class ModelPickerScreen(Screen[None]):
|
|
|
82
85
|
"Select the AI model you want to use for your tasks.",
|
|
83
86
|
id="model-picker-summary",
|
|
84
87
|
)
|
|
85
|
-
yield ListView(
|
|
88
|
+
yield ListView(id="model-list")
|
|
86
89
|
with Horizontal(id="model-actions"):
|
|
87
90
|
yield Button("Select \\[ENTER]", variant="primary", id="select")
|
|
88
91
|
yield Button("Done \\[ESC]", id="done")
|
|
89
92
|
|
|
90
|
-
def
|
|
91
|
-
|
|
93
|
+
def _rebuild_model_list(self) -> None:
|
|
94
|
+
"""Rebuild the model list from current config.
|
|
95
|
+
|
|
96
|
+
This method is called both on first show and when screen is resumed
|
|
97
|
+
to ensure the list always reflects the current configuration.
|
|
98
|
+
"""
|
|
99
|
+
logger.debug("Rebuilding model list from current config")
|
|
100
|
+
|
|
101
|
+
# Load current config with force_reload to get latest API keys
|
|
92
102
|
config_manager = self.config_manager
|
|
93
|
-
config = config_manager.load()
|
|
103
|
+
config = config_manager.load(force_reload=True)
|
|
104
|
+
|
|
105
|
+
# Log provider key status
|
|
106
|
+
logger.debug(
|
|
107
|
+
"Provider keys: openai=%s, anthropic=%s, google=%s, shotgun=%s",
|
|
108
|
+
config_manager._provider_has_api_key(config.openai),
|
|
109
|
+
config_manager._provider_has_api_key(config.anthropic),
|
|
110
|
+
config_manager._provider_has_api_key(config.google),
|
|
111
|
+
config_manager._provider_has_api_key(config.shotgun),
|
|
112
|
+
)
|
|
113
|
+
|
|
94
114
|
current_model = config.selected_model or ModelName.CLAUDE_SONNET_4_5
|
|
95
115
|
self.selected_model = current_model
|
|
116
|
+
logger.debug("Current selected model: %s", current_model)
|
|
96
117
|
|
|
97
|
-
#
|
|
118
|
+
# Rebuild the model list with current available models
|
|
98
119
|
list_view = self.query_one(ListView)
|
|
120
|
+
|
|
121
|
+
# Remove all existing items
|
|
122
|
+
old_count = len(list(list_view.children))
|
|
123
|
+
for child in list(list_view.children):
|
|
124
|
+
child.remove()
|
|
125
|
+
logger.debug("Removed %d existing model items from list", old_count)
|
|
126
|
+
|
|
127
|
+
# Add new items (labels already have correct text including current indicator)
|
|
128
|
+
new_items = self._build_model_items(config)
|
|
129
|
+
for item in new_items:
|
|
130
|
+
list_view.append(item)
|
|
131
|
+
logger.debug("Added %d available model items to list", len(new_items))
|
|
132
|
+
|
|
133
|
+
# Find and highlight current selection (if it's in the filtered list)
|
|
99
134
|
if list_view.children:
|
|
100
135
|
for i, child in enumerate(list_view.children):
|
|
101
136
|
if isinstance(child, ListItem) and child.id:
|
|
@@ -106,7 +141,20 @@ class ModelPickerScreen(Screen[None]):
|
|
|
106
141
|
if model_name == current_model:
|
|
107
142
|
list_view.index = i
|
|
108
143
|
break
|
|
109
|
-
|
|
144
|
+
|
|
145
|
+
def on_show(self) -> None:
|
|
146
|
+
"""Rebuild model list when screen is first shown."""
|
|
147
|
+
logger.debug("ModelPickerScreen.on_show() called")
|
|
148
|
+
self._rebuild_model_list()
|
|
149
|
+
|
|
150
|
+
def on_screenresume(self) -> None:
|
|
151
|
+
"""Rebuild model list when screen is resumed (subsequent visits).
|
|
152
|
+
|
|
153
|
+
This is called when returning to the screen after it was suspended,
|
|
154
|
+
ensuring the model list reflects any config changes made while away.
|
|
155
|
+
"""
|
|
156
|
+
logger.debug("ModelPickerScreen.on_screenresume() called")
|
|
157
|
+
self._rebuild_model_list()
|
|
110
158
|
|
|
111
159
|
def action_done(self) -> None:
|
|
112
160
|
self.dismiss()
|
|
@@ -138,13 +186,19 @@ class ModelPickerScreen(Screen[None]):
|
|
|
138
186
|
return app.config_manager
|
|
139
187
|
|
|
140
188
|
def refresh_model_labels(self) -> None:
|
|
141
|
-
"""Update the list view entries to reflect current selection.
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
)
|
|
189
|
+
"""Update the list view entries to reflect current selection.
|
|
190
|
+
|
|
191
|
+
Note: This method only updates labels for currently displayed models.
|
|
192
|
+
To rebuild the entire list after provider changes, on_show() should be used.
|
|
193
|
+
"""
|
|
194
|
+
# Load config once with force_reload
|
|
195
|
+
config = self.config_manager.load(force_reload=True)
|
|
196
|
+
current_model = config.selected_model or ModelName.CLAUDE_SONNET_4_5
|
|
197
|
+
|
|
145
198
|
# Update labels for available models only
|
|
146
199
|
for model_name in AVAILABLE_MODELS:
|
|
147
|
-
|
|
200
|
+
# Pass config to avoid multiple force reloads
|
|
201
|
+
if not self._is_model_available(model_name, config):
|
|
148
202
|
continue
|
|
149
203
|
label = self.query_one(
|
|
150
204
|
f"#label-{_sanitize_model_name_for_id(model_name)}", Label
|
|
@@ -153,12 +207,15 @@ class ModelPickerScreen(Screen[None]):
|
|
|
153
207
|
self._model_label(model_name, is_current=model_name == current_model)
|
|
154
208
|
)
|
|
155
209
|
|
|
156
|
-
def _build_model_items(self) -> list[ListItem]:
|
|
210
|
+
def _build_model_items(self, config: ShotgunConfig | None = None) -> list[ListItem]:
|
|
211
|
+
if config is None:
|
|
212
|
+
config = self.config_manager.load(force_reload=True)
|
|
213
|
+
|
|
157
214
|
items: list[ListItem] = []
|
|
158
215
|
current_model = self.selected_model
|
|
159
216
|
for model_name in AVAILABLE_MODELS:
|
|
160
217
|
# Only add models that are available
|
|
161
|
-
if not self._is_model_available(model_name):
|
|
218
|
+
if not self._is_model_available(model_name, config):
|
|
162
219
|
continue
|
|
163
220
|
|
|
164
221
|
label = Label(
|
|
@@ -181,7 +238,9 @@ class ModelPickerScreen(Screen[None]):
|
|
|
181
238
|
return model_name
|
|
182
239
|
return None
|
|
183
240
|
|
|
184
|
-
def _is_model_available(
|
|
241
|
+
def _is_model_available(
|
|
242
|
+
self, model_name: ModelName, config: ShotgunConfig | None = None
|
|
243
|
+
) -> bool:
|
|
185
244
|
"""Check if a model is available based on provider key configuration.
|
|
186
245
|
|
|
187
246
|
A model is available if:
|
|
@@ -190,22 +249,38 @@ class ModelPickerScreen(Screen[None]):
|
|
|
190
249
|
|
|
191
250
|
Args:
|
|
192
251
|
model_name: The model to check availability for
|
|
252
|
+
config: Optional pre-loaded config to avoid multiple reloads
|
|
193
253
|
|
|
194
254
|
Returns:
|
|
195
255
|
True if the model can be used, False otherwise
|
|
196
256
|
"""
|
|
197
|
-
config
|
|
257
|
+
if config is None:
|
|
258
|
+
config = self.config_manager.load(force_reload=True)
|
|
198
259
|
|
|
199
260
|
# If Shotgun Account is configured, all models are available
|
|
200
261
|
if self.config_manager._provider_has_api_key(config.shotgun):
|
|
262
|
+
logger.debug("Model %s available (Shotgun Account configured)", model_name)
|
|
201
263
|
return True
|
|
202
264
|
|
|
203
265
|
# In BYOK mode, check if the model's provider has a key
|
|
204
266
|
if model_name not in MODEL_SPECS:
|
|
267
|
+
logger.debug("Model %s not available (not in MODEL_SPECS)", model_name)
|
|
205
268
|
return False
|
|
206
269
|
|
|
207
270
|
spec = MODEL_SPECS[model_name]
|
|
208
|
-
|
|
271
|
+
# Check provider key directly using the loaded config to avoid stale cache
|
|
272
|
+
provider_config = self.config_manager._get_provider_config(
|
|
273
|
+
config, spec.provider
|
|
274
|
+
)
|
|
275
|
+
has_key = self.config_manager._provider_has_api_key(provider_config)
|
|
276
|
+
logger.debug(
|
|
277
|
+
"Model %s available=%s (provider=%s, has_key=%s)",
|
|
278
|
+
model_name,
|
|
279
|
+
has_key,
|
|
280
|
+
spec.provider,
|
|
281
|
+
has_key,
|
|
282
|
+
)
|
|
283
|
+
return has_key
|
|
209
284
|
|
|
210
285
|
def _model_label(self, model_name: ModelName, is_current: bool) -> str:
|
|
211
286
|
"""Generate label for model with specs and current indicator."""
|
|
@@ -85,6 +85,7 @@ class ProviderConfigScreen(Screen[None]):
|
|
|
85
85
|
|
|
86
86
|
BINDINGS = [
|
|
87
87
|
("escape", "done", "Back"),
|
|
88
|
+
("ctrl+c", "app.quit", "Quit"),
|
|
88
89
|
]
|
|
89
90
|
|
|
90
91
|
selected_provider: reactive[str] = reactive("openai")
|
|
@@ -108,17 +109,30 @@ class ProviderConfigScreen(Screen[None]):
|
|
|
108
109
|
)
|
|
109
110
|
with Horizontal(id="provider-actions"):
|
|
110
111
|
yield Button("Save key \\[ENTER]", variant="primary", id="save")
|
|
112
|
+
yield Button("Authenticate", variant="success", id="authenticate")
|
|
111
113
|
yield Button("Clear key", id="clear", variant="warning")
|
|
112
114
|
yield Button("Done \\[ESC]", id="done")
|
|
113
115
|
|
|
114
116
|
def on_mount(self) -> None:
|
|
115
117
|
self.refresh_provider_status()
|
|
118
|
+
self._update_done_button_visibility()
|
|
116
119
|
list_view = self.query_one(ListView)
|
|
117
120
|
if list_view.children:
|
|
118
121
|
list_view.index = 0
|
|
119
122
|
self.selected_provider = "openai"
|
|
123
|
+
|
|
124
|
+
# Hide authenticate button by default (shown only for shotgun)
|
|
125
|
+
self.query_one("#authenticate", Button).display = False
|
|
120
126
|
self.set_focus(self.query_one("#api-key", Input))
|
|
121
127
|
|
|
128
|
+
def on_screenresume(self) -> None:
|
|
129
|
+
"""Refresh provider status when screen is resumed.
|
|
130
|
+
|
|
131
|
+
This ensures the UI reflects any provider changes made elsewhere.
|
|
132
|
+
"""
|
|
133
|
+
self.refresh_provider_status()
|
|
134
|
+
self._update_done_button_visibility()
|
|
135
|
+
|
|
122
136
|
def action_done(self) -> None:
|
|
123
137
|
self.dismiss()
|
|
124
138
|
|
|
@@ -139,6 +153,10 @@ class ProviderConfigScreen(Screen[None]):
|
|
|
139
153
|
def _on_save_pressed(self) -> None:
|
|
140
154
|
self._save_api_key()
|
|
141
155
|
|
|
156
|
+
@on(Button.Pressed, "#authenticate")
|
|
157
|
+
def _on_authenticate_pressed(self) -> None:
|
|
158
|
+
self.run_worker(self._start_shotgun_auth(), exclusive=True)
|
|
159
|
+
|
|
142
160
|
@on(Button.Pressed, "#clear")
|
|
143
161
|
def _on_clear_pressed(self) -> None:
|
|
144
162
|
self._clear_api_key()
|
|
@@ -155,9 +173,31 @@ class ProviderConfigScreen(Screen[None]):
|
|
|
155
173
|
def watch_selected_provider(self, provider: ProviderType) -> None:
|
|
156
174
|
if not self.is_mounted:
|
|
157
175
|
return
|
|
176
|
+
|
|
177
|
+
# Show/hide UI elements based on provider type
|
|
178
|
+
is_shotgun = provider == "shotgun"
|
|
179
|
+
|
|
158
180
|
input_widget = self.query_one("#api-key", Input)
|
|
159
|
-
|
|
160
|
-
|
|
181
|
+
save_button = self.query_one("#save", Button)
|
|
182
|
+
auth_button = self.query_one("#authenticate", Button)
|
|
183
|
+
|
|
184
|
+
if is_shotgun:
|
|
185
|
+
# Hide API key input and save button
|
|
186
|
+
input_widget.display = False
|
|
187
|
+
save_button.display = False
|
|
188
|
+
|
|
189
|
+
# Only show Authenticate button if shotgun is NOT already configured
|
|
190
|
+
if self._has_provider_key("shotgun"):
|
|
191
|
+
auth_button.display = False
|
|
192
|
+
else:
|
|
193
|
+
auth_button.display = True
|
|
194
|
+
else:
|
|
195
|
+
# Show API key input and save button, hide authenticate button
|
|
196
|
+
input_widget.display = True
|
|
197
|
+
save_button.display = True
|
|
198
|
+
auth_button.display = False
|
|
199
|
+
input_widget.placeholder = self._input_placeholder(provider)
|
|
200
|
+
input_widget.value = ""
|
|
161
201
|
|
|
162
202
|
@property
|
|
163
203
|
def config_manager(self) -> ConfigManager:
|
|
@@ -170,6 +210,12 @@ class ProviderConfigScreen(Screen[None]):
|
|
|
170
210
|
label = self.query_one(f"#label-{provider_id}", Label)
|
|
171
211
|
label.update(self._provider_label(provider_id))
|
|
172
212
|
|
|
213
|
+
def _update_done_button_visibility(self) -> None:
|
|
214
|
+
"""Show/hide Done button based on whether any provider keys are configured."""
|
|
215
|
+
done_button = self.query_one("#done", Button)
|
|
216
|
+
has_keys = self.config_manager.has_any_provider_key()
|
|
217
|
+
done_button.display = has_keys
|
|
218
|
+
|
|
173
219
|
def _build_provider_items(self) -> list[ListItem]:
|
|
174
220
|
items: list[ListItem] = []
|
|
175
221
|
for provider_id in get_configurable_providers():
|
|
@@ -235,6 +281,7 @@ class ProviderConfigScreen(Screen[None]):
|
|
|
235
281
|
|
|
236
282
|
input_widget.value = ""
|
|
237
283
|
self.refresh_provider_status()
|
|
284
|
+
self._update_done_button_visibility()
|
|
238
285
|
self.notify(
|
|
239
286
|
f"Saved API key for {self._provider_display_name(self.selected_provider)}."
|
|
240
287
|
)
|
|
@@ -247,7 +294,26 @@ class ProviderConfigScreen(Screen[None]):
|
|
|
247
294
|
return
|
|
248
295
|
|
|
249
296
|
self.refresh_provider_status()
|
|
297
|
+
self._update_done_button_visibility()
|
|
250
298
|
self.query_one("#api-key", Input).value = ""
|
|
299
|
+
|
|
300
|
+
# If we just cleared shotgun, show the Authenticate button
|
|
301
|
+
if self.selected_provider == "shotgun":
|
|
302
|
+
auth_button = self.query_one("#authenticate", Button)
|
|
303
|
+
auth_button.display = True
|
|
304
|
+
|
|
251
305
|
self.notify(
|
|
252
306
|
f"Cleared API key for {self._provider_display_name(self.selected_provider)}."
|
|
253
307
|
)
|
|
308
|
+
|
|
309
|
+
async def _start_shotgun_auth(self) -> None:
|
|
310
|
+
"""Launch Shotgun Account authentication flow."""
|
|
311
|
+
from .shotgun_auth import ShotgunAuthScreen
|
|
312
|
+
|
|
313
|
+
# Push the auth screen and wait for result
|
|
314
|
+
result = await self.app.push_screen_wait(ShotgunAuthScreen())
|
|
315
|
+
|
|
316
|
+
# Refresh provider status after auth completes
|
|
317
|
+
if result:
|
|
318
|
+
self.refresh_provider_status()
|
|
319
|
+
# Notify handled by auth screen
|