shotgun-sh 0.2.8.dev2__py3-none-any.whl → 0.3.3.dev1__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.
Files changed (175) hide show
  1. shotgun/agents/agent_manager.py +382 -60
  2. shotgun/agents/common.py +15 -9
  3. shotgun/agents/config/README.md +89 -0
  4. shotgun/agents/config/__init__.py +10 -1
  5. shotgun/agents/config/constants.py +0 -6
  6. shotgun/agents/config/manager.py +383 -82
  7. shotgun/agents/config/models.py +122 -18
  8. shotgun/agents/config/provider.py +81 -15
  9. shotgun/agents/config/streaming_test.py +119 -0
  10. shotgun/agents/context_analyzer/__init__.py +28 -0
  11. shotgun/agents/context_analyzer/analyzer.py +475 -0
  12. shotgun/agents/context_analyzer/constants.py +9 -0
  13. shotgun/agents/context_analyzer/formatter.py +115 -0
  14. shotgun/agents/context_analyzer/models.py +212 -0
  15. shotgun/agents/conversation/__init__.py +18 -0
  16. shotgun/agents/conversation/filters.py +164 -0
  17. shotgun/agents/conversation/history/chunking.py +278 -0
  18. shotgun/agents/{history → conversation/history}/compaction.py +36 -5
  19. shotgun/agents/{history → conversation/history}/constants.py +5 -0
  20. shotgun/agents/conversation/history/file_content_deduplication.py +216 -0
  21. shotgun/agents/{history → conversation/history}/history_processors.py +380 -8
  22. shotgun/agents/{history → conversation/history}/token_counting/anthropic.py +25 -1
  23. shotgun/agents/{history → conversation/history}/token_counting/base.py +14 -3
  24. shotgun/agents/{history → conversation/history}/token_counting/openai.py +11 -1
  25. shotgun/agents/{history → conversation/history}/token_counting/sentencepiece_counter.py +8 -0
  26. shotgun/agents/{history → conversation/history}/token_counting/tokenizer_cache.py +3 -1
  27. shotgun/agents/{history → conversation/history}/token_counting/utils.py +0 -3
  28. shotgun/agents/{conversation_manager.py → conversation/manager.py} +36 -20
  29. shotgun/agents/{conversation_history.py → conversation/models.py} +8 -92
  30. shotgun/agents/error/__init__.py +11 -0
  31. shotgun/agents/error/models.py +19 -0
  32. shotgun/agents/export.py +2 -2
  33. shotgun/agents/plan.py +2 -2
  34. shotgun/agents/research.py +3 -3
  35. shotgun/agents/runner.py +230 -0
  36. shotgun/agents/specify.py +2 -2
  37. shotgun/agents/tasks.py +2 -2
  38. shotgun/agents/tools/codebase/codebase_shell.py +6 -0
  39. shotgun/agents/tools/codebase/directory_lister.py +6 -0
  40. shotgun/agents/tools/codebase/file_read.py +11 -2
  41. shotgun/agents/tools/codebase/query_graph.py +6 -0
  42. shotgun/agents/tools/codebase/retrieve_code.py +6 -0
  43. shotgun/agents/tools/file_management.py +27 -7
  44. shotgun/agents/tools/registry.py +217 -0
  45. shotgun/agents/tools/web_search/__init__.py +8 -8
  46. shotgun/agents/tools/web_search/anthropic.py +8 -2
  47. shotgun/agents/tools/web_search/gemini.py +7 -1
  48. shotgun/agents/tools/web_search/openai.py +8 -2
  49. shotgun/agents/tools/web_search/utils.py +2 -2
  50. shotgun/agents/usage_manager.py +16 -11
  51. shotgun/api_endpoints.py +7 -3
  52. shotgun/build_constants.py +2 -2
  53. shotgun/cli/clear.py +53 -0
  54. shotgun/cli/compact.py +188 -0
  55. shotgun/cli/config.py +8 -5
  56. shotgun/cli/context.py +154 -0
  57. shotgun/cli/error_handler.py +24 -0
  58. shotgun/cli/export.py +34 -34
  59. shotgun/cli/feedback.py +4 -2
  60. shotgun/cli/models.py +1 -0
  61. shotgun/cli/plan.py +34 -34
  62. shotgun/cli/research.py +18 -10
  63. shotgun/cli/spec/__init__.py +5 -0
  64. shotgun/cli/spec/backup.py +81 -0
  65. shotgun/cli/spec/commands.py +132 -0
  66. shotgun/cli/spec/models.py +48 -0
  67. shotgun/cli/spec/pull_service.py +219 -0
  68. shotgun/cli/specify.py +20 -19
  69. shotgun/cli/tasks.py +34 -34
  70. shotgun/cli/update.py +16 -2
  71. shotgun/codebase/core/change_detector.py +5 -3
  72. shotgun/codebase/core/code_retrieval.py +4 -2
  73. shotgun/codebase/core/ingestor.py +163 -15
  74. shotgun/codebase/core/manager.py +13 -4
  75. shotgun/codebase/core/nl_query.py +1 -1
  76. shotgun/codebase/models.py +2 -0
  77. shotgun/exceptions.py +357 -0
  78. shotgun/llm_proxy/__init__.py +17 -0
  79. shotgun/llm_proxy/client.py +215 -0
  80. shotgun/llm_proxy/models.py +137 -0
  81. shotgun/logging_config.py +60 -27
  82. shotgun/main.py +77 -11
  83. shotgun/posthog_telemetry.py +38 -29
  84. shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +28 -2
  85. shotgun/prompts/agents/partials/interactive_mode.j2 +3 -3
  86. shotgun/prompts/agents/plan.j2 +16 -0
  87. shotgun/prompts/agents/research.j2 +16 -3
  88. shotgun/prompts/agents/specify.j2 +54 -1
  89. shotgun/prompts/agents/state/system_state.j2 +0 -2
  90. shotgun/prompts/agents/tasks.j2 +16 -0
  91. shotgun/prompts/history/chunk_summarization.j2 +34 -0
  92. shotgun/prompts/history/combine_summaries.j2 +53 -0
  93. shotgun/sdk/codebase.py +14 -3
  94. shotgun/sentry_telemetry.py +163 -16
  95. shotgun/settings.py +243 -0
  96. shotgun/shotgun_web/__init__.py +67 -1
  97. shotgun/shotgun_web/client.py +42 -1
  98. shotgun/shotgun_web/constants.py +46 -0
  99. shotgun/shotgun_web/exceptions.py +29 -0
  100. shotgun/shotgun_web/models.py +390 -0
  101. shotgun/shotgun_web/shared_specs/__init__.py +32 -0
  102. shotgun/shotgun_web/shared_specs/file_scanner.py +175 -0
  103. shotgun/shotgun_web/shared_specs/hasher.py +83 -0
  104. shotgun/shotgun_web/shared_specs/models.py +71 -0
  105. shotgun/shotgun_web/shared_specs/upload_pipeline.py +329 -0
  106. shotgun/shotgun_web/shared_specs/utils.py +34 -0
  107. shotgun/shotgun_web/specs_client.py +703 -0
  108. shotgun/shotgun_web/supabase_client.py +31 -0
  109. shotgun/telemetry.py +10 -33
  110. shotgun/tui/app.py +310 -46
  111. shotgun/tui/commands/__init__.py +1 -1
  112. shotgun/tui/components/context_indicator.py +179 -0
  113. shotgun/tui/components/mode_indicator.py +70 -0
  114. shotgun/tui/components/status_bar.py +48 -0
  115. shotgun/tui/containers.py +91 -0
  116. shotgun/tui/dependencies.py +39 -0
  117. shotgun/tui/layout.py +5 -0
  118. shotgun/tui/protocols.py +45 -0
  119. shotgun/tui/screens/chat/__init__.py +5 -0
  120. shotgun/tui/screens/chat/chat.tcss +54 -0
  121. shotgun/tui/screens/chat/chat_screen.py +1531 -0
  122. shotgun/tui/screens/chat/codebase_index_prompt_screen.py +243 -0
  123. shotgun/tui/screens/chat/codebase_index_selection.py +12 -0
  124. shotgun/tui/screens/chat/help_text.py +40 -0
  125. shotgun/tui/screens/chat/prompt_history.py +48 -0
  126. shotgun/tui/screens/chat.tcss +11 -0
  127. shotgun/tui/screens/chat_screen/command_providers.py +91 -4
  128. shotgun/tui/screens/chat_screen/hint_message.py +76 -1
  129. shotgun/tui/screens/chat_screen/history/__init__.py +22 -0
  130. shotgun/tui/screens/chat_screen/history/agent_response.py +66 -0
  131. shotgun/tui/screens/chat_screen/history/chat_history.py +115 -0
  132. shotgun/tui/screens/chat_screen/history/formatters.py +115 -0
  133. shotgun/tui/screens/chat_screen/history/partial_response.py +43 -0
  134. shotgun/tui/screens/chat_screen/history/user_question.py +42 -0
  135. shotgun/tui/screens/confirmation_dialog.py +191 -0
  136. shotgun/tui/screens/directory_setup.py +45 -41
  137. shotgun/tui/screens/feedback.py +14 -7
  138. shotgun/tui/screens/github_issue.py +111 -0
  139. shotgun/tui/screens/model_picker.py +77 -32
  140. shotgun/tui/screens/onboarding.py +580 -0
  141. shotgun/tui/screens/pipx_migration.py +205 -0
  142. shotgun/tui/screens/provider_config.py +116 -35
  143. shotgun/tui/screens/shared_specs/__init__.py +21 -0
  144. shotgun/tui/screens/shared_specs/create_spec_dialog.py +273 -0
  145. shotgun/tui/screens/shared_specs/models.py +56 -0
  146. shotgun/tui/screens/shared_specs/share_specs_dialog.py +390 -0
  147. shotgun/tui/screens/shared_specs/upload_progress_screen.py +452 -0
  148. shotgun/tui/screens/shotgun_auth.py +112 -18
  149. shotgun/tui/screens/spec_pull.py +288 -0
  150. shotgun/tui/screens/welcome.py +137 -11
  151. shotgun/tui/services/__init__.py +5 -0
  152. shotgun/tui/services/conversation_service.py +187 -0
  153. shotgun/tui/state/__init__.py +7 -0
  154. shotgun/tui/state/processing_state.py +185 -0
  155. shotgun/tui/utils/mode_progress.py +14 -7
  156. shotgun/tui/widgets/__init__.py +5 -0
  157. shotgun/tui/widgets/widget_coordinator.py +263 -0
  158. shotgun/utils/file_system_utils.py +22 -2
  159. shotgun/utils/marketing.py +110 -0
  160. shotgun/utils/update_checker.py +69 -14
  161. shotgun_sh-0.3.3.dev1.dist-info/METADATA +472 -0
  162. shotgun_sh-0.3.3.dev1.dist-info/RECORD +229 -0
  163. {shotgun_sh-0.2.8.dev2.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/WHEEL +1 -1
  164. {shotgun_sh-0.2.8.dev2.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/entry_points.txt +1 -0
  165. {shotgun_sh-0.2.8.dev2.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/licenses/LICENSE +1 -1
  166. shotgun/tui/screens/chat.py +0 -996
  167. shotgun/tui/screens/chat_screen/history.py +0 -335
  168. shotgun_sh-0.2.8.dev2.dist-info/METADATA +0 -126
  169. shotgun_sh-0.2.8.dev2.dist-info/RECORD +0 -155
  170. /shotgun/agents/{history → conversation/history}/__init__.py +0 -0
  171. /shotgun/agents/{history → conversation/history}/context_extraction.py +0 -0
  172. /shotgun/agents/{history → conversation/history}/history_building.py +0 -0
  173. /shotgun/agents/{history → conversation/history}/message_utils.py +0 -0
  174. /shotgun/agents/{history → conversation/history}/token_counting/__init__.py +0 -0
  175. /shotgun/agents/{history → conversation/history}/token_estimation.py +0 -0
@@ -0,0 +1,205 @@
1
+ """Migration notice screen for pipx users."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from textual import on
8
+ from textual.app import ComposeResult
9
+ from textual.containers import Container, Horizontal, VerticalScroll
10
+ from textual.events import Resize
11
+ from textual.screen import ModalScreen
12
+ from textual.widgets import Button, Label, Markdown
13
+
14
+ from shotgun.tui.layout import COMPACT_HEIGHT_THRESHOLD
15
+
16
+ if TYPE_CHECKING:
17
+ pass
18
+
19
+
20
+ class PipxMigrationScreen(ModalScreen[None]):
21
+ """Modal screen warning pipx users about migration to uvx."""
22
+
23
+ CSS = """
24
+ PipxMigrationScreen {
25
+ align: center middle;
26
+ }
27
+
28
+ #migration-container {
29
+ width: 90;
30
+ height: auto;
31
+ max-height: 90%;
32
+ border: thick $error;
33
+ background: $surface;
34
+ padding: 2;
35
+ }
36
+
37
+ #migration-content {
38
+ height: 1fr;
39
+ padding: 1 0;
40
+ }
41
+
42
+ #buttons-container {
43
+ height: auto;
44
+ padding: 2 0 1 0;
45
+ }
46
+
47
+ #action-buttons {
48
+ width: 100%;
49
+ height: auto;
50
+ align: center middle;
51
+ }
52
+
53
+ .action-button {
54
+ margin: 0 1;
55
+ min-width: 20;
56
+ }
57
+
58
+ #migration-status {
59
+ height: auto;
60
+ padding: 1;
61
+ min-height: 1;
62
+ text-align: center;
63
+ }
64
+
65
+ /* Compact styles for short terminals */
66
+ #migration-container.compact {
67
+ padding: 1;
68
+ max-height: 98%;
69
+ }
70
+
71
+ #migration-content.compact {
72
+ padding: 0;
73
+ }
74
+
75
+ #buttons-container.compact {
76
+ padding: 1 0 0 0;
77
+ }
78
+
79
+ #migration-status.compact {
80
+ padding: 0;
81
+ }
82
+ """
83
+
84
+ BINDINGS = [
85
+ ("escape", "dismiss", "Continue Anyway"),
86
+ ("ctrl+c", "app.quit", "Quit"),
87
+ ]
88
+
89
+ def compose(self) -> ComposeResult:
90
+ """Compose the migration notice modal."""
91
+ with Container(id="migration-container"):
92
+ with VerticalScroll(id="migration-content"):
93
+ yield Markdown(
94
+ """
95
+ ## We've Switched to uvx
96
+
97
+ We've switched from `pipx` to `uvx` as the primary installation method due to critical build issues with our `kuzu` dependency.
98
+
99
+ ### The Problem
100
+ Users with pipx encounter cmake build errors during installation because pip falls back to building from source instead of using pre-built binary wheels.
101
+
102
+ ### The Solution: uvx
103
+ - ✅ **No build tools required** - Binary wheels enforced
104
+ - ✅ **10-100x faster** - Much faster than pipx
105
+ - ✅ **Better reliability** - No cmake/build errors
106
+
107
+ ### How to Migrate
108
+
109
+ **1. Uninstall shotgun-sh from pipx:**
110
+ ```bash
111
+ pipx uninstall shotgun-sh
112
+ ```
113
+
114
+ **2. Install uv:**
115
+ ```bash
116
+ curl -LsSf https://astral.sh/uv/install.sh | sh
117
+ ```
118
+ Or with Homebrew: `brew install uv`
119
+
120
+ **3. Run shotgun-sh with uvx:**
121
+ ```bash
122
+ uvx shotgun-sh
123
+ ```
124
+ Or install permanently: `uv tool install shotgun-sh`
125
+
126
+ ---
127
+
128
+ ### Need Help?
129
+
130
+ **Discord:** https://discord.gg/5RmY6J2N7s
131
+
132
+ **Full Migration Guide:** https://github.com/shotgun-sh/shotgun/blob/main/docs/PIPX_MIGRATION.md
133
+ """
134
+ )
135
+
136
+ with Container(id="buttons-container"):
137
+ yield Label("", id="migration-status")
138
+ with Horizontal(id="action-buttons"):
139
+ yield Button(
140
+ "Copy Instructions to Clipboard",
141
+ variant="default",
142
+ id="copy-instructions",
143
+ classes="action-button",
144
+ )
145
+ yield Button(
146
+ "Continue Anyway",
147
+ variant="primary",
148
+ id="continue",
149
+ classes="action-button",
150
+ )
151
+
152
+ def on_mount(self) -> None:
153
+ """Focus the continue button and ensure scroll starts at top."""
154
+ self.query_one("#continue", Button).focus()
155
+ self.query_one("#migration-content", VerticalScroll).scroll_home(animate=False)
156
+ # Apply compact layout if starting in a short terminal
157
+ self._apply_compact_layout(self.app.size.height < COMPACT_HEIGHT_THRESHOLD)
158
+
159
+ @on(Resize)
160
+ def handle_resize(self, event: Resize) -> None:
161
+ """Adjust layout based on terminal height."""
162
+ self._apply_compact_layout(event.size.height < COMPACT_HEIGHT_THRESHOLD)
163
+
164
+ def _apply_compact_layout(self, compact: bool) -> None:
165
+ """Apply or remove compact layout classes for short terminals."""
166
+ container = self.query_one("#migration-container")
167
+ content = self.query_one("#migration-content")
168
+ buttons_container = self.query_one("#buttons-container")
169
+ status = self.query_one("#migration-status")
170
+
171
+ if compact:
172
+ container.add_class("compact")
173
+ content.add_class("compact")
174
+ buttons_container.add_class("compact")
175
+ status.add_class("compact")
176
+ else:
177
+ container.remove_class("compact")
178
+ content.remove_class("compact")
179
+ buttons_container.remove_class("compact")
180
+ status.remove_class("compact")
181
+
182
+ @on(Button.Pressed, "#copy-instructions")
183
+ def _copy_instructions(self) -> None:
184
+ """Copy all migration instructions to clipboard."""
185
+ instructions = """# Step 1: Uninstall from pipx
186
+ pipx uninstall shotgun-sh
187
+
188
+ # Step 2: Install uv
189
+ curl -LsSf https://astral.sh/uv/install.sh | sh
190
+
191
+ # Step 3: Run shotgun with uvx
192
+ uvx shotgun-sh"""
193
+ status_label = self.query_one("#migration-status", Label)
194
+ try:
195
+ import pyperclip # type: ignore[import-untyped] # noqa: PGH003
196
+
197
+ pyperclip.copy(instructions)
198
+ status_label.update("✓ Copied migration instructions to clipboard!")
199
+ except ImportError:
200
+ status_label.update("⚠️ Clipboard not available. See instructions above.")
201
+
202
+ @on(Button.Pressed, "#continue")
203
+ def _continue(self) -> None:
204
+ """Dismiss the modal and continue."""
205
+ self.dismiss()
@@ -7,11 +7,13 @@ from typing import TYPE_CHECKING, cast
7
7
  from textual import on
8
8
  from textual.app import ComposeResult
9
9
  from textual.containers import Horizontal, Vertical
10
+ from textual.events import Resize
10
11
  from textual.reactive import reactive
11
12
  from textual.screen import Screen
12
13
  from textual.widgets import Button, Input, Label, ListItem, ListView, Markdown, Static
13
14
 
14
15
  from shotgun.agents.config import ConfigManager, ProviderType
16
+ from shotgun.tui.layout import COMPACT_HEIGHT_THRESHOLD
15
17
 
16
18
  if TYPE_CHECKING:
17
19
  from ..app import ShotgunApp
@@ -77,6 +79,38 @@ class ProviderConfigScreen(Screen[None]):
77
79
  #provider-list {
78
80
  padding: 1;
79
81
  }
82
+ #provider-status {
83
+ height: auto;
84
+ padding: 0 1;
85
+ min-height: 1;
86
+ }
87
+ #provider-status.error {
88
+ color: $error;
89
+ }
90
+
91
+ /* Compact styles for short terminals */
92
+ ProviderConfigScreen.compact #titlebox {
93
+ margin: 0;
94
+ padding: 0;
95
+ border: none;
96
+ }
97
+
98
+ ProviderConfigScreen.compact #provider-config-summary {
99
+ display: none;
100
+ }
101
+
102
+ ProviderConfigScreen.compact #provider-links {
103
+ display: none;
104
+ }
105
+
106
+ ProviderConfigScreen.compact #provider-list {
107
+ margin: 0;
108
+ padding: 0;
109
+ }
110
+
111
+ ProviderConfigScreen.compact #provider-actions {
112
+ padding: 0;
113
+ }
80
114
  """
81
115
 
82
116
  BINDINGS = [
@@ -97,12 +131,13 @@ class ProviderConfigScreen(Screen[None]):
97
131
  "Don't have an API Key? Use these links to get one: [OpenAI](https://platform.openai.com/api-keys) | [Anthropic](https://console.anthropic.com) | [Google Gemini](https://aistudio.google.com)",
98
132
  id="provider-links",
99
133
  )
100
- yield ListView(*self._build_provider_items(), id="provider-list")
134
+ yield ListView(*self._build_provider_items_sync(), id="provider-list")
101
135
  yield Input(
102
136
  placeholder=self._input_placeholder(self.selected_provider),
103
137
  password=True,
104
138
  id="api-key",
105
139
  )
140
+ yield Label("", id="provider-status")
106
141
  with Horizontal(id="provider-actions"):
107
142
  yield Button("Save key \\[ENTER]", variant="primary", id="save")
108
143
  yield Button("Authenticate", variant="success", id="authenticate")
@@ -110,8 +145,6 @@ class ProviderConfigScreen(Screen[None]):
110
145
  yield Button("Done \\[ESC]", id="done")
111
146
 
112
147
  def on_mount(self) -> None:
113
- self.refresh_provider_status()
114
- self._update_done_button_visibility()
115
148
  list_view = self.query_one(ListView)
116
149
  if list_view.children:
117
150
  list_view.index = 0
@@ -121,13 +154,35 @@ class ProviderConfigScreen(Screen[None]):
121
154
  self.query_one("#authenticate", Button).display = False
122
155
  self.set_focus(self.query_one("#api-key", Input))
123
156
 
157
+ # Refresh UI asynchronously
158
+ self.run_worker(self._refresh_ui(), exclusive=False)
159
+
160
+ # Apply layout based on terminal height
161
+ self._apply_layout_for_height(self.app.size.height)
162
+
163
+ @on(Resize)
164
+ def handle_resize(self, event: Resize) -> None:
165
+ """Adjust layout based on terminal height."""
166
+ self._apply_layout_for_height(event.size.height)
167
+
168
+ def _apply_layout_for_height(self, height: int) -> None:
169
+ """Apply appropriate layout based on terminal height."""
170
+ if height < COMPACT_HEIGHT_THRESHOLD:
171
+ self.add_class("compact")
172
+ else:
173
+ self.remove_class("compact")
174
+
124
175
  def on_screenresume(self) -> None:
125
176
  """Refresh provider status when screen is resumed.
126
177
 
127
178
  This ensures the UI reflects any provider changes made elsewhere.
128
179
  """
129
- self.refresh_provider_status()
130
- self._update_done_button_visibility()
180
+ self.run_worker(self._refresh_ui(), exclusive=False)
181
+
182
+ async def _refresh_ui(self) -> None:
183
+ """Refresh provider status and button visibility."""
184
+ await self.refresh_provider_status()
185
+ await self._update_done_button_visibility()
131
186
 
132
187
  def action_done(self) -> None:
133
188
  self.dismiss()
@@ -170,7 +225,11 @@ class ProviderConfigScreen(Screen[None]):
170
225
  if not self.is_mounted:
171
226
  return
172
227
 
173
- # Show/hide UI elements based on provider type
228
+ # Show/hide UI elements based on provider type asynchronously
229
+ self.run_worker(self._update_provider_ui(provider), exclusive=False)
230
+
231
+ async def _update_provider_ui(self, provider: ProviderType) -> None:
232
+ """Update UI elements based on selected provider."""
174
233
  is_shotgun = provider == "shotgun"
175
234
 
176
235
  input_widget = self.query_one("#api-key", Input)
@@ -183,7 +242,7 @@ class ProviderConfigScreen(Screen[None]):
183
242
  save_button.display = False
184
243
 
185
244
  # Only show Authenticate button if shotgun is NOT already configured
186
- if self._has_provider_key("shotgun"):
245
+ if await self._has_provider_key("shotgun"):
187
246
  auth_button.display = False
188
247
  else:
189
248
  auth_button.display = True
@@ -200,22 +259,29 @@ class ProviderConfigScreen(Screen[None]):
200
259
  app = cast("ShotgunApp", self.app)
201
260
  return app.config_manager
202
261
 
203
- def refresh_provider_status(self) -> None:
262
+ async def refresh_provider_status(self) -> None:
204
263
  """Update the list view entries to reflect configured providers."""
205
264
  for provider_id in get_configurable_providers():
206
265
  label = self.query_one(f"#label-{provider_id}", Label)
207
- label.update(self._provider_label(provider_id))
266
+ label.update(await self._provider_label(provider_id))
208
267
 
209
- def _update_done_button_visibility(self) -> None:
268
+ async def _update_done_button_visibility(self) -> None:
210
269
  """Show/hide Done button based on whether any provider keys are configured."""
211
270
  done_button = self.query_one("#done", Button)
212
- has_keys = self.config_manager.has_any_provider_key()
271
+ has_keys = await self.config_manager.has_any_provider_key()
213
272
  done_button.display = has_keys
214
273
 
215
- def _build_provider_items(self) -> list[ListItem]:
274
+ def _build_provider_items_sync(self) -> list[ListItem]:
275
+ """Build provider items synchronously for compose().
276
+
277
+ Labels will be populated with status asynchronously in on_mount().
278
+ """
216
279
  items: list[ListItem] = []
217
280
  for provider_id in get_configurable_providers():
218
- label = Label(self._provider_label(provider_id), id=f"label-{provider_id}")
281
+ # Create labels with placeholder text - will be updated in on_mount()
282
+ label = Label(
283
+ self._provider_display_name(provider_id), id=f"label-{provider_id}"
284
+ )
219
285
  items.append(ListItem(label, id=f"provider-{provider_id}"))
220
286
  return items
221
287
 
@@ -225,11 +291,10 @@ class ProviderConfigScreen(Screen[None]):
225
291
  provider_id = item.id.removeprefix("provider-")
226
292
  return provider_id if provider_id in get_configurable_providers() else None
227
293
 
228
- def _provider_label(self, provider_id: str) -> str:
294
+ async def _provider_label(self, provider_id: str) -> str:
229
295
  display = self._provider_display_name(provider_id)
230
- status = (
231
- "Configured" if self._has_provider_key(provider_id) else "Not configured"
232
- )
296
+ has_key = await self._has_provider_key(provider_id)
297
+ status = "Configured" if has_key else "Not configured"
233
298
  return f"{display} · {status}"
234
299
 
235
300
  def _provider_display_name(self, provider_id: str) -> str:
@@ -244,53 +309,67 @@ class ProviderConfigScreen(Screen[None]):
244
309
  def _input_placeholder(self, provider_id: str) -> str:
245
310
  return f"{self._provider_display_name(provider_id)} API key"
246
311
 
247
- def _has_provider_key(self, provider_id: str) -> bool:
312
+ async def _has_provider_key(self, provider_id: str) -> bool:
248
313
  """Check if provider has a configured API key."""
249
314
  if provider_id == "shotgun":
250
315
  # Check shotgun key directly
251
- config = self.config_manager.load()
316
+ config = await self.config_manager.load()
252
317
  return self.config_manager._provider_has_api_key(config.shotgun)
253
318
  else:
254
319
  # Check LLM provider key
255
320
  try:
256
321
  provider = ProviderType(provider_id)
257
- return self.config_manager.has_provider_key(provider)
322
+ return await self.config_manager.has_provider_key(provider)
258
323
  except ValueError:
259
324
  return False
260
325
 
261
326
  def _save_api_key(self) -> None:
327
+ self.run_worker(self._do_save_api_key(), exclusive=True)
328
+
329
+ async def _do_save_api_key(self) -> None:
330
+ """Async implementation of API key saving."""
262
331
  input_widget = self.query_one("#api-key", Input)
263
332
  api_key = input_widget.value.strip()
333
+ status_label = self.query_one("#provider-status", Label)
264
334
 
265
335
  if not api_key:
266
- self.notify("Enter an API key before saving.", severity="error")
336
+ status_label.update("Enter an API key before saving.")
337
+ status_label.add_class("error")
267
338
  return
268
339
 
269
340
  try:
270
- self.config_manager.update_provider(
341
+ await self.config_manager.update_provider(
271
342
  self.selected_provider,
272
343
  api_key=api_key,
273
344
  )
274
345
  except Exception as exc: # pragma: no cover - defensive; textual path
275
- self.notify(f"Failed to save key: {exc}", severity="error")
346
+ status_label.update(f"Failed to save key: {exc}")
347
+ status_label.add_class("error")
276
348
  return
277
349
 
278
350
  input_widget.value = ""
279
- self.refresh_provider_status()
280
- self._update_done_button_visibility()
281
- self.notify(
282
- f"Saved API key for {self._provider_display_name(self.selected_provider)}."
351
+ await self.refresh_provider_status()
352
+ await self._update_done_button_visibility()
353
+ status_label.update(
354
+ f"Saved API key for {self._provider_display_name(self.selected_provider)}."
283
355
  )
356
+ status_label.remove_class("error")
284
357
 
285
358
  def _clear_api_key(self) -> None:
359
+ self.run_worker(self._do_clear_api_key(), exclusive=True)
360
+
361
+ async def _do_clear_api_key(self) -> None:
362
+ """Async implementation of API key clearing."""
363
+ status_label = self.query_one("#provider-status", Label)
286
364
  try:
287
- self.config_manager.clear_provider_key(self.selected_provider)
365
+ await self.config_manager.clear_provider_key(self.selected_provider)
288
366
  except Exception as exc: # pragma: no cover - defensive; textual path
289
- self.notify(f"Failed to clear key: {exc}", severity="error")
367
+ status_label.update(f"Failed to clear key: {exc}")
368
+ status_label.add_class("error")
290
369
  return
291
370
 
292
- self.refresh_provider_status()
293
- self._update_done_button_visibility()
371
+ await self.refresh_provider_status()
372
+ await self._update_done_button_visibility()
294
373
  self.query_one("#api-key", Input).value = ""
295
374
 
296
375
  # If we just cleared shotgun, show the Authenticate button
@@ -298,9 +377,10 @@ class ProviderConfigScreen(Screen[None]):
298
377
  auth_button = self.query_one("#authenticate", Button)
299
378
  auth_button.display = True
300
379
 
301
- self.notify(
302
- f"Cleared API key for {self._provider_display_name(self.selected_provider)}."
380
+ status_label.update(
381
+ f"Cleared API key for {self._provider_display_name(self.selected_provider)}."
303
382
  )
383
+ status_label.remove_class("error")
304
384
 
305
385
  async def _start_shotgun_auth(self) -> None:
306
386
  """Launch Shotgun Account authentication flow."""
@@ -311,5 +391,6 @@ class ProviderConfigScreen(Screen[None]):
311
391
 
312
392
  # Refresh provider status after auth completes
313
393
  if result:
314
- self.refresh_provider_status()
315
- # Notify handled by auth screen
394
+ await self.refresh_provider_status()
395
+ # Auto-dismiss provider config screen after successful auth
396
+ self.dismiss()
@@ -0,0 +1,21 @@
1
+ """Shared specs TUI screens and dialogs."""
2
+
3
+ from shotgun.tui.screens.shared_specs.create_spec_dialog import CreateSpecDialog
4
+ from shotgun.tui.screens.shared_specs.models import (
5
+ CreateSpecResult,
6
+ ShareSpecsAction,
7
+ ShareSpecsResult,
8
+ UploadScreenResult,
9
+ )
10
+ from shotgun.tui.screens.shared_specs.share_specs_dialog import ShareSpecsDialog
11
+ from shotgun.tui.screens.shared_specs.upload_progress_screen import UploadProgressScreen
12
+
13
+ __all__ = [
14
+ "CreateSpecDialog",
15
+ "CreateSpecResult",
16
+ "ShareSpecsAction",
17
+ "ShareSpecsDialog",
18
+ "ShareSpecsResult",
19
+ "UploadProgressScreen",
20
+ "UploadScreenResult",
21
+ ]