shotgun-sh 0.2.17__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 (112) hide show
  1. shotgun/agents/agent_manager.py +28 -14
  2. shotgun/agents/common.py +1 -1
  3. shotgun/agents/config/README.md +89 -0
  4. shotgun/agents/config/__init__.py +10 -1
  5. shotgun/agents/config/manager.py +323 -53
  6. shotgun/agents/config/models.py +85 -21
  7. shotgun/agents/config/provider.py +51 -13
  8. shotgun/agents/config/streaming_test.py +119 -0
  9. shotgun/agents/context_analyzer/analyzer.py +6 -2
  10. shotgun/agents/conversation/__init__.py +18 -0
  11. shotgun/agents/conversation/filters.py +164 -0
  12. shotgun/agents/conversation/history/chunking.py +278 -0
  13. shotgun/agents/{history → conversation/history}/compaction.py +27 -1
  14. shotgun/agents/{history → conversation/history}/constants.py +5 -0
  15. shotgun/agents/conversation/history/file_content_deduplication.py +216 -0
  16. shotgun/agents/{history → conversation/history}/history_processors.py +267 -3
  17. shotgun/agents/{history → conversation/history}/token_counting/anthropic.py +8 -0
  18. shotgun/agents/{conversation_manager.py → conversation/manager.py} +1 -1
  19. shotgun/agents/{conversation_history.py → conversation/models.py} +8 -94
  20. shotgun/agents/error/__init__.py +11 -0
  21. shotgun/agents/error/models.py +19 -0
  22. shotgun/agents/runner.py +230 -0
  23. shotgun/agents/tools/web_search/openai.py +1 -1
  24. shotgun/build_constants.py +2 -2
  25. shotgun/cli/clear.py +1 -1
  26. shotgun/cli/compact.py +5 -3
  27. shotgun/cli/context.py +44 -1
  28. shotgun/cli/error_handler.py +24 -0
  29. shotgun/cli/export.py +34 -34
  30. shotgun/cli/plan.py +34 -34
  31. shotgun/cli/research.py +17 -9
  32. shotgun/cli/spec/__init__.py +5 -0
  33. shotgun/cli/spec/backup.py +81 -0
  34. shotgun/cli/spec/commands.py +132 -0
  35. shotgun/cli/spec/models.py +48 -0
  36. shotgun/cli/spec/pull_service.py +219 -0
  37. shotgun/cli/specify.py +20 -19
  38. shotgun/cli/tasks.py +34 -34
  39. shotgun/codebase/core/ingestor.py +153 -7
  40. shotgun/codebase/models.py +2 -0
  41. shotgun/exceptions.py +325 -0
  42. shotgun/llm_proxy/__init__.py +17 -0
  43. shotgun/llm_proxy/client.py +215 -0
  44. shotgun/llm_proxy/models.py +137 -0
  45. shotgun/logging_config.py +42 -0
  46. shotgun/main.py +4 -0
  47. shotgun/posthog_telemetry.py +1 -1
  48. shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +28 -3
  49. shotgun/prompts/agents/partials/interactive_mode.j2 +3 -3
  50. shotgun/prompts/agents/plan.j2 +16 -0
  51. shotgun/prompts/agents/research.j2 +16 -3
  52. shotgun/prompts/agents/specify.j2 +54 -1
  53. shotgun/prompts/agents/state/system_state.j2 +0 -2
  54. shotgun/prompts/agents/tasks.j2 +16 -0
  55. shotgun/prompts/history/chunk_summarization.j2 +34 -0
  56. shotgun/prompts/history/combine_summaries.j2 +53 -0
  57. shotgun/sdk/codebase.py +14 -3
  58. shotgun/settings.py +5 -0
  59. shotgun/shotgun_web/__init__.py +67 -1
  60. shotgun/shotgun_web/client.py +42 -1
  61. shotgun/shotgun_web/constants.py +46 -0
  62. shotgun/shotgun_web/exceptions.py +29 -0
  63. shotgun/shotgun_web/models.py +390 -0
  64. shotgun/shotgun_web/shared_specs/__init__.py +32 -0
  65. shotgun/shotgun_web/shared_specs/file_scanner.py +175 -0
  66. shotgun/shotgun_web/shared_specs/hasher.py +83 -0
  67. shotgun/shotgun_web/shared_specs/models.py +71 -0
  68. shotgun/shotgun_web/shared_specs/upload_pipeline.py +329 -0
  69. shotgun/shotgun_web/shared_specs/utils.py +34 -0
  70. shotgun/shotgun_web/specs_client.py +703 -0
  71. shotgun/shotgun_web/supabase_client.py +31 -0
  72. shotgun/tui/app.py +73 -9
  73. shotgun/tui/containers.py +1 -1
  74. shotgun/tui/layout.py +5 -0
  75. shotgun/tui/screens/chat/chat_screen.py +372 -95
  76. shotgun/tui/screens/chat/codebase_index_prompt_screen.py +196 -17
  77. shotgun/tui/screens/chat_screen/command_providers.py +13 -2
  78. shotgun/tui/screens/chat_screen/hint_message.py +76 -1
  79. shotgun/tui/screens/confirmation_dialog.py +40 -0
  80. shotgun/tui/screens/directory_setup.py +45 -41
  81. shotgun/tui/screens/feedback.py +10 -3
  82. shotgun/tui/screens/github_issue.py +11 -2
  83. shotgun/tui/screens/model_picker.py +28 -8
  84. shotgun/tui/screens/onboarding.py +149 -0
  85. shotgun/tui/screens/pipx_migration.py +58 -6
  86. shotgun/tui/screens/provider_config.py +66 -8
  87. shotgun/tui/screens/shared_specs/__init__.py +21 -0
  88. shotgun/tui/screens/shared_specs/create_spec_dialog.py +273 -0
  89. shotgun/tui/screens/shared_specs/models.py +56 -0
  90. shotgun/tui/screens/shared_specs/share_specs_dialog.py +390 -0
  91. shotgun/tui/screens/shared_specs/upload_progress_screen.py +452 -0
  92. shotgun/tui/screens/shotgun_auth.py +110 -16
  93. shotgun/tui/screens/spec_pull.py +288 -0
  94. shotgun/tui/screens/welcome.py +123 -0
  95. shotgun/tui/services/conversation_service.py +5 -2
  96. shotgun/tui/widgets/widget_coordinator.py +1 -1
  97. {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/METADATA +9 -2
  98. {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/RECORD +112 -77
  99. {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/WHEEL +1 -1
  100. /shotgun/agents/{history → conversation/history}/__init__.py +0 -0
  101. /shotgun/agents/{history → conversation/history}/context_extraction.py +0 -0
  102. /shotgun/agents/{history → conversation/history}/history_building.py +0 -0
  103. /shotgun/agents/{history → conversation/history}/message_utils.py +0 -0
  104. /shotgun/agents/{history → conversation/history}/token_counting/__init__.py +0 -0
  105. /shotgun/agents/{history → conversation/history}/token_counting/base.py +0 -0
  106. /shotgun/agents/{history → conversation/history}/token_counting/openai.py +0 -0
  107. /shotgun/agents/{history → conversation/history}/token_counting/sentencepiece_counter.py +0 -0
  108. /shotgun/agents/{history → conversation/history}/token_counting/tokenizer_cache.py +0 -0
  109. /shotgun/agents/{history → conversation/history}/token_counting/utils.py +0 -0
  110. /shotgun/agents/{history → conversation/history}/token_estimation.py +0 -0
  111. {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/entry_points.txt +0 -0
  112. {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/licenses/LICENSE +0 -0
@@ -72,6 +72,11 @@ class ModelPickerScreen(Screen[ModelConfigUpdated | None]):
72
72
  padding: 1 0;
73
73
  }
74
74
  }
75
+ #model-picker-status {
76
+ height: auto;
77
+ padding: 0 1;
78
+ color: $error;
79
+ }
75
80
  #model-actions {
76
81
  padding: 1;
77
82
  }
@@ -84,7 +89,7 @@ class ModelPickerScreen(Screen[ModelConfigUpdated | None]):
84
89
  ("escape", "done", "Back"),
85
90
  ]
86
91
 
87
- selected_model: reactive[ModelName] = reactive(ModelName.GPT_5)
92
+ selected_model: reactive[ModelName] = reactive(ModelName.GPT_5_1)
88
93
 
89
94
  def compose(self) -> ComposeResult:
90
95
  with Vertical(id="titlebox"):
@@ -94,6 +99,7 @@ class ModelPickerScreen(Screen[ModelConfigUpdated | None]):
94
99
  id="model-picker-summary",
95
100
  )
96
101
  yield ListView(id="model-list")
102
+ yield Label("", id="model-picker-status")
97
103
  with Horizontal(id="model-actions"):
98
104
  yield Button("Select \\[ENTER]", variant="primary", id="select")
99
105
  yield Button("Done \\[ESC]", id="done")
@@ -287,6 +293,12 @@ class ModelPickerScreen(Screen[ModelConfigUpdated | None]):
287
293
  )
288
294
  return has_key
289
295
 
296
+ def _format_tokens(self, tokens: int) -> str:
297
+ """Format token count for display (K for thousands, M for millions)."""
298
+ if tokens >= 1_000_000:
299
+ return f"{tokens / 1_000_000:.1f}M"
300
+ return f"{tokens // 1000}K"
301
+
290
302
  def _model_label(self, model_name: ModelName, is_current: bool) -> str:
291
303
  """Generate label for model with specs and current indicator."""
292
304
  if model_name not in MODEL_SPECS:
@@ -296,13 +308,13 @@ class ModelPickerScreen(Screen[ModelConfigUpdated | None]):
296
308
  display_name = self._model_display_name(model_name)
297
309
 
298
310
  # Format context/output tokens in readable format
299
- input_k = spec.max_input_tokens // 1000
300
- output_k = spec.max_output_tokens // 1000
311
+ input_fmt = self._format_tokens(spec.max_input_tokens)
312
+ output_fmt = self._format_tokens(spec.max_output_tokens)
301
313
 
302
- label = f"{display_name} · {input_k}K context · {output_k}K output"
314
+ label = f"{display_name} · {input_fmt} context · {output_fmt} output"
303
315
 
304
316
  # Add cost indicator for expensive models
305
- if model_name == ModelName.CLAUDE_OPUS_4_1:
317
+ if model_name == ModelName.CLAUDE_OPUS_4_5:
306
318
  label += " · Expensive"
307
319
 
308
320
  if is_current:
@@ -313,10 +325,17 @@ class ModelPickerScreen(Screen[ModelConfigUpdated | None]):
313
325
  def _model_display_name(self, model_name: ModelName) -> str:
314
326
  """Get human-readable model name."""
315
327
  names = {
316
- ModelName.GPT_5: "GPT-5 (OpenAI)",
317
- ModelName.CLAUDE_OPUS_4_1: "Claude Opus 4.1 (Anthropic)",
328
+ ModelName.GPT_5_1: "GPT-5.1 (OpenAI)",
329
+ ModelName.GPT_5_1_CODEX: "GPT-5.1 Codex (OpenAI)",
330
+ ModelName.GPT_5_1_CODEX_MINI: "GPT-5.1 Codex Mini (OpenAI)",
331
+ ModelName.CLAUDE_OPUS_4_5: "Claude Opus 4.5 (Anthropic)",
332
+ ModelName.CLAUDE_SONNET_4: "Claude Sonnet 4 (Anthropic)",
318
333
  ModelName.CLAUDE_SONNET_4_5: "Claude Sonnet 4.5 (Anthropic)",
334
+ ModelName.CLAUDE_HAIKU_4_5: "Claude Haiku 4.5 (Anthropic)",
319
335
  ModelName.GEMINI_2_5_PRO: "Gemini 2.5 Pro (Google)",
336
+ ModelName.GEMINI_2_5_FLASH: "Gemini 2.5 Flash (Google)",
337
+ ModelName.GEMINI_2_5_FLASH_LITE: "Gemini 2.5 Flash Lite (Google)",
338
+ ModelName.GEMINI_3_PRO_PREVIEW: "Gemini 3 Pro Preview (Google)",
320
339
  }
321
340
  return names.get(model_name, model_name.value)
322
341
 
@@ -349,4 +368,5 @@ class ModelPickerScreen(Screen[ModelConfigUpdated | None]):
349
368
  )
350
369
  )
351
370
  except Exception as exc: # pragma: no cover - defensive; textual path
352
- self.notify(f"Failed to select model: {exc}", severity="error")
371
+ status_label = self.query_one("#model-picker-status", Label)
372
+ status_label.update(f"❌ Failed to select model: {exc}")
@@ -5,9 +5,12 @@ import webbrowser
5
5
  from textual import on
6
6
  from textual.app import ComposeResult
7
7
  from textual.containers import Container, Horizontal, VerticalScroll
8
+ from textual.events import Resize
8
9
  from textual.screen import ModalScreen
9
10
  from textual.widgets import Button, Markdown, Static
10
11
 
12
+ from shotgun.tui.layout import COMPACT_HEIGHT_THRESHOLD, TINY_HEIGHT_THRESHOLD
13
+
11
14
 
12
15
  class OnboardingModal(ModalScreen[None]):
13
16
  """Multi-page onboarding modal for new users.
@@ -120,6 +123,80 @@ class OnboardingModal(ModalScreen[None]):
120
123
  padding: 0;
121
124
  margin: 2 0 1 0;
122
125
  }
126
+
127
+ /* Tiny screen fallback */
128
+ #tiny-screen-container {
129
+ display: none;
130
+ width: auto;
131
+ height: auto;
132
+ padding: 1 2;
133
+ background: $surface;
134
+ text-align: center;
135
+ }
136
+
137
+ #tiny-screen-message {
138
+ padding: 1 0;
139
+ }
140
+
141
+ #tiny-screen-link {
142
+ padding: 1 0;
143
+ color: $accent;
144
+ }
145
+
146
+ /* Compact styles for short terminals */
147
+ #onboarding-container.compact {
148
+ padding: 1;
149
+ max-height: 98%;
150
+ }
151
+
152
+ #progress-sidebar.compact {
153
+ padding: 0;
154
+ }
155
+
156
+ .progress-item.compact {
157
+ padding: 0;
158
+ }
159
+
160
+ #onboarding-header.compact {
161
+ padding-bottom: 0;
162
+ }
163
+
164
+ #onboarding-content.compact {
165
+ padding: 0;
166
+ }
167
+
168
+ #page-indicator.compact {
169
+ padding: 0;
170
+ }
171
+
172
+ #buttons-container.compact {
173
+ padding: 0;
174
+ }
175
+
176
+ #resource-sections.compact {
177
+ padding: 0;
178
+ }
179
+
180
+ #resource-sections.compact Button {
181
+ margin: 0 0 1 0;
182
+ }
183
+
184
+ #video-section.compact {
185
+ margin: 0;
186
+ }
187
+
188
+ #docs-section.compact {
189
+ margin: 1 0 0 0;
190
+ }
191
+
192
+ /* Tiny mode - hide full onboarding, show minimal message */
193
+ OnboardingModal.tiny #onboarding-container {
194
+ display: none;
195
+ }
196
+
197
+ OnboardingModal.tiny #tiny-screen-container {
198
+ display: block;
199
+ }
123
200
  """
124
201
 
125
202
  BINDINGS = [
@@ -143,6 +220,20 @@ class OnboardingModal(ModalScreen[None]):
143
220
 
144
221
  def compose(self) -> ComposeResult:
145
222
  """Compose the onboarding modal."""
223
+ # Tiny screen fallback - shown when terminal is too small
224
+ with Container(id="tiny-screen-container"):
225
+ yield Static(
226
+ "Your screen is too small for the onboarding wizard.",
227
+ id="tiny-screen-message",
228
+ )
229
+ yield Static(
230
+ "[@click=screen.open_usage_guide]View usage instructions[/]",
231
+ id="tiny-screen-link",
232
+ markup=True,
233
+ )
234
+ yield Button("Start Shotgunning", id="tiny-close-button")
235
+
236
+ # Full onboarding container
146
237
  with Container(id="onboarding-container"):
147
238
  # Left sidebar for progress tracking
148
239
  with Container(id="progress-sidebar"):
@@ -192,6 +283,63 @@ class OnboardingModal(ModalScreen[None]):
192
283
  def on_mount(self) -> None:
193
284
  """Set up the modal after mounting."""
194
285
  self.update_page()
286
+ # Apply layout based on terminal height
287
+ self._apply_layout_for_height(self.app.size.height)
288
+
289
+ @on(Resize)
290
+ def handle_resize(self, event: Resize) -> None:
291
+ """Adjust layout based on terminal height."""
292
+ self._apply_layout_for_height(event.size.height)
293
+
294
+ def _apply_layout_for_height(self, height: int) -> None:
295
+ """Apply appropriate layout based on terminal height."""
296
+ if height < TINY_HEIGHT_THRESHOLD:
297
+ self.add_class("tiny")
298
+ self.remove_class("compact")
299
+ elif height < COMPACT_HEIGHT_THRESHOLD:
300
+ self.remove_class("tiny")
301
+ self._apply_compact_classes(True)
302
+ else:
303
+ self.remove_class("tiny")
304
+ self._apply_compact_classes(False)
305
+
306
+ def _apply_compact_classes(self, compact: bool) -> None:
307
+ """Apply or remove compact layout classes."""
308
+ container = self.query_one("#onboarding-container")
309
+ sidebar = self.query_one("#progress-sidebar")
310
+ header = self.query_one("#onboarding-header")
311
+ content = self.query_one("#onboarding-content")
312
+ page_indicator = self.query_one("#page-indicator")
313
+ buttons_container = self.query_one("#buttons-container")
314
+ resource_sections = self.query_one("#resource-sections")
315
+ progress_items = self.query(".progress-item")
316
+
317
+ if compact:
318
+ container.add_class("compact")
319
+ sidebar.add_class("compact")
320
+ header.add_class("compact")
321
+ content.add_class("compact")
322
+ page_indicator.add_class("compact")
323
+ buttons_container.add_class("compact")
324
+ resource_sections.add_class("compact")
325
+ for item in progress_items:
326
+ item.add_class("compact")
327
+ else:
328
+ container.remove_class("compact")
329
+ sidebar.remove_class("compact")
330
+ header.remove_class("compact")
331
+ content.remove_class("compact")
332
+ page_indicator.remove_class("compact")
333
+ buttons_container.remove_class("compact")
334
+ resource_sections.remove_class("compact")
335
+ for item in progress_items:
336
+ item.remove_class("compact")
337
+
338
+ def action_open_usage_guide(self) -> None:
339
+ """Open the usage guide in browser."""
340
+ webbrowser.open(
341
+ "https://github.com/shotgun-sh/shotgun?tab=readme-ov-file#-usage"
342
+ )
195
343
 
196
344
  def update_page(self) -> None:
197
345
  """Update the displayed page content and navigation buttons."""
@@ -412,6 +560,7 @@ Intelligently compress the conversation history while preserving important conte
412
560
  self.dismiss()
413
561
 
414
562
  @on(Button.Pressed, "#close-button")
563
+ @on(Button.Pressed, "#tiny-close-button")
415
564
  def handle_close(self) -> None:
416
565
  """Handle close button press."""
417
566
  self.dismiss()
@@ -7,8 +7,11 @@ from typing import TYPE_CHECKING
7
7
  from textual import on
8
8
  from textual.app import ComposeResult
9
9
  from textual.containers import Container, Horizontal, VerticalScroll
10
+ from textual.events import Resize
10
11
  from textual.screen import ModalScreen
11
- from textual.widgets import Button, Markdown
12
+ from textual.widgets import Button, Label, Markdown
13
+
14
+ from shotgun.tui.layout import COMPACT_HEIGHT_THRESHOLD
12
15
 
13
16
  if TYPE_CHECKING:
14
17
  pass
@@ -51,6 +54,31 @@ class PipxMigrationScreen(ModalScreen[None]):
51
54
  margin: 0 1;
52
55
  min-width: 20;
53
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
+ }
54
82
  """
55
83
 
56
84
  BINDINGS = [
@@ -106,6 +134,7 @@ Or install permanently: `uv tool install shotgun-sh`
106
134
  )
107
135
 
108
136
  with Container(id="buttons-container"):
137
+ yield Label("", id="migration-status")
109
138
  with Horizontal(id="action-buttons"):
110
139
  yield Button(
111
140
  "Copy Instructions to Clipboard",
@@ -124,6 +153,31 @@ Or install permanently: `uv tool install shotgun-sh`
124
153
  """Focus the continue button and ensure scroll starts at top."""
125
154
  self.query_one("#continue", Button).focus()
126
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")
127
181
 
128
182
  @on(Button.Pressed, "#copy-instructions")
129
183
  def _copy_instructions(self) -> None:
@@ -136,16 +190,14 @@ curl -LsSf https://astral.sh/uv/install.sh | sh
136
190
 
137
191
  # Step 3: Run shotgun with uvx
138
192
  uvx shotgun-sh"""
193
+ status_label = self.query_one("#migration-status", Label)
139
194
  try:
140
195
  import pyperclip # type: ignore[import-untyped] # noqa: PGH003
141
196
 
142
197
  pyperclip.copy(instructions)
143
- self.notify("Copied migration instructions to clipboard!")
198
+ status_label.update("Copied migration instructions to clipboard!")
144
199
  except ImportError:
145
- self.notify(
146
- "Clipboard not available. See instructions above.",
147
- severity="warning",
148
- )
200
+ status_label.update("⚠️ Clipboard not available. See instructions above.")
149
201
 
150
202
  @on(Button.Pressed, "#continue")
151
203
  def _continue(self) -> None:
@@ -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 = [
@@ -103,6 +137,7 @@ class ProviderConfigScreen(Screen[None]):
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")
@@ -122,6 +157,21 @@ class ProviderConfigScreen(Screen[None]):
122
157
  # Refresh UI asynchronously
123
158
  self.run_worker(self._refresh_ui(), exclusive=False)
124
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
+
125
175
  def on_screenresume(self) -> None:
126
176
  """Refresh provider status when screen is resumed.
127
177
 
@@ -280,9 +330,11 @@ class ProviderConfigScreen(Screen[None]):
280
330
  """Async implementation of API key saving."""
281
331
  input_widget = self.query_one("#api-key", Input)
282
332
  api_key = input_widget.value.strip()
333
+ status_label = self.query_one("#provider-status", Label)
283
334
 
284
335
  if not api_key:
285
- 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")
286
338
  return
287
339
 
288
340
  try:
@@ -291,25 +343,29 @@ class ProviderConfigScreen(Screen[None]):
291
343
  api_key=api_key,
292
344
  )
293
345
  except Exception as exc: # pragma: no cover - defensive; textual path
294
- 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")
295
348
  return
296
349
 
297
350
  input_widget.value = ""
298
351
  await self.refresh_provider_status()
299
352
  await self._update_done_button_visibility()
300
- self.notify(
301
- f"Saved API key for {self._provider_display_name(self.selected_provider)}."
353
+ status_label.update(
354
+ f"Saved API key for {self._provider_display_name(self.selected_provider)}."
302
355
  )
356
+ status_label.remove_class("error")
303
357
 
304
358
  def _clear_api_key(self) -> None:
305
359
  self.run_worker(self._do_clear_api_key(), exclusive=True)
306
360
 
307
361
  async def _do_clear_api_key(self) -> None:
308
362
  """Async implementation of API key clearing."""
363
+ status_label = self.query_one("#provider-status", Label)
309
364
  try:
310
365
  await self.config_manager.clear_provider_key(self.selected_provider)
311
366
  except Exception as exc: # pragma: no cover - defensive; textual path
312
- 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")
313
369
  return
314
370
 
315
371
  await self.refresh_provider_status()
@@ -321,9 +377,10 @@ class ProviderConfigScreen(Screen[None]):
321
377
  auth_button = self.query_one("#authenticate", Button)
322
378
  auth_button.display = True
323
379
 
324
- self.notify(
325
- 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)}."
326
382
  )
383
+ status_label.remove_class("error")
327
384
 
328
385
  async def _start_shotgun_auth(self) -> None:
329
386
  """Launch Shotgun Account authentication flow."""
@@ -335,4 +392,5 @@ class ProviderConfigScreen(Screen[None]):
335
392
  # Refresh provider status after auth completes
336
393
  if result:
337
394
  await self.refresh_provider_status()
338
- # Notify handled by auth screen
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
+ ]