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
|
@@ -1,12 +1,35 @@
|
|
|
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
|
-
from textual.containers import Container
|
|
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, Static
|
|
12
|
+
from textual.widgets import Button, Label, Markdown, Static
|
|
13
|
+
|
|
14
|
+
from shotgun.tui.layout import COMPACT_HEIGHT_THRESHOLD
|
|
15
|
+
from shotgun.utils.file_system_utils import get_shotgun_home
|
|
16
|
+
|
|
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)
|
|
10
33
|
|
|
11
34
|
|
|
12
35
|
class CodebaseIndexPromptScreen(ModalScreen[bool]):
|
|
@@ -19,39 +42,182 @@ class CodebaseIndexPromptScreen(ModalScreen[bool]):
|
|
|
19
42
|
}
|
|
20
43
|
|
|
21
44
|
CodebaseIndexPromptScreen > #index-prompt-dialog {
|
|
22
|
-
width:
|
|
23
|
-
max-width:
|
|
45
|
+
width: 80%;
|
|
46
|
+
max-width: 90;
|
|
24
47
|
height: auto;
|
|
48
|
+
max-height: 85%;
|
|
25
49
|
border: wide $primary;
|
|
26
50
|
padding: 1 2;
|
|
27
51
|
layout: vertical;
|
|
28
52
|
background: $surface;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
#index-prompt-title {
|
|
56
|
+
text-style: bold;
|
|
57
|
+
color: $text-accent;
|
|
58
|
+
text-align: center;
|
|
59
|
+
padding-bottom: 1;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
#index-prompt-content {
|
|
29
63
|
height: auto;
|
|
64
|
+
max-height: 1fr;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
#index-prompt-info {
|
|
68
|
+
padding: 0 1;
|
|
30
69
|
}
|
|
31
70
|
|
|
32
71
|
#index-prompt-buttons {
|
|
33
72
|
layout: horizontal;
|
|
34
73
|
align-horizontal: right;
|
|
35
74
|
height: auto;
|
|
75
|
+
padding-top: 1;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
#index-prompt-buttons Button {
|
|
79
|
+
margin: 0 1;
|
|
80
|
+
min-width: 12;
|
|
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;
|
|
36
124
|
}
|
|
37
125
|
"""
|
|
38
126
|
|
|
39
127
|
def compose(self) -> ComposeResult:
|
|
128
|
+
storage_path = get_shotgun_home() / "codebases"
|
|
129
|
+
cwd = Path.cwd()
|
|
130
|
+
is_home = _is_home_directory()
|
|
131
|
+
|
|
40
132
|
with Container(id="index-prompt-dialog"):
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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"""
|
|
155
|
+
## 🔒 Your code never leaves your computer
|
|
156
|
+
|
|
157
|
+
Shotgun will index the codebase at:
|
|
158
|
+
**`{cwd}`**
|
|
159
|
+
_(This is the current working directory where you started Shotgun)_
|
|
160
|
+
|
|
161
|
+
### What happens during indexing:
|
|
162
|
+
|
|
163
|
+
- **Stays on your computer**: Index is stored locally at `{storage_path}` - it will not be stored on a server
|
|
164
|
+
- **Zero cost**: Indexing runs entirely on your machine
|
|
165
|
+
- **Runs in the background**: Usually takes 1-3 minutes, and you can continue using Shotgun while it indexes
|
|
166
|
+
- **Enable code understanding**: Allows Shotgun to answer questions about your codebase
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
If you're curious, you can review how Shotgun indexes/queries code by taking a look at the [source code](https://github.com/shotgun-sh/shotgun).
|
|
171
|
+
|
|
172
|
+
We take your privacy seriously. You can read our full [privacy policy](https://app.shotgun.sh/privacy) for more details.
|
|
173
|
+
"""
|
|
174
|
+
yield Label(
|
|
175
|
+
"Want to index your codebase?",
|
|
176
|
+
id="index-prompt-title",
|
|
177
|
+
)
|
|
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,
|
|
53
183
|
)
|
|
54
|
-
|
|
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")
|
|
55
221
|
|
|
56
222
|
@on(Button.Pressed, "#index-prompt-cancel")
|
|
57
223
|
def handle_cancel(self, event: Button.Pressed) -> None:
|
|
@@ -62,3 +228,16 @@ class CodebaseIndexPromptScreen(ModalScreen[bool]):
|
|
|
62
228
|
def handle_confirm(self, event: Button.Pressed) -> None:
|
|
63
229
|
event.stop()
|
|
64
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()
|
|
@@ -5,6 +5,7 @@ from textual.command import DiscoveryHit, Hit, Provider
|
|
|
5
5
|
|
|
6
6
|
from shotgun.agents.models import AgentType
|
|
7
7
|
from shotgun.codebase.models import CodebaseGraph
|
|
8
|
+
from shotgun.tui.screens.chat_screen.hint_message import HintMessage
|
|
8
9
|
from shotgun.tui.screens.model_picker import ModelPickerScreen
|
|
9
10
|
from shotgun.tui.screens.provider_config import ProviderConfigScreen
|
|
10
11
|
|
|
@@ -271,8 +272,8 @@ class DeleteCodebasePaletteProvider(Provider):
|
|
|
271
272
|
try:
|
|
272
273
|
result = await self.chat_screen.codebase_sdk.list_codebases()
|
|
273
274
|
except Exception as exc: # pragma: no cover - defensive UI path
|
|
274
|
-
self.chat_screen.
|
|
275
|
-
f"Unable to load codebases: {exc}"
|
|
275
|
+
self.chat_screen.agent_manager.add_hint_message(
|
|
276
|
+
HintMessage(message=f"❌ Unable to load codebases: {exc}")
|
|
276
277
|
)
|
|
277
278
|
return []
|
|
278
279
|
return result.graphs
|
|
@@ -359,6 +360,11 @@ class UnifiedCommandProvider(Provider):
|
|
|
359
360
|
self.open_model_picker,
|
|
360
361
|
help="🤖 Choose which AI model to use",
|
|
361
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
|
+
)
|
|
362
368
|
yield DiscoveryHit(
|
|
363
369
|
"Show context",
|
|
364
370
|
self.chat_screen.action_show_context,
|
|
@@ -411,6 +417,11 @@ class UnifiedCommandProvider(Provider):
|
|
|
411
417
|
self.open_model_picker,
|
|
412
418
|
"🤖 Choose which AI model to use",
|
|
413
419
|
),
|
|
420
|
+
(
|
|
421
|
+
"Share specs to workspace",
|
|
422
|
+
self.chat_screen.share_specs_command,
|
|
423
|
+
"📤 Upload .shotgun/ files to share with your team",
|
|
424
|
+
),
|
|
414
425
|
(
|
|
415
426
|
"Show context",
|
|
416
427
|
self.chat_screen.action_show_context,
|
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
from typing import Literal
|
|
2
2
|
|
|
3
3
|
from pydantic import BaseModel
|
|
4
|
+
from textual import on
|
|
4
5
|
from textual.app import ComposeResult
|
|
6
|
+
from textual.containers import Horizontal
|
|
5
7
|
from textual.widget import Widget
|
|
6
|
-
from textual.widgets import Markdown
|
|
8
|
+
from textual.widgets import Button, Label, Markdown, Static
|
|
9
|
+
|
|
10
|
+
from shotgun.logging_config import get_logger
|
|
11
|
+
|
|
12
|
+
logger = get_logger(__name__)
|
|
7
13
|
|
|
8
14
|
|
|
9
15
|
class HintMessage(BaseModel):
|
|
10
16
|
message: str
|
|
11
17
|
kind: Literal["hint"] = "hint"
|
|
18
|
+
# Optional email copy functionality
|
|
19
|
+
email: str | None = None
|
|
20
|
+
markdown_after: str | None = None
|
|
12
21
|
|
|
13
22
|
|
|
14
23
|
class HintMessageWidget(Widget):
|
|
@@ -30,6 +39,30 @@ class HintMessageWidget(Widget):
|
|
|
30
39
|
}
|
|
31
40
|
}
|
|
32
41
|
|
|
42
|
+
HintMessageWidget .email-copy-row {
|
|
43
|
+
width: auto;
|
|
44
|
+
height: auto;
|
|
45
|
+
margin: 1 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
HintMessageWidget .email-text {
|
|
49
|
+
width: auto;
|
|
50
|
+
margin-right: 1;
|
|
51
|
+
content-align: left middle;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
HintMessageWidget .copy-btn {
|
|
55
|
+
width: auto;
|
|
56
|
+
min-width: 12;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
HintMessageWidget #copy-status {
|
|
60
|
+
height: 1;
|
|
61
|
+
width: 100%;
|
|
62
|
+
margin-top: 1;
|
|
63
|
+
content-align: left middle;
|
|
64
|
+
}
|
|
65
|
+
|
|
33
66
|
"""
|
|
34
67
|
|
|
35
68
|
def __init__(self, message: HintMessage) -> None:
|
|
@@ -37,4 +70,46 @@ class HintMessageWidget(Widget):
|
|
|
37
70
|
self.message = message
|
|
38
71
|
|
|
39
72
|
def compose(self) -> ComposeResult:
|
|
73
|
+
# Main message markdown
|
|
40
74
|
yield Markdown(markdown=f"{self.message.message}")
|
|
75
|
+
|
|
76
|
+
# Optional email copy section
|
|
77
|
+
if self.message.email:
|
|
78
|
+
# Email + copy button on same line
|
|
79
|
+
with Horizontal(classes="email-copy-row"):
|
|
80
|
+
yield Static(f"Contact: {self.message.email}", classes="email-text")
|
|
81
|
+
yield Button("Copy email", id="copy-email-btn", classes="copy-btn")
|
|
82
|
+
|
|
83
|
+
# Status feedback label
|
|
84
|
+
yield Label("", id="copy-status")
|
|
85
|
+
|
|
86
|
+
# Optional markdown after email
|
|
87
|
+
if self.message.markdown_after:
|
|
88
|
+
yield Markdown(self.message.markdown_after)
|
|
89
|
+
|
|
90
|
+
@on(Button.Pressed, "#copy-email-btn")
|
|
91
|
+
def _copy_email(self) -> None:
|
|
92
|
+
"""Copy email address to clipboard when button is pressed."""
|
|
93
|
+
if not self.message.email:
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
status_label = self.query_one("#copy-status", Label)
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
import pyperclip # type: ignore[import-untyped] # noqa: PGH003
|
|
100
|
+
|
|
101
|
+
pyperclip.copy(self.message.email)
|
|
102
|
+
status_label.update("✓ Copied to clipboard!")
|
|
103
|
+
logger.debug(
|
|
104
|
+
f"Successfully copied email to clipboard: {self.message.email}"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
except ImportError:
|
|
108
|
+
status_label.update(
|
|
109
|
+
f"⚠️ Clipboard unavailable. Please manually copy: {self.message.email}"
|
|
110
|
+
)
|
|
111
|
+
logger.warning("pyperclip not available for clipboard operations")
|
|
112
|
+
|
|
113
|
+
except Exception as e:
|
|
114
|
+
status_label.update(f"⚠️ Copy failed: {e}")
|
|
115
|
+
logger.error(f"Failed to copy email to clipboard: {e}", exc_info=True)
|
|
@@ -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."""
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Screen for
|
|
1
|
+
"""Screen for displaying .shotgun directory creation errors."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -8,13 +8,20 @@ from textual import on
|
|
|
8
8
|
from textual.app import ComposeResult
|
|
9
9
|
from textual.containers import Horizontal, Vertical
|
|
10
10
|
from textual.screen import Screen
|
|
11
|
-
from textual.widgets import Button, Static
|
|
12
|
-
|
|
13
|
-
from shotgun.utils.file_system_utils import ensure_shotgun_directory_exists
|
|
11
|
+
from textual.widgets import Button, Label, Static
|
|
14
12
|
|
|
15
13
|
|
|
16
14
|
class DirectorySetupScreen(Screen[None]):
|
|
17
|
-
"""
|
|
15
|
+
"""Display an error when .shotgun directory creation fails."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, error_message: str) -> None:
|
|
18
|
+
"""Initialize the error screen.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
error_message: The error message to display to the user.
|
|
22
|
+
"""
|
|
23
|
+
super().__init__()
|
|
24
|
+
self.error_message = error_message
|
|
18
25
|
|
|
19
26
|
CSS = """
|
|
20
27
|
DirectorySetupScreen {
|
|
@@ -56,58 +63,55 @@ class DirectorySetupScreen(Screen[None]):
|
|
|
56
63
|
#directory-actions > * {
|
|
57
64
|
margin-right: 2;
|
|
58
65
|
}
|
|
66
|
+
|
|
67
|
+
#directory-status {
|
|
68
|
+
height: auto;
|
|
69
|
+
padding: 0 1;
|
|
70
|
+
min-height: 1;
|
|
71
|
+
color: $error;
|
|
72
|
+
text-align: center;
|
|
73
|
+
}
|
|
59
74
|
"""
|
|
60
75
|
|
|
61
76
|
BINDINGS = [
|
|
62
|
-
("enter", "
|
|
77
|
+
("enter", "retry", "Retry"),
|
|
63
78
|
("escape", "cancel", "Exit"),
|
|
64
79
|
]
|
|
65
80
|
|
|
66
81
|
def compose(self) -> ComposeResult:
|
|
67
82
|
with Vertical(id="titlebox"):
|
|
68
|
-
yield Static(
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
yield Static(
|
|
72
|
-
|
|
73
|
-
yield
|
|
74
|
-
|
|
83
|
+
yield Static(
|
|
84
|
+
"Failed to create .shotgun directory", id="directory-setup-title"
|
|
85
|
+
)
|
|
86
|
+
yield Static("Shotgun was unable to create the .shotgun directory in:\n")
|
|
87
|
+
yield Static(f"[$foreground-muted]({Path.cwd().resolve()})[/]\n")
|
|
88
|
+
yield Static(f"[bold red]Error:[/] {self.error_message}\n")
|
|
89
|
+
yield Static(
|
|
90
|
+
"This directory is required for storing workspace data. "
|
|
91
|
+
"Please check permissions and try again."
|
|
75
92
|
)
|
|
76
|
-
|
|
93
|
+
yield Label("", id="directory-status")
|
|
94
|
+
with Horizontal(id="directory-actions"):
|
|
95
|
+
yield Button("Retry \\[ENTER]", variant="primary", id="retry")
|
|
96
|
+
yield Button("Exit \\[ESC]", variant="default", id="exit")
|
|
77
97
|
|
|
78
98
|
def on_mount(self) -> None:
|
|
79
|
-
self.set_focus(self.query_one("#
|
|
99
|
+
self.set_focus(self.query_one("#retry", Button))
|
|
80
100
|
|
|
81
|
-
def
|
|
82
|
-
|
|
101
|
+
def action_retry(self) -> None:
|
|
102
|
+
"""Retry by dismissing the screen, which will trigger refresh_startup_screen."""
|
|
103
|
+
self.dismiss()
|
|
83
104
|
|
|
84
105
|
def action_cancel(self) -> None:
|
|
85
|
-
|
|
106
|
+
"""Exit the application."""
|
|
107
|
+
self.app.exit()
|
|
86
108
|
|
|
87
|
-
@on(Button.Pressed, "#
|
|
88
|
-
def
|
|
89
|
-
|
|
109
|
+
@on(Button.Pressed, "#retry")
|
|
110
|
+
def _on_retry_pressed(self) -> None:
|
|
111
|
+
"""Retry by dismissing the screen."""
|
|
112
|
+
self.dismiss()
|
|
90
113
|
|
|
91
114
|
@on(Button.Pressed, "#exit")
|
|
92
115
|
def _on_exit_pressed(self) -> None:
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def _initialize_directory(self) -> None:
|
|
96
|
-
try:
|
|
97
|
-
path = ensure_shotgun_directory_exists()
|
|
98
|
-
except Exception as exc: # pragma: no cover - defensive; textual path
|
|
99
|
-
self.notify(f"Failed to initialize directory: {exc}", severity="error")
|
|
100
|
-
return
|
|
101
|
-
|
|
102
|
-
# Double-check a directory now exists; guard against unexpected filesystem state.
|
|
103
|
-
if not path.is_dir():
|
|
104
|
-
self.notify(
|
|
105
|
-
"Unable to initialize .shotgun directory due to filesystem conflict.",
|
|
106
|
-
severity="error",
|
|
107
|
-
)
|
|
108
|
-
return
|
|
109
|
-
|
|
110
|
-
self.dismiss()
|
|
111
|
-
|
|
112
|
-
def _exit_application(self) -> None:
|
|
116
|
+
"""Exit the application."""
|
|
113
117
|
self.app.exit()
|
shotgun/tui/screens/feedback.py
CHANGED
|
@@ -76,6 +76,13 @@ class FeedbackScreen(Screen[Feedback | None]):
|
|
|
76
76
|
#feedback-type-list {
|
|
77
77
|
padding: 1;
|
|
78
78
|
}
|
|
79
|
+
|
|
80
|
+
#feedback-status {
|
|
81
|
+
height: auto;
|
|
82
|
+
padding: 0 1;
|
|
83
|
+
min-height: 1;
|
|
84
|
+
color: $error;
|
|
85
|
+
}
|
|
79
86
|
"""
|
|
80
87
|
|
|
81
88
|
BINDINGS = [
|
|
@@ -96,6 +103,7 @@ class FeedbackScreen(Screen[Feedback | None]):
|
|
|
96
103
|
"",
|
|
97
104
|
id="feedback-description",
|
|
98
105
|
)
|
|
106
|
+
yield Label("", id="feedback-status")
|
|
99
107
|
with Horizontal(id="feedback-actions"):
|
|
100
108
|
yield Button("Submit", variant="primary", id="submit")
|
|
101
109
|
yield Button("Cancel \\[ESC]", id="cancel")
|
|
@@ -176,9 +184,8 @@ class FeedbackScreen(Screen[Feedback | None]):
|
|
|
176
184
|
description = text_area.text.strip()
|
|
177
185
|
|
|
178
186
|
if not description:
|
|
179
|
-
self.
|
|
180
|
-
|
|
181
|
-
)
|
|
187
|
+
status_label = self.query_one("#feedback-status", Label)
|
|
188
|
+
status_label.update("❌ Please enter a description before submitting.")
|
|
182
189
|
return
|
|
183
190
|
|
|
184
191
|
app = cast("ShotgunApp", self.app)
|
|
@@ -6,7 +6,7 @@ from textual import on
|
|
|
6
6
|
from textual.app import ComposeResult
|
|
7
7
|
from textual.containers import Container, Vertical
|
|
8
8
|
from textual.screen import ModalScreen
|
|
9
|
-
from textual.widgets import Button, Markdown, Static
|
|
9
|
+
from textual.widgets import Button, Label, Markdown, Static
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class GitHubIssueScreen(ModalScreen[None]):
|
|
@@ -47,6 +47,13 @@ class GitHubIssueScreen(ModalScreen[None]):
|
|
|
47
47
|
margin: 1 1;
|
|
48
48
|
min-width: 20;
|
|
49
49
|
}
|
|
50
|
+
|
|
51
|
+
#issue-status {
|
|
52
|
+
height: auto;
|
|
53
|
+
padding: 1;
|
|
54
|
+
min-height: 1;
|
|
55
|
+
text-align: center;
|
|
56
|
+
}
|
|
50
57
|
"""
|
|
51
58
|
|
|
52
59
|
BINDINGS = [
|
|
@@ -85,6 +92,7 @@ We review all issues and will respond as soon as possible!
|
|
|
85
92
|
id="issue-markdown",
|
|
86
93
|
)
|
|
87
94
|
with Vertical(id="issue-buttons"):
|
|
95
|
+
yield Label("", id="issue-status")
|
|
88
96
|
yield Button(
|
|
89
97
|
"🐙 Open GitHub Issues", id="github-button", variant="primary"
|
|
90
98
|
)
|
|
@@ -94,7 +102,8 @@ We review all issues and will respond as soon as possible!
|
|
|
94
102
|
def handle_github(self) -> None:
|
|
95
103
|
"""Open GitHub issues page in browser."""
|
|
96
104
|
webbrowser.open("https://github.com/shotgun-sh/shotgun/issues")
|
|
97
|
-
self.
|
|
105
|
+
status_label = self.query_one("#issue-status", Label)
|
|
106
|
+
status_label.update("✓ Opening GitHub Issues in your browser...")
|
|
98
107
|
|
|
99
108
|
@on(Button.Pressed, "#close-button")
|
|
100
109
|
def handle_close(self) -> None:
|