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
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
"""Screen showing upload progress for sharing specs."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from textual import on, work
|
|
6
|
+
from textual.app import ComposeResult
|
|
7
|
+
from textual.containers import Container, Horizontal
|
|
8
|
+
from textual.events import Resize
|
|
9
|
+
from textual.screen import ModalScreen
|
|
10
|
+
from textual.widgets import Button, Label, ProgressBar, Static
|
|
11
|
+
from textual.worker import Worker, WorkerCancelled, get_current_worker
|
|
12
|
+
|
|
13
|
+
from shotgun.logging_config import get_logger
|
|
14
|
+
from shotgun.shotgun_web.shared_specs.models import UploadProgress, UploadResult
|
|
15
|
+
from shotgun.shotgun_web.shared_specs.upload_pipeline import run_upload_pipeline
|
|
16
|
+
from shotgun.shotgun_web.shared_specs.utils import UploadPhase, format_bytes
|
|
17
|
+
from shotgun.shotgun_web.specs_client import SpecsClient
|
|
18
|
+
from shotgun.tui.layout import COMPACT_HEIGHT_THRESHOLD
|
|
19
|
+
from shotgun.tui.screens.shared_specs.models import UploadScreenResult
|
|
20
|
+
|
|
21
|
+
logger = get_logger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class UploadProgressScreen(ModalScreen[UploadScreenResult]):
|
|
25
|
+
"""Screen showing upload progress for sharing specs.
|
|
26
|
+
|
|
27
|
+
Displays:
|
|
28
|
+
- Current phase (scanning, hashing, uploading, closing)
|
|
29
|
+
- Progress bar
|
|
30
|
+
- Current file being processed
|
|
31
|
+
- Bytes uploaded / total bytes
|
|
32
|
+
|
|
33
|
+
On success, shows URL with options to open in browser or copy.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
DEFAULT_CSS = """
|
|
37
|
+
UploadProgressScreen {
|
|
38
|
+
align: center middle;
|
|
39
|
+
background: rgba(0, 0, 0, 0.0);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
UploadProgressScreen > #dialog-container {
|
|
43
|
+
width: 80%;
|
|
44
|
+
max-width: 90;
|
|
45
|
+
height: auto;
|
|
46
|
+
border: wide $primary;
|
|
47
|
+
padding: 1 2;
|
|
48
|
+
layout: vertical;
|
|
49
|
+
background: $surface;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
#dialog-title {
|
|
53
|
+
text-style: bold;
|
|
54
|
+
color: $text-accent;
|
|
55
|
+
padding-bottom: 1;
|
|
56
|
+
text-align: center;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
#phase-label {
|
|
60
|
+
padding: 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
#progress-bar {
|
|
64
|
+
width: 100%;
|
|
65
|
+
padding: 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
#file-label {
|
|
69
|
+
color: $text-muted;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
#bytes-label {
|
|
73
|
+
color: $text-muted;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
#error-label {
|
|
77
|
+
color: $error;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
#success-label {
|
|
81
|
+
color: $success;
|
|
82
|
+
text-style: bold;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
#dialog-buttons {
|
|
86
|
+
layout: horizontal;
|
|
87
|
+
align-horizontal: center;
|
|
88
|
+
height: auto;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
#dialog-buttons Button {
|
|
92
|
+
margin: 0 1;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/* Hide elements initially */
|
|
96
|
+
#success-label {
|
|
97
|
+
display: none;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
#error-label {
|
|
101
|
+
display: none;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/* Compact styles for short terminals */
|
|
105
|
+
UploadProgressScreen.compact #dialog-container {
|
|
106
|
+
padding: 0 2;
|
|
107
|
+
max-height: 98%;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
UploadProgressScreen.compact #dialog-title {
|
|
111
|
+
padding-bottom: 0;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
UploadProgressScreen.compact #phase-label {
|
|
115
|
+
padding: 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
UploadProgressScreen.compact #progress-bar {
|
|
119
|
+
padding: 0;
|
|
120
|
+
}
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
BINDINGS = [
|
|
124
|
+
("escape", "cancel", "Cancel"),
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
def __init__(
|
|
128
|
+
self,
|
|
129
|
+
workspace_id: str,
|
|
130
|
+
# For existing spec - add version (spec_id required, version_id optional)
|
|
131
|
+
spec_id: str | None = None,
|
|
132
|
+
version_id: str | None = None,
|
|
133
|
+
# For new spec - create spec + version
|
|
134
|
+
spec_name: str | None = None,
|
|
135
|
+
spec_description: str | None = None,
|
|
136
|
+
spec_is_public: bool = False,
|
|
137
|
+
project_root: Path | None = None,
|
|
138
|
+
) -> None:
|
|
139
|
+
"""Initialize the screen.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
workspace_id: Workspace UUID
|
|
143
|
+
spec_id: Spec UUID (for existing spec) or None (for new spec)
|
|
144
|
+
version_id: Version UUID (if already created) or None
|
|
145
|
+
spec_name: Name for new spec (triggers spec creation)
|
|
146
|
+
spec_description: Description for new spec
|
|
147
|
+
spec_is_public: Whether new spec should be public
|
|
148
|
+
project_root: Project root containing .shotgun/ (defaults to cwd)
|
|
149
|
+
|
|
150
|
+
Usage:
|
|
151
|
+
# Add version to existing spec (creates version)
|
|
152
|
+
UploadProgressScreen(workspace_id="...", spec_id="...")
|
|
153
|
+
|
|
154
|
+
# Create new spec and version
|
|
155
|
+
UploadProgressScreen(workspace_id="...", spec_name="My Spec")
|
|
156
|
+
|
|
157
|
+
# Use pre-created version (legacy mode)
|
|
158
|
+
UploadProgressScreen(workspace_id="...", spec_id="...", version_id="...")
|
|
159
|
+
"""
|
|
160
|
+
super().__init__()
|
|
161
|
+
self.workspace_id = workspace_id
|
|
162
|
+
self.spec_id = spec_id
|
|
163
|
+
self.version_id = version_id
|
|
164
|
+
self.spec_name = spec_name
|
|
165
|
+
self.spec_description = spec_description
|
|
166
|
+
self.spec_is_public = spec_is_public
|
|
167
|
+
self.project_root = project_root
|
|
168
|
+
self._result: UploadResult | None = None
|
|
169
|
+
self._upload_worker: Worker[UploadResult] | None = None
|
|
170
|
+
self._cancelled = False
|
|
171
|
+
|
|
172
|
+
def compose(self) -> ComposeResult:
|
|
173
|
+
"""Compose the screen widgets."""
|
|
174
|
+
with Container(id="dialog-container"):
|
|
175
|
+
yield Label("Sharing specs to workspace", id="dialog-title")
|
|
176
|
+
|
|
177
|
+
# Progress section
|
|
178
|
+
yield Static("Phase 1/4: Scanning files...", id="phase-label")
|
|
179
|
+
yield ProgressBar(total=100, id="progress-bar")
|
|
180
|
+
yield Static("", id="file-label")
|
|
181
|
+
yield Static("", id="bytes-label")
|
|
182
|
+
|
|
183
|
+
# Error section (hidden by default)
|
|
184
|
+
yield Static("", id="error-label")
|
|
185
|
+
|
|
186
|
+
# Success section (hidden by default)
|
|
187
|
+
yield Static("Specs shared successfully!", id="success-label")
|
|
188
|
+
|
|
189
|
+
# Buttons
|
|
190
|
+
with Horizontal(id="dialog-buttons"):
|
|
191
|
+
yield Button("Cancel", id="cancel-btn")
|
|
192
|
+
yield Button("Open in Browser", variant="primary", id="open-btn")
|
|
193
|
+
yield Button("Copy URL", id="copy-btn")
|
|
194
|
+
yield Button("Done", id="done-btn")
|
|
195
|
+
|
|
196
|
+
def on_mount(self) -> None:
|
|
197
|
+
"""Start the upload when screen is mounted."""
|
|
198
|
+
# Hide success buttons initially
|
|
199
|
+
self.query_one("#open-btn", Button).display = False
|
|
200
|
+
self.query_one("#copy-btn", Button).display = False
|
|
201
|
+
self.query_one("#done-btn", Button).display = False
|
|
202
|
+
|
|
203
|
+
# Apply compact layout if starting in a short terminal
|
|
204
|
+
self._apply_compact_layout(self.app.size.height < COMPACT_HEIGHT_THRESHOLD)
|
|
205
|
+
|
|
206
|
+
# Start the upload
|
|
207
|
+
self._start_upload()
|
|
208
|
+
|
|
209
|
+
@on(Resize)
|
|
210
|
+
def handle_resize(self, event: Resize) -> None:
|
|
211
|
+
"""Adjust layout based on terminal height."""
|
|
212
|
+
self._apply_compact_layout(event.size.height < COMPACT_HEIGHT_THRESHOLD)
|
|
213
|
+
|
|
214
|
+
def _apply_compact_layout(self, compact: bool) -> None:
|
|
215
|
+
"""Apply or remove compact layout class for short terminals."""
|
|
216
|
+
if compact:
|
|
217
|
+
self.add_class("compact")
|
|
218
|
+
else:
|
|
219
|
+
self.remove_class("compact")
|
|
220
|
+
|
|
221
|
+
@work(exclusive=True)
|
|
222
|
+
async def _start_upload(self) -> None:
|
|
223
|
+
"""Run the upload pipeline."""
|
|
224
|
+
worker = get_current_worker()
|
|
225
|
+
self._upload_worker = worker
|
|
226
|
+
|
|
227
|
+
def on_progress(progress: UploadProgress) -> None:
|
|
228
|
+
"""Handle progress updates from the pipeline."""
|
|
229
|
+
# Check if we should cancel
|
|
230
|
+
if worker.is_cancelled:
|
|
231
|
+
raise WorkerCancelled()
|
|
232
|
+
|
|
233
|
+
# Update UI directly (we're in an async context via @work)
|
|
234
|
+
self._update_progress(progress)
|
|
235
|
+
|
|
236
|
+
try:
|
|
237
|
+
# Phase 0: Create spec/version if needed
|
|
238
|
+
client = SpecsClient()
|
|
239
|
+
|
|
240
|
+
if self.spec_name:
|
|
241
|
+
# Creating a new spec
|
|
242
|
+
self._show_creating_phase("Creating spec...")
|
|
243
|
+
if worker.is_cancelled:
|
|
244
|
+
raise WorkerCancelled()
|
|
245
|
+
|
|
246
|
+
create_response = await client.create_spec(
|
|
247
|
+
self.workspace_id,
|
|
248
|
+
name=self.spec_name,
|
|
249
|
+
description=self.spec_description,
|
|
250
|
+
)
|
|
251
|
+
self.spec_id = create_response.spec.id
|
|
252
|
+
self.version_id = create_response.version.id
|
|
253
|
+
|
|
254
|
+
# Set public if requested
|
|
255
|
+
if self.spec_is_public:
|
|
256
|
+
self._show_creating_phase("Setting visibility...")
|
|
257
|
+
if worker.is_cancelled:
|
|
258
|
+
raise WorkerCancelled()
|
|
259
|
+
await client.update_spec(
|
|
260
|
+
self.workspace_id, self.spec_id, is_public=True
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
elif self.spec_id and not self.version_id:
|
|
264
|
+
# Adding version to existing spec
|
|
265
|
+
self._show_creating_phase("Creating version...")
|
|
266
|
+
if worker.is_cancelled:
|
|
267
|
+
raise WorkerCancelled()
|
|
268
|
+
|
|
269
|
+
version_response = await client.create_version(
|
|
270
|
+
self.workspace_id, self.spec_id
|
|
271
|
+
)
|
|
272
|
+
self.version_id = version_response.version.id
|
|
273
|
+
|
|
274
|
+
# Validate we have spec_id and version_id
|
|
275
|
+
if not self.spec_id or not self.version_id:
|
|
276
|
+
self._show_error("Missing spec or version ID")
|
|
277
|
+
return
|
|
278
|
+
|
|
279
|
+
# Run upload pipeline
|
|
280
|
+
result = await run_upload_pipeline(
|
|
281
|
+
self.workspace_id,
|
|
282
|
+
self.spec_id,
|
|
283
|
+
self.version_id,
|
|
284
|
+
self.project_root,
|
|
285
|
+
on_progress=on_progress,
|
|
286
|
+
)
|
|
287
|
+
self._result = result
|
|
288
|
+
self._show_result(result)
|
|
289
|
+
except WorkerCancelled:
|
|
290
|
+
self._cancelled = True
|
|
291
|
+
self._show_cancelled()
|
|
292
|
+
except Exception as e:
|
|
293
|
+
logger.exception(f"Upload failed: {type(e).__name__}: {e}")
|
|
294
|
+
error_msg = str(e) if str(e) else type(e).__name__
|
|
295
|
+
self._show_error(error_msg)
|
|
296
|
+
|
|
297
|
+
def _show_creating_phase(self, message: str) -> None:
|
|
298
|
+
"""Update UI to show creating phase."""
|
|
299
|
+
phase_label = self.query_one("#phase-label", Static)
|
|
300
|
+
phase_label.update(message)
|
|
301
|
+
# Keep progress bar at 0 during creation
|
|
302
|
+
progress_bar = self.query_one("#progress-bar", ProgressBar)
|
|
303
|
+
progress_bar.update(progress=0)
|
|
304
|
+
# Clear file/bytes labels
|
|
305
|
+
self.query_one("#file-label", Static).update("")
|
|
306
|
+
self.query_one("#bytes-label", Static).update("")
|
|
307
|
+
|
|
308
|
+
def _update_progress(self, progress: UploadProgress) -> None:
|
|
309
|
+
"""Update the UI with progress information."""
|
|
310
|
+
phase_names = {
|
|
311
|
+
UploadPhase.CREATING: "Creating spec...",
|
|
312
|
+
UploadPhase.SCANNING: "Phase 1/4: Scanning files...",
|
|
313
|
+
UploadPhase.HASHING: "Phase 2/4: Calculating hashes...",
|
|
314
|
+
UploadPhase.UPLOADING: "Phase 3/4: Uploading files...",
|
|
315
|
+
UploadPhase.CLOSING: "Phase 4/4: Finalizing version...",
|
|
316
|
+
UploadPhase.COMPLETE: "Complete!",
|
|
317
|
+
UploadPhase.ERROR: "Error",
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
phase_label = self.query_one("#phase-label", Static)
|
|
321
|
+
progress_bar = self.query_one("#progress-bar", ProgressBar)
|
|
322
|
+
file_label = self.query_one("#file-label", Static)
|
|
323
|
+
bytes_label = self.query_one("#bytes-label", Static)
|
|
324
|
+
|
|
325
|
+
# Update phase label
|
|
326
|
+
phase_text = phase_names.get(progress.phase, progress.phase)
|
|
327
|
+
if progress.total > 0:
|
|
328
|
+
phase_text = f"{phase_text} ({progress.current}/{progress.total})"
|
|
329
|
+
phase_label.update(phase_text)
|
|
330
|
+
|
|
331
|
+
# Update progress bar
|
|
332
|
+
if progress.total > 0:
|
|
333
|
+
percentage = (progress.current / progress.total) * 100
|
|
334
|
+
progress_bar.update(progress=percentage)
|
|
335
|
+
else:
|
|
336
|
+
progress_bar.update(progress=0)
|
|
337
|
+
|
|
338
|
+
# Update file label
|
|
339
|
+
if progress.current_file:
|
|
340
|
+
file_label.update(f"Current: {progress.current_file}")
|
|
341
|
+
else:
|
|
342
|
+
file_label.update("")
|
|
343
|
+
|
|
344
|
+
# Update bytes label
|
|
345
|
+
if progress.total_bytes > 0:
|
|
346
|
+
bytes_label.update(
|
|
347
|
+
f"Uploaded: {format_bytes(progress.bytes_uploaded)} / {format_bytes(progress.total_bytes)}"
|
|
348
|
+
)
|
|
349
|
+
else:
|
|
350
|
+
bytes_label.update("")
|
|
351
|
+
|
|
352
|
+
def _show_result(self, result: UploadResult) -> None:
|
|
353
|
+
"""Show the upload result."""
|
|
354
|
+
if result.success:
|
|
355
|
+
self._show_success(result.web_url)
|
|
356
|
+
else:
|
|
357
|
+
self._show_error(result.error or "Unknown error")
|
|
358
|
+
|
|
359
|
+
def _show_success(self, web_url: str | None) -> None:
|
|
360
|
+
"""Show success state."""
|
|
361
|
+
# Update phase label
|
|
362
|
+
self.query_one("#phase-label", Static).update("Upload complete!")
|
|
363
|
+
self.query_one("#progress-bar", ProgressBar).update(progress=100)
|
|
364
|
+
|
|
365
|
+
# Show success label
|
|
366
|
+
self.query_one("#success-label", Static).display = True
|
|
367
|
+
|
|
368
|
+
# Hide cancel, show success buttons
|
|
369
|
+
self.query_one("#cancel-btn", Button).display = False
|
|
370
|
+
self.query_one("#open-btn", Button).display = bool(web_url)
|
|
371
|
+
self.query_one("#copy-btn", Button).display = bool(web_url)
|
|
372
|
+
self.query_one("#done-btn", Button).display = True
|
|
373
|
+
|
|
374
|
+
def _show_error(self, error: str) -> None:
|
|
375
|
+
"""Show error state."""
|
|
376
|
+
error_label = self.query_one("#error-label", Static)
|
|
377
|
+
error_label.update(f"Error: {error}")
|
|
378
|
+
error_label.display = True
|
|
379
|
+
|
|
380
|
+
# Hide cancel, show done
|
|
381
|
+
self.query_one("#cancel-btn", Button).display = False
|
|
382
|
+
self.query_one("#done-btn", Button).display = True
|
|
383
|
+
|
|
384
|
+
def _show_cancelled(self) -> None:
|
|
385
|
+
"""Show cancelled state."""
|
|
386
|
+
self.query_one("#phase-label", Static).update("Upload cancelled")
|
|
387
|
+
self.query_one("#cancel-btn", Button).display = False
|
|
388
|
+
self.query_one("#done-btn", Button).display = True
|
|
389
|
+
|
|
390
|
+
@on(Button.Pressed, "#cancel-btn")
|
|
391
|
+
def _on_cancel(self, event: Button.Pressed) -> None:
|
|
392
|
+
"""Handle cancel button."""
|
|
393
|
+
event.stop()
|
|
394
|
+
self._cancel_upload()
|
|
395
|
+
|
|
396
|
+
@on(Button.Pressed, "#open-btn")
|
|
397
|
+
def _on_open(self, event: Button.Pressed) -> None:
|
|
398
|
+
"""Handle open in browser button."""
|
|
399
|
+
event.stop()
|
|
400
|
+
if self._result and self._result.web_url:
|
|
401
|
+
import webbrowser
|
|
402
|
+
|
|
403
|
+
webbrowser.open(self._result.web_url)
|
|
404
|
+
|
|
405
|
+
@on(Button.Pressed, "#copy-btn")
|
|
406
|
+
def _on_copy(self, event: Button.Pressed) -> None:
|
|
407
|
+
"""Handle copy URL button."""
|
|
408
|
+
event.stop()
|
|
409
|
+
if self._result and self._result.web_url:
|
|
410
|
+
try:
|
|
411
|
+
import pyperclip # type: ignore[import-untyped]
|
|
412
|
+
|
|
413
|
+
pyperclip.copy(self._result.web_url)
|
|
414
|
+
self.query_one("#copy-btn", Button).label = "Copied!"
|
|
415
|
+
except Exception:
|
|
416
|
+
# pyperclip may not be available on all systems
|
|
417
|
+
logger.debug("pyperclip not available for URL copy")
|
|
418
|
+
|
|
419
|
+
@on(Button.Pressed, "#done-btn")
|
|
420
|
+
def _on_done(self, event: Button.Pressed) -> None:
|
|
421
|
+
"""Handle done button."""
|
|
422
|
+
event.stop()
|
|
423
|
+
self._dismiss_with_result()
|
|
424
|
+
|
|
425
|
+
def action_cancel(self) -> None:
|
|
426
|
+
"""Handle escape key."""
|
|
427
|
+
if self._result:
|
|
428
|
+
# Upload complete, just dismiss
|
|
429
|
+
self._dismiss_with_result()
|
|
430
|
+
else:
|
|
431
|
+
# Upload in progress, cancel it
|
|
432
|
+
self._cancel_upload()
|
|
433
|
+
|
|
434
|
+
def _cancel_upload(self) -> None:
|
|
435
|
+
"""Cancel the upload."""
|
|
436
|
+
if self._upload_worker and not self._upload_worker.is_cancelled:
|
|
437
|
+
self._cancelled = True
|
|
438
|
+
self._upload_worker.cancel()
|
|
439
|
+
|
|
440
|
+
def _dismiss_with_result(self) -> None:
|
|
441
|
+
"""Dismiss the screen with the appropriate result."""
|
|
442
|
+
if self._cancelled:
|
|
443
|
+
self.dismiss(UploadScreenResult(success=False, cancelled=True))
|
|
444
|
+
elif self._result:
|
|
445
|
+
self.dismiss(
|
|
446
|
+
UploadScreenResult(
|
|
447
|
+
success=self._result.success,
|
|
448
|
+
web_url=self._result.web_url,
|
|
449
|
+
)
|
|
450
|
+
)
|
|
451
|
+
else:
|
|
452
|
+
self.dismiss(UploadScreenResult(success=False))
|
|
@@ -8,6 +8,7 @@ import httpx
|
|
|
8
8
|
from textual import on
|
|
9
9
|
from textual.app import ComposeResult
|
|
10
10
|
from textual.containers import Vertical
|
|
11
|
+
from textual.events import Resize
|
|
11
12
|
from textual.screen import Screen
|
|
12
13
|
from textual.widgets import Button, Label, Markdown, Static
|
|
13
14
|
from textual.worker import Worker, WorkerState
|
|
@@ -19,6 +20,7 @@ from shotgun.shotgun_web import (
|
|
|
19
20
|
TokenStatus,
|
|
20
21
|
)
|
|
21
22
|
from shotgun.shotgun_web.constants import DEFAULT_POLL_INTERVAL_SECONDS
|
|
23
|
+
from shotgun.tui.layout import COMPACT_HEIGHT_THRESHOLD
|
|
22
24
|
|
|
23
25
|
if TYPE_CHECKING:
|
|
24
26
|
from ..app import ShotgunApp
|
|
@@ -76,25 +78,51 @@ class ShotgunAuthScreen(Screen[bool]):
|
|
|
76
78
|
padding: 1;
|
|
77
79
|
align: center middle;
|
|
78
80
|
}
|
|
81
|
+
|
|
82
|
+
/* Compact styles for short terminals */
|
|
83
|
+
ShotgunAuthScreen.compact #titlebox {
|
|
84
|
+
margin: 0;
|
|
85
|
+
padding: 0;
|
|
86
|
+
border: none;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
ShotgunAuthScreen.compact #auth-subtitle {
|
|
90
|
+
display: none;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
ShotgunAuthScreen.compact #content {
|
|
94
|
+
padding: 0;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
ShotgunAuthScreen.compact #instructions {
|
|
98
|
+
display: none;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
ShotgunAuthScreen.compact #actions {
|
|
102
|
+
padding: 0;
|
|
103
|
+
}
|
|
79
104
|
"""
|
|
80
105
|
|
|
81
106
|
BINDINGS = [
|
|
82
107
|
("escape", "cancel", "Cancel"),
|
|
83
108
|
]
|
|
84
109
|
|
|
85
|
-
def __init__(
|
|
110
|
+
def __init__(
|
|
111
|
+
self,
|
|
112
|
+
title: str = "Shotgun Account Setup",
|
|
113
|
+
subtitle: str = "Authenticate with your Shotgun Account to get started",
|
|
114
|
+
) -> None:
|
|
86
115
|
super().__init__()
|
|
116
|
+
self._title = title
|
|
117
|
+
self._subtitle = subtitle
|
|
87
118
|
self.token: str | None = None
|
|
88
119
|
self.auth_url: str | None = None
|
|
89
120
|
self.poll_worker: Worker[None] | None = None
|
|
90
121
|
|
|
91
122
|
def compose(self) -> ComposeResult:
|
|
92
123
|
with Vertical(id="titlebox"):
|
|
93
|
-
yield Static(
|
|
94
|
-
yield Static(
|
|
95
|
-
"Authenticate with your Shotgun Account to get started",
|
|
96
|
-
id="auth-subtitle",
|
|
97
|
-
)
|
|
124
|
+
yield Static(self._title, id="auth-title")
|
|
125
|
+
yield Static(self._subtitle, id="auth-subtitle")
|
|
98
126
|
|
|
99
127
|
with Vertical(id="content"):
|
|
100
128
|
yield Label("Initializing...", id="status")
|
|
@@ -113,8 +141,21 @@ class ShotgunAuthScreen(Screen[bool]):
|
|
|
113
141
|
|
|
114
142
|
def on_mount(self) -> None:
|
|
115
143
|
"""Start authentication flow when screen is mounted."""
|
|
144
|
+
self._apply_layout_for_height(self.app.size.height)
|
|
116
145
|
self.run_worker(self._start_auth_flow(), exclusive=True)
|
|
117
146
|
|
|
147
|
+
@on(Resize)
|
|
148
|
+
def handle_resize(self, event: Resize) -> None:
|
|
149
|
+
"""Adjust layout based on terminal height."""
|
|
150
|
+
self._apply_layout_for_height(event.size.height)
|
|
151
|
+
|
|
152
|
+
def _apply_layout_for_height(self, height: int) -> None:
|
|
153
|
+
"""Apply appropriate layout based on terminal height."""
|
|
154
|
+
if height < COMPACT_HEIGHT_THRESHOLD:
|
|
155
|
+
self.add_class("compact")
|
|
156
|
+
else:
|
|
157
|
+
self.remove_class("compact")
|
|
158
|
+
|
|
118
159
|
def action_cancel(self) -> None:
|
|
119
160
|
"""Cancel authentication and close screen."""
|
|
120
161
|
if self.poll_worker and self.poll_worker.state == WorkerState.RUNNING:
|
|
@@ -213,9 +254,22 @@ class ShotgunAuthScreen(Screen[bool]):
|
|
|
213
254
|
logger.info("Authentication completed successfully")
|
|
214
255
|
|
|
215
256
|
if status_response.litellm_key and status_response.supabase_key:
|
|
257
|
+
# Fetch user info to get workspace_id
|
|
258
|
+
workspace_id: str | None = None
|
|
259
|
+
try:
|
|
260
|
+
me_response = client.get_me(status_response.supabase_key)
|
|
261
|
+
workspace_id = me_response.workspace.id
|
|
262
|
+
logger.info("Fetched workspace_id: %s", workspace_id)
|
|
263
|
+
except Exception as e:
|
|
264
|
+
# Log warning but continue - workspace_id can be fetched later
|
|
265
|
+
logger.warning(
|
|
266
|
+
"Failed to fetch workspace_id from /api/me: %s", e
|
|
267
|
+
)
|
|
268
|
+
|
|
216
269
|
await self.config_manager.update_shotgun_account(
|
|
217
270
|
api_key=status_response.litellm_key,
|
|
218
271
|
supabase_jwt=status_response.supabase_key,
|
|
272
|
+
workspace_id=workspace_id,
|
|
219
273
|
)
|
|
220
274
|
|
|
221
275
|
self.query_one("#status", Label).update(
|