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.
- shotgun/agents/agent_manager.py +3 -3
- shotgun/agents/common.py +1 -1
- shotgun/agents/config/manager.py +36 -21
- shotgun/agents/config/models.py +30 -0
- shotgun/agents/config/provider.py +27 -14
- 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/{conversation_manager.py → conversation/manager.py} +1 -1
- shotgun/agents/{conversation_history.py → conversation/models.py} +8 -94
- shotgun/agents/tools/web_search/openai.py +1 -1
- shotgun/cli/clear.py +1 -1
- shotgun/cli/compact.py +5 -3
- shotgun/cli/context.py +1 -1
- shotgun/cli/spec/__init__.py +5 -0
- shotgun/cli/spec/backup.py +81 -0
- shotgun/cli/spec/commands.py +130 -0
- shotgun/cli/spec/models.py +30 -0
- shotgun/cli/spec/pull_service.py +165 -0
- shotgun/codebase/core/ingestor.py +153 -7
- shotgun/codebase/models.py +2 -0
- shotgun/exceptions.py +5 -3
- shotgun/main.py +2 -0
- shotgun/posthog_telemetry.py +1 -1
- shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +3 -3
- shotgun/prompts/agents/partials/interactive_mode.j2 +3 -3
- shotgun/prompts/agents/research.j2 +0 -3
- shotgun/prompts/history/chunk_summarization.j2 +34 -0
- shotgun/prompts/history/combine_summaries.j2 +53 -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 +291 -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 +39 -0
- shotgun/tui/containers.py +1 -1
- shotgun/tui/layout.py +5 -0
- shotgun/tui/screens/chat/chat_screen.py +212 -16
- shotgun/tui/screens/chat/codebase_index_prompt_screen.py +147 -19
- shotgun/tui/screens/chat_screen/command_providers.py +10 -0
- shotgun/tui/screens/chat_screen/history/chat_history.py +0 -36
- shotgun/tui/screens/confirmation_dialog.py +40 -0
- shotgun/tui/screens/model_picker.py +7 -1
- shotgun/tui/screens/onboarding.py +149 -0
- shotgun/tui/screens/pipx_migration.py +46 -0
- shotgun/tui/screens/provider_config.py +41 -0
- 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 +60 -6
- shotgun/tui/screens/spec_pull.py +286 -0
- shotgun/tui/screens/welcome.py +91 -0
- shotgun/tui/services/conversation_service.py +5 -2
- shotgun/tui/widgets/widget_coordinator.py +1 -1
- {shotgun_sh-0.2.23.dev1.dist-info → shotgun_sh-0.2.29.dev2.dist-info}/METADATA +1 -1
- {shotgun_sh-0.2.23.dev1.dist-info → shotgun_sh-0.2.29.dev2.dist-info}/RECORD +86 -59
- {shotgun_sh-0.2.23.dev1.dist-info → shotgun_sh-0.2.29.dev2.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/anthropic.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.23.dev1.dist-info → shotgun_sh-0.2.29.dev2.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
|
68
|
-
|
|
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
|
-
|
|
90
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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.
|
|
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
|
|