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.
Files changed (112) hide show
  1. shotgun/agents/agent_manager.py +28 -14
  2. shotgun/agents/common.py +1 -1
  3. shotgun/agents/config/README.md +89 -0
  4. shotgun/agents/config/__init__.py +10 -1
  5. shotgun/agents/config/manager.py +323 -53
  6. shotgun/agents/config/models.py +85 -21
  7. shotgun/agents/config/provider.py +51 -13
  8. shotgun/agents/config/streaming_test.py +119 -0
  9. shotgun/agents/context_analyzer/analyzer.py +6 -2
  10. shotgun/agents/conversation/__init__.py +18 -0
  11. shotgun/agents/conversation/filters.py +164 -0
  12. shotgun/agents/conversation/history/chunking.py +278 -0
  13. shotgun/agents/{history → conversation/history}/compaction.py +27 -1
  14. shotgun/agents/{history → conversation/history}/constants.py +5 -0
  15. shotgun/agents/conversation/history/file_content_deduplication.py +216 -0
  16. shotgun/agents/{history → conversation/history}/history_processors.py +267 -3
  17. shotgun/agents/{history → conversation/history}/token_counting/anthropic.py +8 -0
  18. shotgun/agents/{conversation_manager.py → conversation/manager.py} +1 -1
  19. shotgun/agents/{conversation_history.py → conversation/models.py} +8 -94
  20. shotgun/agents/error/__init__.py +11 -0
  21. shotgun/agents/error/models.py +19 -0
  22. shotgun/agents/runner.py +230 -0
  23. shotgun/agents/tools/web_search/openai.py +1 -1
  24. shotgun/build_constants.py +2 -2
  25. shotgun/cli/clear.py +1 -1
  26. shotgun/cli/compact.py +5 -3
  27. shotgun/cli/context.py +44 -1
  28. shotgun/cli/error_handler.py +24 -0
  29. shotgun/cli/export.py +34 -34
  30. shotgun/cli/plan.py +34 -34
  31. shotgun/cli/research.py +17 -9
  32. shotgun/cli/spec/__init__.py +5 -0
  33. shotgun/cli/spec/backup.py +81 -0
  34. shotgun/cli/spec/commands.py +132 -0
  35. shotgun/cli/spec/models.py +48 -0
  36. shotgun/cli/spec/pull_service.py +219 -0
  37. shotgun/cli/specify.py +20 -19
  38. shotgun/cli/tasks.py +34 -34
  39. shotgun/codebase/core/ingestor.py +153 -7
  40. shotgun/codebase/models.py +2 -0
  41. shotgun/exceptions.py +325 -0
  42. shotgun/llm_proxy/__init__.py +17 -0
  43. shotgun/llm_proxy/client.py +215 -0
  44. shotgun/llm_proxy/models.py +137 -0
  45. shotgun/logging_config.py +42 -0
  46. shotgun/main.py +4 -0
  47. shotgun/posthog_telemetry.py +1 -1
  48. shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +28 -3
  49. shotgun/prompts/agents/partials/interactive_mode.j2 +3 -3
  50. shotgun/prompts/agents/plan.j2 +16 -0
  51. shotgun/prompts/agents/research.j2 +16 -3
  52. shotgun/prompts/agents/specify.j2 +54 -1
  53. shotgun/prompts/agents/state/system_state.j2 +0 -2
  54. shotgun/prompts/agents/tasks.j2 +16 -0
  55. shotgun/prompts/history/chunk_summarization.j2 +34 -0
  56. shotgun/prompts/history/combine_summaries.j2 +53 -0
  57. shotgun/sdk/codebase.py +14 -3
  58. shotgun/settings.py +5 -0
  59. shotgun/shotgun_web/__init__.py +67 -1
  60. shotgun/shotgun_web/client.py +42 -1
  61. shotgun/shotgun_web/constants.py +46 -0
  62. shotgun/shotgun_web/exceptions.py +29 -0
  63. shotgun/shotgun_web/models.py +390 -0
  64. shotgun/shotgun_web/shared_specs/__init__.py +32 -0
  65. shotgun/shotgun_web/shared_specs/file_scanner.py +175 -0
  66. shotgun/shotgun_web/shared_specs/hasher.py +83 -0
  67. shotgun/shotgun_web/shared_specs/models.py +71 -0
  68. shotgun/shotgun_web/shared_specs/upload_pipeline.py +329 -0
  69. shotgun/shotgun_web/shared_specs/utils.py +34 -0
  70. shotgun/shotgun_web/specs_client.py +703 -0
  71. shotgun/shotgun_web/supabase_client.py +31 -0
  72. shotgun/tui/app.py +73 -9
  73. shotgun/tui/containers.py +1 -1
  74. shotgun/tui/layout.py +5 -0
  75. shotgun/tui/screens/chat/chat_screen.py +372 -95
  76. shotgun/tui/screens/chat/codebase_index_prompt_screen.py +196 -17
  77. shotgun/tui/screens/chat_screen/command_providers.py +13 -2
  78. shotgun/tui/screens/chat_screen/hint_message.py +76 -1
  79. shotgun/tui/screens/confirmation_dialog.py +40 -0
  80. shotgun/tui/screens/directory_setup.py +45 -41
  81. shotgun/tui/screens/feedback.py +10 -3
  82. shotgun/tui/screens/github_issue.py +11 -2
  83. shotgun/tui/screens/model_picker.py +28 -8
  84. shotgun/tui/screens/onboarding.py +149 -0
  85. shotgun/tui/screens/pipx_migration.py +58 -6
  86. shotgun/tui/screens/provider_config.py +66 -8
  87. shotgun/tui/screens/shared_specs/__init__.py +21 -0
  88. shotgun/tui/screens/shared_specs/create_spec_dialog.py +273 -0
  89. shotgun/tui/screens/shared_specs/models.py +56 -0
  90. shotgun/tui/screens/shared_specs/share_specs_dialog.py +390 -0
  91. shotgun/tui/screens/shared_specs/upload_progress_screen.py +452 -0
  92. shotgun/tui/screens/shotgun_auth.py +110 -16
  93. shotgun/tui/screens/spec_pull.py +288 -0
  94. shotgun/tui/screens/welcome.py +123 -0
  95. shotgun/tui/services/conversation_service.py +5 -2
  96. shotgun/tui/widgets/widget_coordinator.py +1 -1
  97. {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/METADATA +9 -2
  98. {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/RECORD +112 -77
  99. {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/WHEEL +1 -1
  100. /shotgun/agents/{history → conversation/history}/__init__.py +0 -0
  101. /shotgun/agents/{history → conversation/history}/context_extraction.py +0 -0
  102. /shotgun/agents/{history → conversation/history}/history_building.py +0 -0
  103. /shotgun/agents/{history → conversation/history}/message_utils.py +0 -0
  104. /shotgun/agents/{history → conversation/history}/token_counting/__init__.py +0 -0
  105. /shotgun/agents/{history → conversation/history}/token_counting/base.py +0 -0
  106. /shotgun/agents/{history → conversation/history}/token_counting/openai.py +0 -0
  107. /shotgun/agents/{history → conversation/history}/token_counting/sentencepiece_counter.py +0 -0
  108. /shotgun/agents/{history → conversation/history}/token_counting/tokenizer_cache.py +0 -0
  109. /shotgun/agents/{history → conversation/history}/token_counting/utils.py +0 -0
  110. /shotgun/agents/{history → conversation/history}/token_estimation.py +0 -0
  111. {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/entry_points.txt +0 -0
  112. {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.3.3.dev1.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))