shotgun-sh 0.2.11.dev3__py3-none-any.whl → 0.2.19__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.

Files changed (39) hide show
  1. shotgun/agents/agent_manager.py +66 -12
  2. shotgun/agents/config/README.md +89 -0
  3. shotgun/agents/config/__init__.py +10 -1
  4. shotgun/agents/config/manager.py +287 -32
  5. shotgun/agents/config/models.py +21 -1
  6. shotgun/agents/config/provider.py +27 -0
  7. shotgun/agents/config/streaming_test.py +119 -0
  8. shotgun/agents/conversation_manager.py +14 -7
  9. shotgun/agents/history/history_processors.py +99 -3
  10. shotgun/agents/history/token_counting/openai.py +3 -1
  11. shotgun/build_constants.py +3 -3
  12. shotgun/exceptions.py +32 -0
  13. shotgun/logging_config.py +42 -0
  14. shotgun/main.py +2 -0
  15. shotgun/posthog_telemetry.py +18 -25
  16. shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +3 -2
  17. shotgun/sentry_telemetry.py +157 -1
  18. shotgun/settings.py +5 -0
  19. shotgun/tui/app.py +16 -15
  20. shotgun/tui/screens/chat/chat_screen.py +156 -61
  21. shotgun/tui/screens/chat_screen/command_providers.py +13 -2
  22. shotgun/tui/screens/chat_screen/history/chat_history.py +1 -2
  23. shotgun/tui/screens/directory_setup.py +14 -5
  24. shotgun/tui/screens/feedback.py +10 -3
  25. shotgun/tui/screens/github_issue.py +111 -0
  26. shotgun/tui/screens/model_picker.py +8 -1
  27. shotgun/tui/screens/onboarding.py +431 -0
  28. shotgun/tui/screens/pipx_migration.py +12 -6
  29. shotgun/tui/screens/provider_config.py +25 -8
  30. shotgun/tui/screens/shotgun_auth.py +0 -10
  31. shotgun/tui/screens/welcome.py +32 -0
  32. shotgun/tui/services/conversation_service.py +8 -6
  33. shotgun/tui/widgets/widget_coordinator.py +3 -2
  34. shotgun_sh-0.2.19.dist-info/METADATA +465 -0
  35. {shotgun_sh-0.2.11.dev3.dist-info → shotgun_sh-0.2.19.dist-info}/RECORD +38 -33
  36. shotgun_sh-0.2.11.dev3.dist-info/METADATA +0 -130
  37. {shotgun_sh-0.2.11.dev3.dist-info → shotgun_sh-0.2.19.dist-info}/WHEEL +0 -0
  38. {shotgun_sh-0.2.11.dev3.dist-info → shotgun_sh-0.2.19.dist-info}/entry_points.txt +0 -0
  39. {shotgun_sh-0.2.11.dev3.dist-info → shotgun_sh-0.2.19.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,431 @@
1
+ """Onboarding popup modal for first-time users."""
2
+
3
+ import webbrowser
4
+
5
+ from textual import on
6
+ from textual.app import ComposeResult
7
+ from textual.containers import Container, Horizontal, VerticalScroll
8
+ from textual.screen import ModalScreen
9
+ from textual.widgets import Button, Markdown, Static
10
+
11
+
12
+ class OnboardingModal(ModalScreen[None]):
13
+ """Multi-page onboarding modal for new users.
14
+
15
+ This modal presents helpful resources and tips for using Shotgun across
16
+ multiple pages. Users can navigate between pages using Next/Back buttons.
17
+ """
18
+
19
+ CSS = """
20
+ OnboardingModal {
21
+ align: center middle;
22
+ }
23
+
24
+ #onboarding-container {
25
+ width: 95;
26
+ max-width: 100;
27
+ height: auto;
28
+ max-height: 90%;
29
+ border: thick $primary;
30
+ background: $surface;
31
+ padding: 2;
32
+ }
33
+
34
+ #progress-sidebar {
35
+ width: 26;
36
+ dock: left;
37
+ border-right: solid $primary;
38
+ padding: 1;
39
+ height: 100%;
40
+ }
41
+
42
+ #main-content {
43
+ width: 1fr;
44
+ height: auto;
45
+ }
46
+
47
+ #progress-header {
48
+ text-style: bold;
49
+ padding-bottom: 1;
50
+ color: $text-accent;
51
+ }
52
+
53
+ .progress-item {
54
+ padding: 1 0;
55
+ }
56
+
57
+ .progress-item-current {
58
+ color: $accent;
59
+ text-style: bold;
60
+ }
61
+
62
+ .progress-item-visited {
63
+ color: $success;
64
+ }
65
+
66
+ .progress-item-unvisited {
67
+ color: $text-muted;
68
+ }
69
+
70
+ #onboarding-header {
71
+ text-style: bold;
72
+ color: $text-accent;
73
+ padding-bottom: 1;
74
+ text-align: center;
75
+ }
76
+
77
+ #onboarding-content {
78
+ height: 1fr;
79
+ padding: 1 0;
80
+ }
81
+
82
+ #page-indicator {
83
+ text-align: center;
84
+ color: $text-muted;
85
+ padding: 1 0;
86
+ }
87
+
88
+ #buttons-container {
89
+ height: auto;
90
+ padding: 1 0 0 0;
91
+ }
92
+
93
+ #navigation-buttons {
94
+ width: 100%;
95
+ height: auto;
96
+ align: center middle;
97
+ }
98
+
99
+ .nav-button {
100
+ margin: 0 1;
101
+ min-width: 12;
102
+ }
103
+
104
+ #resource-sections {
105
+ padding: 1 0;
106
+ height: auto;
107
+ }
108
+
109
+ #resource-sections Button {
110
+ width: 100%;
111
+ margin: 0 0 2 0;
112
+ }
113
+
114
+ #video-section {
115
+ padding: 0;
116
+ margin: 0 0 1 0;
117
+ }
118
+
119
+ #docs-section {
120
+ padding: 0;
121
+ margin: 2 0 1 0;
122
+ }
123
+ """
124
+
125
+ BINDINGS = [
126
+ ("escape", "dismiss", "Close"),
127
+ ("ctrl+c", "app.quit", "Quit"),
128
+ ]
129
+
130
+ def __init__(self) -> None:
131
+ """Initialize the onboarding modal."""
132
+ super().__init__()
133
+ self.current_page = 0
134
+ self.total_pages = 4
135
+ self.page_titles = [
136
+ "Getting Started",
137
+ "Discovering the 5 Modes",
138
+ "Prompting Better",
139
+ "Context Management!",
140
+ ]
141
+ # Track which pages have been visited (in memory only)
142
+ self.visited_pages: set[int] = {0} # Start on page 0, so it's visited
143
+
144
+ def compose(self) -> ComposeResult:
145
+ """Compose the onboarding modal."""
146
+ with Container(id="onboarding-container"):
147
+ # Left sidebar for progress tracking
148
+ with Container(id="progress-sidebar"):
149
+ yield Static("Progress", id="progress-header")
150
+ for i in range(self.total_pages):
151
+ yield Static(
152
+ f"{i + 1}. {self.page_titles[i]}",
153
+ id=f"progress-item-{i}",
154
+ classes="progress-item",
155
+ )
156
+
157
+ # Main content area
158
+ with Container(id="main-content"):
159
+ yield Static("Welcome to Shotgun!", id="onboarding-header")
160
+ with VerticalScroll(id="onboarding-content"):
161
+ yield Markdown(id="page-content")
162
+ # Resource sections (only shown on page 1)
163
+ with Container(id="resource-sections"):
164
+ yield Markdown(
165
+ "### 🎥 Video Demo\nWatch our demo video to see Shotgun in action",
166
+ id="video-section",
167
+ )
168
+ yield Button(
169
+ "▶️ Watch Demo Video",
170
+ id="youtube-button",
171
+ variant="success",
172
+ )
173
+ yield Markdown(
174
+ "### 📖 Documentation\nRead the comprehensive usage guide for detailed instructions",
175
+ id="docs-section",
176
+ )
177
+ yield Button(
178
+ "📚 Read Usage Guide", id="usage-button", variant="primary"
179
+ )
180
+ yield Static(id="page-indicator")
181
+ with Container(id="buttons-container"):
182
+ with Horizontal(id="navigation-buttons"):
183
+ yield Button("Back", id="back-button", classes="nav-button")
184
+ yield Button(
185
+ "Next",
186
+ id="next-button",
187
+ classes="nav-button",
188
+ variant="primary",
189
+ )
190
+ yield Button("Close", id="close-button", classes="nav-button")
191
+
192
+ def on_mount(self) -> None:
193
+ """Set up the modal after mounting."""
194
+ self.update_page()
195
+
196
+ def update_page(self) -> None:
197
+ """Update the displayed page content and navigation buttons."""
198
+ # Mark current page as visited
199
+ self.visited_pages.add(self.current_page)
200
+
201
+ # Update page content
202
+ content_widget = self.query_one("#page-content", Markdown)
203
+ content_widget.update(self.get_page_content())
204
+
205
+ # Update page indicator
206
+ page_indicator = self.query_one("#page-indicator", Static)
207
+ page_indicator.update(f"Page {self.current_page + 1} of {self.total_pages}")
208
+
209
+ # Update progress sidebar
210
+ for i in range(self.total_pages):
211
+ progress_item = self.query_one(f"#progress-item-{i}", Static)
212
+ # Remove all progress classes first
213
+ progress_item.remove_class(
214
+ "progress-item-current",
215
+ "progress-item-visited",
216
+ "progress-item-unvisited",
217
+ )
218
+ # Add appropriate class
219
+ if i == self.current_page:
220
+ progress_item.add_class("progress-item-current")
221
+ progress_item.update(f"▶ {i + 1}. {self.page_titles[i]}")
222
+ elif i in self.visited_pages:
223
+ progress_item.add_class("progress-item-visited")
224
+ progress_item.update(f"✓ {i + 1}. {self.page_titles[i]}")
225
+ else:
226
+ progress_item.add_class("progress-item-unvisited")
227
+ progress_item.update(f" {i + 1}. {self.page_titles[i]}")
228
+
229
+ # Show/hide resource sections (only on page 1)
230
+ resource_sections = self.query_one("#resource-sections", Container)
231
+ resource_sections.display = self.current_page == 0
232
+
233
+ # Update button visibility and states
234
+ back_button = self.query_one("#back-button", Button)
235
+ next_button = self.query_one("#next-button", Button)
236
+
237
+ # Update back button label and state
238
+ if self.current_page == 0:
239
+ back_button.disabled = True
240
+ back_button.label = "Back"
241
+ else:
242
+ back_button.disabled = False
243
+ prev_title = self.page_titles[self.current_page - 1]
244
+ back_button.label = f"← {prev_title}"
245
+
246
+ # Update next button label
247
+ if self.current_page == self.total_pages - 1:
248
+ next_button.label = "Finish"
249
+ else:
250
+ next_title = self.page_titles[self.current_page + 1]
251
+ next_button.label = f"{next_title} (Next →)"
252
+
253
+ # Focus the appropriate button
254
+ if self.current_page == 0:
255
+ next_button.focus()
256
+ else:
257
+ next_button.focus()
258
+
259
+ # Scroll content to top
260
+ self.query_one("#onboarding-content", VerticalScroll).scroll_home(animate=False)
261
+
262
+ def get_page_content(self) -> str:
263
+ """Get the content for the current page."""
264
+ if self.current_page == 0:
265
+ return self._page_1_resources()
266
+ elif self.current_page == 1:
267
+ return self._page_2_modes()
268
+ elif self.current_page == 2:
269
+ return self._page_3_prompts()
270
+ else:
271
+ return self._page_4_context_management()
272
+
273
+ def _page_1_resources(self) -> str:
274
+ """Page 1: Helpful resources."""
275
+ return """
276
+ ## Getting Started Resources
277
+
278
+ Here are some helpful resources to get you up to speed with Shotgun:
279
+ """
280
+
281
+ def _page_2_modes(self) -> str:
282
+ """Page 2: Explanation of the 5 modes."""
283
+ return """
284
+ ## Understanding Shotgun's 5 Modes
285
+
286
+ Shotgun has 5 specialized modes, each designed for specific tasks. Each mode writes to its own dedicated file in `.shotgun/`:
287
+
288
+ ### 🔬 Research Mode
289
+ Research topics with web search and synthesize findings. Perfect for gathering information and exploring new concepts.
290
+
291
+ **Writes to:** `.shotgun/research.md`
292
+
293
+ ### 📝 Specify Mode
294
+ Create detailed specifications and requirements documents. Great for planning features and documenting requirements.
295
+
296
+ **Writes to:** `.shotgun/specification.md`
297
+
298
+ ### 📋 Plan Mode
299
+ Create comprehensive, actionable plans with milestones. Ideal for breaking down large projects into manageable steps.
300
+
301
+ **Writes to:** `.shotgun/plan.md`
302
+
303
+ ### ✅ Tasks Mode
304
+ Generate specific, actionable tasks from research and plans. Best for getting concrete next steps and action items.
305
+
306
+ **Writes to:** `.shotgun/tasks.md`
307
+
308
+ ### 📤 Export Mode
309
+ Export artifacts and findings to various formats. Creates documentation like Claude.md (AI instructions), Agent.md (agent specs), PRDs, and other deliverables. Can write to any file in `.shotgun/` except the mode-specific files above.
310
+
311
+ **Writes to:** `.shotgun/Claude.md`, `.shotgun/Agent.md`, `.shotgun/PRD.md`, etc.
312
+
313
+ ---
314
+
315
+ **Tip:** You can switch between modes using `Shift+Tab` or `Ctrl+P` to open the command palette!
316
+ """
317
+
318
+ def _page_3_prompts(self) -> str:
319
+ """Page 3: Tips for better prompts."""
320
+ return """
321
+ ## Writing Better Prompts
322
+
323
+ Here are some tips to get the best results from Shotgun:
324
+
325
+ ### 1. Ask for Research First
326
+ Before jumping into a task, ask Shotgun to research the codebase or topic:
327
+
328
+ > "Can you research how authentication works in this codebase?"
329
+
330
+ ### 2. Request Clarifying Questions
331
+ Let Shotgun ask you questions to better understand your needs:
332
+
333
+ > "I want to add user profiles. Please ask me clarifying questions before starting."
334
+
335
+ ### 3. Be Specific About Context
336
+ Provide relevant context about what you're trying to accomplish:
337
+
338
+ > "I'm working on the payment flow. I need to add support for refunds."
339
+
340
+ ### 4. Use the Right Mode
341
+ Switch to the appropriate mode for your task:
342
+ - Use **Research** for exploration
343
+ - Use **Specify** for requirements
344
+ - Use **Plan** for implementation strategy
345
+ - Use **Tasks** for actionable next steps
346
+
347
+ ---
348
+
349
+ **Remember:** Shotgun works best when you give it context and let it ask questions!
350
+ """
351
+
352
+ def _page_4_context_management(self) -> str:
353
+ """Page 4: Context management and conversation controls."""
354
+ return """
355
+ ## Managing Conversation Context
356
+
357
+ As conversations grow, you may need to manage the context sent to the AI model.
358
+
359
+ ### Clear Conversation
360
+ Completely start over with a fresh conversation.
361
+
362
+ **How to use:**
363
+ - Open Command Palette: `Ctrl+P`
364
+ - Type: "Clear Conversation"
365
+ - Confirm the action
366
+
367
+ **When to use:**
368
+ - Starting a completely new task or project
369
+ - When you want a clean slate
370
+ - Context has become too cluttered
371
+
372
+ ---
373
+
374
+ ### Compact Conversation
375
+ Intelligently compress the conversation history while preserving important context.
376
+
377
+ **How to use:**
378
+ - Open Command Palette: `Ctrl+P`
379
+ - Type: "Compact Conversation"
380
+ - Shotgun will compress older messages automatically
381
+
382
+ **When to use:**
383
+ - Conversation is getting long but you want to keep context
384
+ - Running into token limits
385
+ - Want to reduce costs while maintaining continuity
386
+
387
+ **What it does:**
388
+ - Summarizes older messages
389
+ - Keeps recent messages intact
390
+ - Preserves key information and decisions
391
+
392
+ ---
393
+
394
+ **Tip:** Use `Ctrl+U` to view your current usage and see how much context you're using!
395
+ """
396
+
397
+ @on(Button.Pressed, "#back-button")
398
+ def handle_back(self) -> None:
399
+ """Handle back button press."""
400
+ if self.current_page > 0:
401
+ self.current_page -= 1
402
+ self.update_page()
403
+
404
+ @on(Button.Pressed, "#next-button")
405
+ def handle_next(self) -> None:
406
+ """Handle next/finish button press."""
407
+ if self.current_page < self.total_pages - 1:
408
+ self.current_page += 1
409
+ self.update_page()
410
+ else:
411
+ # On last page, finish closes the modal
412
+ self.dismiss()
413
+
414
+ @on(Button.Pressed, "#close-button")
415
+ def handle_close(self) -> None:
416
+ """Handle close button press."""
417
+ self.dismiss()
418
+
419
+ @on(Button.Pressed, "#youtube-button")
420
+ def handle_youtube(self) -> None:
421
+ """Open demo section in README."""
422
+ webbrowser.open(
423
+ "https://github.com/shotgun-sh/shotgun?tab=readme-ov-file#-demo"
424
+ )
425
+
426
+ @on(Button.Pressed, "#usage-button")
427
+ def handle_usage_guide(self) -> None:
428
+ """Open usage guide in browser."""
429
+ webbrowser.open(
430
+ "https://github.com/shotgun-sh/shotgun?tab=readme-ov-file#-usage"
431
+ )
@@ -8,7 +8,7 @@ from textual import on
8
8
  from textual.app import ComposeResult
9
9
  from textual.containers import Container, Horizontal, VerticalScroll
10
10
  from textual.screen import ModalScreen
11
- from textual.widgets import Button, Markdown
11
+ from textual.widgets import Button, Label, Markdown
12
12
 
13
13
  if TYPE_CHECKING:
14
14
  pass
@@ -51,6 +51,13 @@ class PipxMigrationScreen(ModalScreen[None]):
51
51
  margin: 0 1;
52
52
  min-width: 20;
53
53
  }
54
+
55
+ #migration-status {
56
+ height: auto;
57
+ padding: 1;
58
+ min-height: 1;
59
+ text-align: center;
60
+ }
54
61
  """
55
62
 
56
63
  BINDINGS = [
@@ -106,6 +113,7 @@ Or install permanently: `uv tool install shotgun-sh`
106
113
  )
107
114
 
108
115
  with Container(id="buttons-container"):
116
+ yield Label("", id="migration-status")
109
117
  with Horizontal(id="action-buttons"):
110
118
  yield Button(
111
119
  "Copy Instructions to Clipboard",
@@ -136,16 +144,14 @@ curl -LsSf https://astral.sh/uv/install.sh | sh
136
144
 
137
145
  # Step 3: Run shotgun with uvx
138
146
  uvx shotgun-sh"""
147
+ status_label = self.query_one("#migration-status", Label)
139
148
  try:
140
149
  import pyperclip # type: ignore[import-untyped] # noqa: PGH003
141
150
 
142
151
  pyperclip.copy(instructions)
143
- self.notify("Copied migration instructions to clipboard!")
152
+ status_label.update("Copied migration instructions to clipboard!")
144
153
  except ImportError:
145
- self.notify(
146
- "Clipboard not available. See instructions above.",
147
- severity="warning",
148
- )
154
+ status_label.update("⚠️ Clipboard not available. See instructions above.")
149
155
 
150
156
  @on(Button.Pressed, "#continue")
151
157
  def _continue(self) -> None:
@@ -77,6 +77,14 @@ class ProviderConfigScreen(Screen[None]):
77
77
  #provider-list {
78
78
  padding: 1;
79
79
  }
80
+ #provider-status {
81
+ height: auto;
82
+ padding: 0 1;
83
+ min-height: 1;
84
+ }
85
+ #provider-status.error {
86
+ color: $error;
87
+ }
80
88
  """
81
89
 
82
90
  BINDINGS = [
@@ -103,6 +111,7 @@ class ProviderConfigScreen(Screen[None]):
103
111
  password=True,
104
112
  id="api-key",
105
113
  )
114
+ yield Label("", id="provider-status")
106
115
  with Horizontal(id="provider-actions"):
107
116
  yield Button("Save key \\[ENTER]", variant="primary", id="save")
108
117
  yield Button("Authenticate", variant="success", id="authenticate")
@@ -280,9 +289,11 @@ class ProviderConfigScreen(Screen[None]):
280
289
  """Async implementation of API key saving."""
281
290
  input_widget = self.query_one("#api-key", Input)
282
291
  api_key = input_widget.value.strip()
292
+ status_label = self.query_one("#provider-status", Label)
283
293
 
284
294
  if not api_key:
285
- self.notify("Enter an API key before saving.", severity="error")
295
+ status_label.update("Enter an API key before saving.")
296
+ status_label.add_class("error")
286
297
  return
287
298
 
288
299
  try:
@@ -291,25 +302,29 @@ class ProviderConfigScreen(Screen[None]):
291
302
  api_key=api_key,
292
303
  )
293
304
  except Exception as exc: # pragma: no cover - defensive; textual path
294
- self.notify(f"Failed to save key: {exc}", severity="error")
305
+ status_label.update(f"Failed to save key: {exc}")
306
+ status_label.add_class("error")
295
307
  return
296
308
 
297
309
  input_widget.value = ""
298
310
  await self.refresh_provider_status()
299
311
  await self._update_done_button_visibility()
300
- self.notify(
301
- f"Saved API key for {self._provider_display_name(self.selected_provider)}."
312
+ status_label.update(
313
+ f"Saved API key for {self._provider_display_name(self.selected_provider)}."
302
314
  )
315
+ status_label.remove_class("error")
303
316
 
304
317
  def _clear_api_key(self) -> None:
305
318
  self.run_worker(self._do_clear_api_key(), exclusive=True)
306
319
 
307
320
  async def _do_clear_api_key(self) -> None:
308
321
  """Async implementation of API key clearing."""
322
+ status_label = self.query_one("#provider-status", Label)
309
323
  try:
310
324
  await self.config_manager.clear_provider_key(self.selected_provider)
311
325
  except Exception as exc: # pragma: no cover - defensive; textual path
312
- self.notify(f"Failed to clear key: {exc}", severity="error")
326
+ status_label.update(f"Failed to clear key: {exc}")
327
+ status_label.add_class("error")
313
328
  return
314
329
 
315
330
  await self.refresh_provider_status()
@@ -321,9 +336,10 @@ class ProviderConfigScreen(Screen[None]):
321
336
  auth_button = self.query_one("#authenticate", Button)
322
337
  auth_button.display = True
323
338
 
324
- self.notify(
325
- f"Cleared API key for {self._provider_display_name(self.selected_provider)}."
339
+ status_label.update(
340
+ f"Cleared API key for {self._provider_display_name(self.selected_provider)}."
326
341
  )
342
+ status_label.remove_class("error")
327
343
 
328
344
  async def _start_shotgun_auth(self) -> None:
329
345
  """Launch Shotgun Account authentication flow."""
@@ -335,4 +351,5 @@ class ProviderConfigScreen(Screen[None]):
335
351
  # Refresh provider status after auth completes
336
352
  if result:
337
353
  await self.refresh_provider_status()
338
- # Notify handled by auth screen
354
+ # Auto-dismiss provider config screen after successful auth
355
+ self.dismiss()
@@ -182,12 +182,10 @@ class ShotgunAuthScreen(Screen[bool]):
182
182
  self.query_one("#status", Label).update(
183
183
  f"❌ Error: Failed to create authentication token\n{e}"
184
184
  )
185
- self.notify("Failed to start authentication", severity="error")
186
185
 
187
186
  except Exception as e:
188
187
  logger.error("Unexpected error during auth flow: %s", e)
189
188
  self.query_one("#status", Label).update(f"❌ Unexpected error: {e}")
190
- self.notify("Authentication failed", severity="error")
191
189
 
192
190
  async def _poll_token_status(self) -> None:
193
191
  """Poll token status until completed or expired."""
@@ -224,17 +222,12 @@ class ShotgunAuthScreen(Screen[bool]):
224
222
  "✅ Authentication successful! Saving credentials..."
225
223
  )
226
224
  await asyncio.sleep(1)
227
- self.notify(
228
- "Shotgun Account configured successfully!",
229
- severity="information",
230
- )
231
225
  self.dismiss(True)
232
226
  else:
233
227
  logger.error("Completed but missing keys")
234
228
  self.query_one("#status", Label).update(
235
229
  "❌ Error: Authentication completed but keys are missing"
236
230
  )
237
- self.notify("Authentication failed", severity="error")
238
231
  await asyncio.sleep(3)
239
232
  self.dismiss(False)
240
233
  return
@@ -250,7 +243,6 @@ class ShotgunAuthScreen(Screen[bool]):
250
243
  "❌ Authentication token expired (30 minutes)\n"
251
244
  "Please try again."
252
245
  )
253
- self.notify("Authentication token expired", severity="error")
254
246
  await asyncio.sleep(3)
255
247
  self.dismiss(False)
256
248
  return
@@ -269,7 +261,6 @@ class ShotgunAuthScreen(Screen[bool]):
269
261
  self.query_one("#status", Label).update(
270
262
  "❌ Authentication token expired"
271
263
  )
272
- self.notify("Authentication token expired", severity="error")
273
264
  await asyncio.sleep(3)
274
265
  self.dismiss(False)
275
266
  return
@@ -290,6 +281,5 @@ class ShotgunAuthScreen(Screen[bool]):
290
281
  self.query_one("#status", Label).update(
291
282
  "❌ Authentication timeout (30 minutes)\nPlease try again."
292
283
  )
293
- self.notify("Authentication timeout", severity="error")
294
284
  await asyncio.sleep(3)
295
285
  self.dismiss(False)
@@ -85,6 +85,21 @@ class WelcomeScreen(Screen[None]):
85
85
  margin: 1 0 0 0;
86
86
  width: 100%;
87
87
  }
88
+
89
+ #migration-warning {
90
+ width: 80%;
91
+ height: auto;
92
+ padding: 2;
93
+ margin: 1 0;
94
+ border: solid $warning;
95
+ background: $warning 20%;
96
+ }
97
+
98
+ #migration-warning-title {
99
+ text-style: bold;
100
+ color: $warning;
101
+ padding: 0 0 1 0;
102
+ }
88
103
  """
89
104
 
90
105
  BINDINGS = [
@@ -99,6 +114,23 @@ class WelcomeScreen(Screen[None]):
99
114
  id="welcome-subtitle",
100
115
  )
101
116
 
117
+ # Show migration warning if migration failed
118
+ app = cast("ShotgunApp", self.app)
119
+ # Note: This is a synchronous call in compose, but config should already be loaded
120
+ if hasattr(app, "config_manager") and app.config_manager._config:
121
+ config = app.config_manager._config
122
+ if config.migration_failed:
123
+ with Vertical(id="migration-warning"):
124
+ yield Static(
125
+ "⚠️ Configuration Migration Failed",
126
+ id="migration-warning-title",
127
+ )
128
+ backup_msg = "Your previous configuration couldn't be migrated automatically."
129
+ if config.migration_backup_path:
130
+ backup_msg += f"\n\nYour old configuration (including API keys) has been backed up to:\n{config.migration_backup_path}"
131
+ backup_msg += "\n\nYou'll need to reconfigure Shotgun by choosing an option below."
132
+ yield Markdown(backup_msg)
133
+
102
134
  with Container(id="options-container"):
103
135
  with Horizontal(id="options"):
104
136
  # Left box - Shotgun Account