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.
- shotgun/agents/agent_manager.py +28 -14
- shotgun/agents/common.py +1 -1
- shotgun/agents/config/README.md +89 -0
- shotgun/agents/config/__init__.py +10 -1
- shotgun/agents/config/manager.py +323 -53
- shotgun/agents/config/models.py +85 -21
- shotgun/agents/config/provider.py +51 -13
- shotgun/agents/config/streaming_test.py +119 -0
- shotgun/agents/context_analyzer/analyzer.py +6 -2
- shotgun/agents/conversation/__init__.py +18 -0
- shotgun/agents/conversation/filters.py +164 -0
- shotgun/agents/conversation/history/chunking.py +278 -0
- shotgun/agents/{history → conversation/history}/compaction.py +27 -1
- shotgun/agents/{history → conversation/history}/constants.py +5 -0
- shotgun/agents/conversation/history/file_content_deduplication.py +216 -0
- shotgun/agents/{history → conversation/history}/history_processors.py +267 -3
- shotgun/agents/{history → conversation/history}/token_counting/anthropic.py +8 -0
- shotgun/agents/{conversation_manager.py → conversation/manager.py} +1 -1
- shotgun/agents/{conversation_history.py → conversation/models.py} +8 -94
- shotgun/agents/error/__init__.py +11 -0
- shotgun/agents/error/models.py +19 -0
- shotgun/agents/runner.py +230 -0
- shotgun/agents/tools/web_search/openai.py +1 -1
- shotgun/build_constants.py +2 -2
- shotgun/cli/clear.py +1 -1
- shotgun/cli/compact.py +5 -3
- shotgun/cli/context.py +44 -1
- shotgun/cli/error_handler.py +24 -0
- shotgun/cli/export.py +34 -34
- shotgun/cli/plan.py +34 -34
- shotgun/cli/research.py +17 -9
- shotgun/cli/spec/__init__.py +5 -0
- shotgun/cli/spec/backup.py +81 -0
- shotgun/cli/spec/commands.py +132 -0
- shotgun/cli/spec/models.py +48 -0
- shotgun/cli/spec/pull_service.py +219 -0
- shotgun/cli/specify.py +20 -19
- shotgun/cli/tasks.py +34 -34
- shotgun/codebase/core/ingestor.py +153 -7
- shotgun/codebase/models.py +2 -0
- shotgun/exceptions.py +325 -0
- shotgun/llm_proxy/__init__.py +17 -0
- shotgun/llm_proxy/client.py +215 -0
- shotgun/llm_proxy/models.py +137 -0
- shotgun/logging_config.py +42 -0
- shotgun/main.py +4 -0
- shotgun/posthog_telemetry.py +1 -1
- shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +28 -3
- shotgun/prompts/agents/partials/interactive_mode.j2 +3 -3
- shotgun/prompts/agents/plan.j2 +16 -0
- shotgun/prompts/agents/research.j2 +16 -3
- shotgun/prompts/agents/specify.j2 +54 -1
- shotgun/prompts/agents/state/system_state.j2 +0 -2
- shotgun/prompts/agents/tasks.j2 +16 -0
- shotgun/prompts/history/chunk_summarization.j2 +34 -0
- shotgun/prompts/history/combine_summaries.j2 +53 -0
- shotgun/sdk/codebase.py +14 -3
- shotgun/settings.py +5 -0
- shotgun/shotgun_web/__init__.py +67 -1
- shotgun/shotgun_web/client.py +42 -1
- shotgun/shotgun_web/constants.py +46 -0
- shotgun/shotgun_web/exceptions.py +29 -0
- shotgun/shotgun_web/models.py +390 -0
- shotgun/shotgun_web/shared_specs/__init__.py +32 -0
- shotgun/shotgun_web/shared_specs/file_scanner.py +175 -0
- shotgun/shotgun_web/shared_specs/hasher.py +83 -0
- shotgun/shotgun_web/shared_specs/models.py +71 -0
- shotgun/shotgun_web/shared_specs/upload_pipeline.py +329 -0
- shotgun/shotgun_web/shared_specs/utils.py +34 -0
- shotgun/shotgun_web/specs_client.py +703 -0
- shotgun/shotgun_web/supabase_client.py +31 -0
- shotgun/tui/app.py +73 -9
- shotgun/tui/containers.py +1 -1
- shotgun/tui/layout.py +5 -0
- shotgun/tui/screens/chat/chat_screen.py +372 -95
- shotgun/tui/screens/chat/codebase_index_prompt_screen.py +196 -17
- shotgun/tui/screens/chat_screen/command_providers.py +13 -2
- shotgun/tui/screens/chat_screen/hint_message.py +76 -1
- shotgun/tui/screens/confirmation_dialog.py +40 -0
- shotgun/tui/screens/directory_setup.py +45 -41
- shotgun/tui/screens/feedback.py +10 -3
- shotgun/tui/screens/github_issue.py +11 -2
- shotgun/tui/screens/model_picker.py +28 -8
- shotgun/tui/screens/onboarding.py +149 -0
- shotgun/tui/screens/pipx_migration.py +58 -6
- shotgun/tui/screens/provider_config.py +66 -8
- shotgun/tui/screens/shared_specs/__init__.py +21 -0
- shotgun/tui/screens/shared_specs/create_spec_dialog.py +273 -0
- shotgun/tui/screens/shared_specs/models.py +56 -0
- shotgun/tui/screens/shared_specs/share_specs_dialog.py +390 -0
- shotgun/tui/screens/shared_specs/upload_progress_screen.py +452 -0
- shotgun/tui/screens/shotgun_auth.py +110 -16
- shotgun/tui/screens/spec_pull.py +288 -0
- shotgun/tui/screens/welcome.py +123 -0
- shotgun/tui/services/conversation_service.py +5 -2
- shotgun/tui/widgets/widget_coordinator.py +1 -1
- {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/METADATA +9 -2
- {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/RECORD +112 -77
- {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/WHEEL +1 -1
- /shotgun/agents/{history → conversation/history}/__init__.py +0 -0
- /shotgun/agents/{history → conversation/history}/context_extraction.py +0 -0
- /shotgun/agents/{history → conversation/history}/history_building.py +0 -0
- /shotgun/agents/{history → conversation/history}/message_utils.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_counting/__init__.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_counting/base.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_counting/openai.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_counting/sentencepiece_counter.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_counting/tokenizer_cache.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_counting/utils.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_estimation.py +0 -0
- {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/entry_points.txt +0 -0
- {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.
|
|
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
|
-
|
|
300
|
-
|
|
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} · {
|
|
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.
|
|
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.
|
|
317
|
-
ModelName.
|
|
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.
|
|
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
|
-
|
|
198
|
+
status_label.update("✓ Copied migration instructions to clipboard!")
|
|
144
199
|
except ImportError:
|
|
145
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
+
]
|