shotgun-sh 0.2.23.dev1__py3-none-any.whl → 0.2.29.dev2__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 (86) hide show
  1. shotgun/agents/agent_manager.py +3 -3
  2. shotgun/agents/common.py +1 -1
  3. shotgun/agents/config/manager.py +36 -21
  4. shotgun/agents/config/models.py +30 -0
  5. shotgun/agents/config/provider.py +27 -14
  6. shotgun/agents/context_analyzer/analyzer.py +6 -2
  7. shotgun/agents/conversation/__init__.py +18 -0
  8. shotgun/agents/conversation/filters.py +164 -0
  9. shotgun/agents/conversation/history/chunking.py +278 -0
  10. shotgun/agents/{history → conversation/history}/compaction.py +27 -1
  11. shotgun/agents/{history → conversation/history}/constants.py +5 -0
  12. shotgun/agents/conversation/history/file_content_deduplication.py +216 -0
  13. shotgun/agents/{history → conversation/history}/history_processors.py +267 -3
  14. shotgun/agents/{conversation_manager.py → conversation/manager.py} +1 -1
  15. shotgun/agents/{conversation_history.py → conversation/models.py} +8 -94
  16. shotgun/agents/tools/web_search/openai.py +1 -1
  17. shotgun/cli/clear.py +1 -1
  18. shotgun/cli/compact.py +5 -3
  19. shotgun/cli/context.py +1 -1
  20. shotgun/cli/spec/__init__.py +5 -0
  21. shotgun/cli/spec/backup.py +81 -0
  22. shotgun/cli/spec/commands.py +130 -0
  23. shotgun/cli/spec/models.py +30 -0
  24. shotgun/cli/spec/pull_service.py +165 -0
  25. shotgun/codebase/core/ingestor.py +153 -7
  26. shotgun/codebase/models.py +2 -0
  27. shotgun/exceptions.py +5 -3
  28. shotgun/main.py +2 -0
  29. shotgun/posthog_telemetry.py +1 -1
  30. shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +3 -3
  31. shotgun/prompts/agents/partials/interactive_mode.j2 +3 -3
  32. shotgun/prompts/agents/research.j2 +0 -3
  33. shotgun/prompts/history/chunk_summarization.j2 +34 -0
  34. shotgun/prompts/history/combine_summaries.j2 +53 -0
  35. shotgun/shotgun_web/__init__.py +67 -1
  36. shotgun/shotgun_web/client.py +42 -1
  37. shotgun/shotgun_web/constants.py +46 -0
  38. shotgun/shotgun_web/exceptions.py +29 -0
  39. shotgun/shotgun_web/models.py +390 -0
  40. shotgun/shotgun_web/shared_specs/__init__.py +32 -0
  41. shotgun/shotgun_web/shared_specs/file_scanner.py +175 -0
  42. shotgun/shotgun_web/shared_specs/hasher.py +83 -0
  43. shotgun/shotgun_web/shared_specs/models.py +71 -0
  44. shotgun/shotgun_web/shared_specs/upload_pipeline.py +291 -0
  45. shotgun/shotgun_web/shared_specs/utils.py +34 -0
  46. shotgun/shotgun_web/specs_client.py +703 -0
  47. shotgun/shotgun_web/supabase_client.py +31 -0
  48. shotgun/tui/app.py +39 -0
  49. shotgun/tui/containers.py +1 -1
  50. shotgun/tui/layout.py +5 -0
  51. shotgun/tui/screens/chat/chat_screen.py +212 -16
  52. shotgun/tui/screens/chat/codebase_index_prompt_screen.py +147 -19
  53. shotgun/tui/screens/chat_screen/command_providers.py +10 -0
  54. shotgun/tui/screens/chat_screen/history/chat_history.py +0 -36
  55. shotgun/tui/screens/confirmation_dialog.py +40 -0
  56. shotgun/tui/screens/model_picker.py +7 -1
  57. shotgun/tui/screens/onboarding.py +149 -0
  58. shotgun/tui/screens/pipx_migration.py +46 -0
  59. shotgun/tui/screens/provider_config.py +41 -0
  60. shotgun/tui/screens/shared_specs/__init__.py +21 -0
  61. shotgun/tui/screens/shared_specs/create_spec_dialog.py +273 -0
  62. shotgun/tui/screens/shared_specs/models.py +56 -0
  63. shotgun/tui/screens/shared_specs/share_specs_dialog.py +390 -0
  64. shotgun/tui/screens/shared_specs/upload_progress_screen.py +452 -0
  65. shotgun/tui/screens/shotgun_auth.py +60 -6
  66. shotgun/tui/screens/spec_pull.py +286 -0
  67. shotgun/tui/screens/welcome.py +91 -0
  68. shotgun/tui/services/conversation_service.py +5 -2
  69. shotgun/tui/widgets/widget_coordinator.py +1 -1
  70. {shotgun_sh-0.2.23.dev1.dist-info → shotgun_sh-0.2.29.dev2.dist-info}/METADATA +1 -1
  71. {shotgun_sh-0.2.23.dev1.dist-info → shotgun_sh-0.2.29.dev2.dist-info}/RECORD +86 -59
  72. {shotgun_sh-0.2.23.dev1.dist-info → shotgun_sh-0.2.29.dev2.dist-info}/WHEEL +1 -1
  73. /shotgun/agents/{history → conversation/history}/__init__.py +0 -0
  74. /shotgun/agents/{history → conversation/history}/context_extraction.py +0 -0
  75. /shotgun/agents/{history → conversation/history}/history_building.py +0 -0
  76. /shotgun/agents/{history → conversation/history}/message_utils.py +0 -0
  77. /shotgun/agents/{history → conversation/history}/token_counting/__init__.py +0 -0
  78. /shotgun/agents/{history → conversation/history}/token_counting/anthropic.py +0 -0
  79. /shotgun/agents/{history → conversation/history}/token_counting/base.py +0 -0
  80. /shotgun/agents/{history → conversation/history}/token_counting/openai.py +0 -0
  81. /shotgun/agents/{history → conversation/history}/token_counting/sentencepiece_counter.py +0 -0
  82. /shotgun/agents/{history → conversation/history}/token_counting/tokenizer_cache.py +0 -0
  83. /shotgun/agents/{history → conversation/history}/token_counting/utils.py +0 -0
  84. /shotgun/agents/{history → conversation/history}/token_estimation.py +0 -0
  85. {shotgun_sh-0.2.23.dev1.dist-info → shotgun_sh-0.2.29.dev2.dist-info}/entry_points.txt +0 -0
  86. {shotgun_sh-0.2.23.dev1.dist-info → shotgun_sh-0.2.29.dev2.dist-info}/licenses/LICENSE +0 -0
@@ -1,16 +1,37 @@
1
1
  """Modal dialog for codebase indexing prompts."""
2
2
 
3
+ import os
4
+ import webbrowser
3
5
  from pathlib import Path
4
6
 
5
7
  from textual import on
6
8
  from textual.app import ComposeResult
7
9
  from textual.containers import Container, VerticalScroll
10
+ from textual.events import Resize
8
11
  from textual.screen import ModalScreen
9
- from textual.widgets import Button, Label, Markdown
12
+ from textual.widgets import Button, Label, Markdown, Static
10
13
 
14
+ from shotgun.tui.layout import COMPACT_HEIGHT_THRESHOLD
11
15
  from shotgun.utils.file_system_utils import get_shotgun_home
12
16
 
13
17
 
18
+ def _is_home_directory() -> bool:
19
+ """Check if cwd is user's home directory.
20
+
21
+ Can be simulated with HOME_DIRECTORY_SIMULATE=true env var for testing.
22
+ """
23
+ if os.environ.get("HOME_DIRECTORY_SIMULATE", "").lower() == "true":
24
+ return True
25
+ return Path.cwd() == Path.home()
26
+
27
+
28
+ def _track_event(event_name: str) -> None:
29
+ """Track an event to PostHog."""
30
+ from shotgun.posthog_telemetry import track_event
31
+
32
+ track_event(event_name)
33
+
34
+
14
35
  class CodebaseIndexPromptScreen(ModalScreen[bool]):
15
36
  """Modal dialog asking whether to index the detected codebase."""
16
37
 
@@ -58,14 +79,79 @@ class CodebaseIndexPromptScreen(ModalScreen[bool]):
58
79
  margin: 0 1;
59
80
  min-width: 12;
60
81
  }
82
+
83
+ #index-prompt-warning {
84
+ background: $surface-lighten-1;
85
+ color: $text;
86
+ padding: 1 2;
87
+ margin-bottom: 1;
88
+ text-align: center;
89
+ }
90
+
91
+ #compact-link {
92
+ text-align: center;
93
+ padding: 1 0;
94
+ display: none;
95
+ }
96
+
97
+ /* Compact styles for short terminals */
98
+ #index-prompt-dialog.compact {
99
+ padding: 0 1;
100
+ border: none;
101
+ max-height: 100%;
102
+ }
103
+
104
+ #index-prompt-dialog.compact #index-prompt-content {
105
+ display: none;
106
+ }
107
+
108
+ #index-prompt-dialog.compact #compact-link {
109
+ display: block;
110
+ }
111
+
112
+ #index-prompt-dialog.compact #index-prompt-warning {
113
+ padding: 0;
114
+ margin-bottom: 0;
115
+ background: transparent;
116
+ }
117
+
118
+ #index-prompt-dialog.compact #index-prompt-title {
119
+ padding-bottom: 0;
120
+ }
121
+
122
+ #index-prompt-dialog.compact #index-prompt-buttons {
123
+ padding-top: 0;
124
+ }
61
125
  """
62
126
 
63
127
  def compose(self) -> ComposeResult:
64
128
  storage_path = get_shotgun_home() / "codebases"
65
129
  cwd = Path.cwd()
130
+ is_home = _is_home_directory()
66
131
 
67
- # Build the markdown content with privacy-first messaging
68
- content = f"""
132
+ with Container(id="index-prompt-dialog"):
133
+ if is_home:
134
+ # Show warning for home directory
135
+ yield Label(
136
+ "Home directory detected",
137
+ id="index-prompt-title",
138
+ )
139
+ yield Static(
140
+ "Running from home directory isn't recommended.",
141
+ id="index-prompt-warning",
142
+ )
143
+ with Container(id="index-prompt-buttons"):
144
+ yield Button(
145
+ "Quit",
146
+ id="index-prompt-quit",
147
+ )
148
+ yield Button(
149
+ "Continue without indexing",
150
+ id="index-prompt-continue",
151
+ )
152
+ else:
153
+ # Normal indexing prompt
154
+ content = f"""
69
155
  ## 🔒 Your code never leaves your computer
70
156
 
71
157
  Shotgun will index the codebase at:
@@ -85,24 +171,53 @@ If you're curious, you can review how Shotgun indexes/queries code by taking a l
85
171
 
86
172
  We take your privacy seriously. You can read our full [privacy policy](https://app.shotgun.sh/privacy) for more details.
87
173
  """
88
-
89
- with Container(id="index-prompt-dialog"):
90
- yield Label(
91
- "Want to index your codebase so Shotgun can understand it?",
92
- id="index-prompt-title",
93
- )
94
- with VerticalScroll(id="index-prompt-content"):
95
- yield Markdown(content, id="index-prompt-info")
96
- with Container(id="index-prompt-buttons"):
97
- yield Button(
98
- "Not now",
99
- id="index-prompt-cancel",
174
+ yield Label(
175
+ "Want to index your codebase?",
176
+ id="index-prompt-title",
100
177
  )
101
- yield Button(
102
- "Index now",
103
- id="index-prompt-confirm",
104
- variant="primary",
178
+ # Compact mode: show only a link
179
+ yield Static(
180
+ "[@click=screen.open_faq]Learn more about indexing[/]",
181
+ id="compact-link",
182
+ markup=True,
105
183
  )
184
+ # Full mode: show detailed content
185
+ with VerticalScroll(id="index-prompt-content"):
186
+ yield Markdown(content, id="index-prompt-info")
187
+ with Container(id="index-prompt-buttons"):
188
+ yield Button(
189
+ "Not now",
190
+ id="index-prompt-cancel",
191
+ )
192
+ yield Button(
193
+ "Index now",
194
+ id="index-prompt-confirm",
195
+ variant="primary",
196
+ )
197
+
198
+ def on_mount(self) -> None:
199
+ """Track when the home directory warning screen is shown and apply compact layout."""
200
+ if _is_home_directory():
201
+ _track_event("home_directory_warning_shown")
202
+ # Apply compact layout if starting in a short terminal
203
+ self._apply_compact_layout(self.app.size.height < COMPACT_HEIGHT_THRESHOLD)
204
+
205
+ @on(Resize)
206
+ def handle_resize(self, event: Resize) -> None:
207
+ """Adjust layout based on terminal height."""
208
+ self._apply_compact_layout(event.size.height < COMPACT_HEIGHT_THRESHOLD)
209
+
210
+ def _apply_compact_layout(self, compact: bool) -> None:
211
+ """Apply or remove compact layout classes for short terminals."""
212
+ dialog = self.query_one("#index-prompt-dialog")
213
+ if compact:
214
+ dialog.add_class("compact")
215
+ else:
216
+ dialog.remove_class("compact")
217
+
218
+ def action_open_faq(self) -> None:
219
+ """Open the FAQ page in a browser."""
220
+ webbrowser.open("https://github.com/shotgun-sh/shotgun?tab=readme-ov-file#faq")
106
221
 
107
222
  @on(Button.Pressed, "#index-prompt-cancel")
108
223
  def handle_cancel(self, event: Button.Pressed) -> None:
@@ -113,3 +228,16 @@ We take your privacy seriously. You can read our full [privacy policy](https://a
113
228
  def handle_confirm(self, event: Button.Pressed) -> None:
114
229
  event.stop()
115
230
  self.dismiss(True)
231
+
232
+ @on(Button.Pressed, "#index-prompt-continue")
233
+ def handle_continue(self, event: Button.Pressed) -> None:
234
+ """Continue without indexing when in home directory."""
235
+ event.stop()
236
+ _track_event("home_directory_warning_continue")
237
+ self.dismiss(False)
238
+
239
+ @on(Button.Pressed, "#index-prompt-quit")
240
+ def handle_quit(self, event: Button.Pressed) -> None:
241
+ event.stop()
242
+ _track_event("home_directory_warning_quit")
243
+ self.app.exit()
@@ -360,6 +360,11 @@ class UnifiedCommandProvider(Provider):
360
360
  self.open_model_picker,
361
361
  help="🤖 Choose which AI model to use",
362
362
  )
363
+ yield DiscoveryHit(
364
+ "Share specs to workspace",
365
+ self.chat_screen.share_specs_command,
366
+ help="📤 Upload .shotgun/ files to share with your team",
367
+ )
363
368
  yield DiscoveryHit(
364
369
  "Show context",
365
370
  self.chat_screen.action_show_context,
@@ -412,6 +417,11 @@ class UnifiedCommandProvider(Provider):
412
417
  self.open_model_picker,
413
418
  "🤖 Choose which AI model to use",
414
419
  ),
420
+ (
421
+ "Share specs to workspace",
422
+ self.chat_screen.share_specs_command,
423
+ "📤 Upload .shotgun/ files to share with your team",
424
+ ),
415
425
  (
416
426
  "Show context",
417
427
  self.chat_screen.action_show_context,
@@ -92,42 +92,6 @@ class ChatHistory(Widget):
92
92
  self.items = messages
93
93
  filtered = list(self.filtered_items())
94
94
 
95
- # Handle case where streaming inflated _rendered_count but final messages differ
96
- # This happens when error replaces ModelResponse with HintMessage
97
- if len(filtered) <= self._rendered_count and filtered:
98
- # Check if the last rendered item type differs from what should be there
99
- # Children: [UserQuestion, AgentResponse, ..., PartialResponse]
100
- # We need to check the item before PartialResponseWidget
101
- num_children = len(self.vertical_tail.children)
102
- if num_children > 1: # Has items besides PartialResponseWidget
103
- last_widget = self.vertical_tail.children[-2] # Item before Partial
104
- last_filtered = filtered[-1]
105
-
106
- # Check type mismatch
107
- type_mismatch = (
108
- (
109
- isinstance(last_widget, AgentResponseWidget)
110
- and isinstance(last_filtered, HintMessage)
111
- )
112
- or (
113
- isinstance(last_widget, HintMessageWidget)
114
- and isinstance(last_filtered, ModelResponse)
115
- )
116
- or (
117
- isinstance(last_widget, AgentResponseWidget)
118
- and isinstance(last_filtered, ModelRequest)
119
- )
120
- or (
121
- isinstance(last_widget, UserQuestionWidget)
122
- and not isinstance(last_filtered, ModelRequest)
123
- )
124
- )
125
-
126
- if type_mismatch:
127
- # Remove the mismatched widget and adjust count
128
- last_widget.remove()
129
- self._rendered_count = len(filtered) - 1
130
-
131
95
  # Only mount new messages that haven't been rendered yet
132
96
  if len(filtered) > self._rendered_count:
133
97
  new_messages = filtered[self._rendered_count :]
@@ -5,9 +5,12 @@ from typing import Literal
5
5
  from textual import on
6
6
  from textual.app import ComposeResult
7
7
  from textual.containers import Container
8
+ from textual.events import Resize
8
9
  from textual.screen import ModalScreen
9
10
  from textual.widgets import Button, Label, Static
10
11
 
12
+ from shotgun.tui.layout import COMPACT_HEIGHT_THRESHOLD
13
+
11
14
  ButtonVariant = Literal["default", "primary", "success", "warning", "error"]
12
15
 
13
16
 
@@ -87,6 +90,20 @@ class ConfirmationDialog(ModalScreen[bool]):
87
90
  #dialog-buttons Button {
88
91
  margin-left: 1;
89
92
  }
93
+
94
+ /* Compact styles for short terminals */
95
+ #dialog-container.compact {
96
+ padding: 0 2;
97
+ max-height: 98%;
98
+ }
99
+
100
+ #dialog-title.compact {
101
+ padding-bottom: 0;
102
+ }
103
+
104
+ #dialog-message.compact {
105
+ padding-bottom: 0;
106
+ }
90
107
  """
91
108
 
92
109
  def __init__(
@@ -138,6 +155,29 @@ class ConfirmationDialog(ModalScreen[bool]):
138
155
  # Focus cancel button by default for safety
139
156
  self.query_one("#cancel", Button).focus()
140
157
 
158
+ # Apply compact layout if starting in a short terminal
159
+ self._apply_compact_layout(self.app.size.height < COMPACT_HEIGHT_THRESHOLD)
160
+
161
+ @on(Resize)
162
+ def handle_resize(self, event: Resize) -> None:
163
+ """Adjust layout based on terminal height."""
164
+ self._apply_compact_layout(event.size.height < COMPACT_HEIGHT_THRESHOLD)
165
+
166
+ def _apply_compact_layout(self, compact: bool) -> None:
167
+ """Apply or remove compact layout classes for short terminals."""
168
+ container = self.query_one("#dialog-container")
169
+ title = self.query_one("#dialog-title")
170
+ message = self.query_one("#dialog-message")
171
+
172
+ if compact:
173
+ container.add_class("compact")
174
+ title.add_class("compact")
175
+ message.add_class("compact")
176
+ else:
177
+ container.remove_class("compact")
178
+ title.remove_class("compact")
179
+ message.remove_class("compact")
180
+
141
181
  @on(Button.Pressed, "#cancel")
142
182
  def handle_cancel(self, event: Button.Pressed) -> None:
143
183
  """Handle cancel button press."""
@@ -89,7 +89,7 @@ class ModelPickerScreen(Screen[ModelConfigUpdated | None]):
89
89
  ("escape", "done", "Back"),
90
90
  ]
91
91
 
92
- selected_model: reactive[ModelName] = reactive(ModelName.GPT_5)
92
+ selected_model: reactive[ModelName] = reactive(ModelName.GPT_5_1)
93
93
 
94
94
  def compose(self) -> ComposeResult:
95
95
  with Vertical(id="titlebox"):
@@ -320,9 +320,15 @@ class ModelPickerScreen(Screen[ModelConfigUpdated | None]):
320
320
  """Get human-readable model name."""
321
321
  names = {
322
322
  ModelName.GPT_5: "GPT-5 (OpenAI)",
323
+ ModelName.GPT_5_MINI: "GPT-5 Mini (OpenAI)",
324
+ ModelName.GPT_5_1: "GPT-5.1 (OpenAI)",
325
+ ModelName.GPT_5_1_CODEX: "GPT-5.1 Codex (OpenAI)",
326
+ ModelName.GPT_5_1_CODEX_MINI: "GPT-5.1 Codex Mini (OpenAI)",
323
327
  ModelName.CLAUDE_OPUS_4_1: "Claude Opus 4.1 (Anthropic)",
324
328
  ModelName.CLAUDE_SONNET_4_5: "Claude Sonnet 4.5 (Anthropic)",
329
+ ModelName.CLAUDE_HAIKU_4_5: "Claude Haiku 4.5 (Anthropic)",
325
330
  ModelName.GEMINI_2_5_PRO: "Gemini 2.5 Pro (Google)",
331
+ ModelName.GEMINI_2_5_FLASH: "Gemini 2.5 Flash (Google)",
326
332
  }
327
333
  return names.get(model_name, model_name.value)
328
334
 
@@ -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,9 +7,12 @@ 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
12
  from textual.widgets import Button, Label, Markdown
12
13
 
14
+ from shotgun.tui.layout import COMPACT_HEIGHT_THRESHOLD
15
+
13
16
  if TYPE_CHECKING:
14
17
  pass
15
18
 
@@ -58,6 +61,24 @@ class PipxMigrationScreen(ModalScreen[None]):
58
61
  min-height: 1;
59
62
  text-align: center;
60
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
+ }
61
82
  """
62
83
 
63
84
  BINDINGS = [
@@ -132,6 +153,31 @@ Or install permanently: `uv tool install shotgun-sh`
132
153
  """Focus the continue button and ensure scroll starts at top."""
133
154
  self.query_one("#continue", Button).focus()
134
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")
135
181
 
136
182
  @on(Button.Pressed, "#copy-instructions")
137
183
  def _copy_instructions(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
@@ -85,6 +87,30 @@ class ProviderConfigScreen(Screen[None]):
85
87
  #provider-status.error {
86
88
  color: $error;
87
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
+ }
88
114
  """
89
115
 
90
116
  BINDINGS = [
@@ -131,6 +157,21 @@ class ProviderConfigScreen(Screen[None]):
131
157
  # Refresh UI asynchronously
132
158
  self.run_worker(self._refresh_ui(), exclusive=False)
133
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
+
134
175
  def on_screenresume(self) -> None:
135
176
  """Refresh provider status when screen is resumed.
136
177