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.

Files changed (86) hide show
  1. shotgun/agents/agent_manager.py +3 -3
  2. shotgun/agents/common.py +1 -1
  3. shotgun/agents/config/manager.py +36 -21
  4. shotgun/agents/config/models.py +30 -0
  5. shotgun/agents/config/provider.py +27 -14
  6. shotgun/agents/context_analyzer/analyzer.py +6 -2
  7. shotgun/agents/conversation/__init__.py +18 -0
  8. shotgun/agents/conversation/filters.py +164 -0
  9. shotgun/agents/conversation/history/chunking.py +278 -0
  10. shotgun/agents/{history → conversation/history}/compaction.py +27 -1
  11. shotgun/agents/{history → conversation/history}/constants.py +5 -0
  12. shotgun/agents/conversation/history/file_content_deduplication.py +216 -0
  13. shotgun/agents/{history → conversation/history}/history_processors.py +267 -3
  14. shotgun/agents/{conversation_manager.py → conversation/manager.py} +1 -1
  15. shotgun/agents/{conversation_history.py → conversation/models.py} +8 -94
  16. shotgun/agents/tools/web_search/openai.py +1 -1
  17. shotgun/cli/clear.py +1 -1
  18. shotgun/cli/compact.py +5 -3
  19. shotgun/cli/context.py +1 -1
  20. shotgun/cli/spec/__init__.py +5 -0
  21. shotgun/cli/spec/backup.py +81 -0
  22. shotgun/cli/spec/commands.py +130 -0
  23. shotgun/cli/spec/models.py +30 -0
  24. shotgun/cli/spec/pull_service.py +165 -0
  25. shotgun/codebase/core/ingestor.py +153 -7
  26. shotgun/codebase/models.py +2 -0
  27. shotgun/exceptions.py +5 -3
  28. shotgun/main.py +2 -0
  29. shotgun/posthog_telemetry.py +1 -1
  30. shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +3 -3
  31. shotgun/prompts/agents/partials/interactive_mode.j2 +3 -3
  32. shotgun/prompts/agents/research.j2 +0 -3
  33. shotgun/prompts/history/chunk_summarization.j2 +34 -0
  34. shotgun/prompts/history/combine_summaries.j2 +53 -0
  35. shotgun/shotgun_web/__init__.py +67 -1
  36. shotgun/shotgun_web/client.py +42 -1
  37. shotgun/shotgun_web/constants.py +46 -0
  38. shotgun/shotgun_web/exceptions.py +29 -0
  39. shotgun/shotgun_web/models.py +390 -0
  40. shotgun/shotgun_web/shared_specs/__init__.py +32 -0
  41. shotgun/shotgun_web/shared_specs/file_scanner.py +175 -0
  42. shotgun/shotgun_web/shared_specs/hasher.py +83 -0
  43. shotgun/shotgun_web/shared_specs/models.py +71 -0
  44. shotgun/shotgun_web/shared_specs/upload_pipeline.py +291 -0
  45. shotgun/shotgun_web/shared_specs/utils.py +34 -0
  46. shotgun/shotgun_web/specs_client.py +703 -0
  47. shotgun/shotgun_web/supabase_client.py +31 -0
  48. shotgun/tui/app.py +39 -0
  49. shotgun/tui/containers.py +1 -1
  50. shotgun/tui/layout.py +5 -0
  51. shotgun/tui/screens/chat/chat_screen.py +212 -16
  52. shotgun/tui/screens/chat/codebase_index_prompt_screen.py +147 -19
  53. shotgun/tui/screens/chat_screen/command_providers.py +10 -0
  54. shotgun/tui/screens/chat_screen/history/chat_history.py +0 -36
  55. shotgun/tui/screens/confirmation_dialog.py +40 -0
  56. shotgun/tui/screens/model_picker.py +7 -1
  57. shotgun/tui/screens/onboarding.py +149 -0
  58. shotgun/tui/screens/pipx_migration.py +46 -0
  59. shotgun/tui/screens/provider_config.py +41 -0
  60. shotgun/tui/screens/shared_specs/__init__.py +21 -0
  61. shotgun/tui/screens/shared_specs/create_spec_dialog.py +273 -0
  62. shotgun/tui/screens/shared_specs/models.py +56 -0
  63. shotgun/tui/screens/shared_specs/share_specs_dialog.py +390 -0
  64. shotgun/tui/screens/shared_specs/upload_progress_screen.py +452 -0
  65. shotgun/tui/screens/shotgun_auth.py +60 -6
  66. shotgun/tui/screens/spec_pull.py +286 -0
  67. shotgun/tui/screens/welcome.py +91 -0
  68. shotgun/tui/services/conversation_service.py +5 -2
  69. shotgun/tui/widgets/widget_coordinator.py +1 -1
  70. {shotgun_sh-0.2.23.dev1.dist-info → shotgun_sh-0.2.29.dev2.dist-info}/METADATA +1 -1
  71. {shotgun_sh-0.2.23.dev1.dist-info → shotgun_sh-0.2.29.dev2.dist-info}/RECORD +86 -59
  72. {shotgun_sh-0.2.23.dev1.dist-info → shotgun_sh-0.2.29.dev2.dist-info}/WHEEL +1 -1
  73. /shotgun/agents/{history → conversation/history}/__init__.py +0 -0
  74. /shotgun/agents/{history → conversation/history}/context_extraction.py +0 -0
  75. /shotgun/agents/{history → conversation/history}/history_building.py +0 -0
  76. /shotgun/agents/{history → conversation/history}/message_utils.py +0 -0
  77. /shotgun/agents/{history → conversation/history}/token_counting/__init__.py +0 -0
  78. /shotgun/agents/{history → conversation/history}/token_counting/anthropic.py +0 -0
  79. /shotgun/agents/{history → conversation/history}/token_counting/base.py +0 -0
  80. /shotgun/agents/{history → conversation/history}/token_counting/openai.py +0 -0
  81. /shotgun/agents/{history → conversation/history}/token_counting/sentencepiece_counter.py +0 -0
  82. /shotgun/agents/{history → conversation/history}/token_counting/tokenizer_cache.py +0 -0
  83. /shotgun/agents/{history → conversation/history}/token_counting/utils.py +0 -0
  84. /shotgun/agents/{history → conversation/history}/token_estimation.py +0 -0
  85. {shotgun_sh-0.2.23.dev1.dist-info → shotgun_sh-0.2.29.dev2.dist-info}/entry_points.txt +0 -0
  86. {shotgun_sh-0.2.23.dev1.dist-info → shotgun_sh-0.2.29.dev2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,286 @@
1
+ """Screen showing download progress for pulling specs."""
2
+
3
+ from textual import on, work
4
+ from textual.app import ComposeResult
5
+ from textual.containers import Container, Horizontal
6
+ from textual.events import Resize
7
+ from textual.screen import ModalScreen
8
+ from textual.widgets import Button, Label, ProgressBar, Static
9
+ from textual.worker import Worker, get_current_worker
10
+
11
+ from shotgun.cli.spec.pull_service import (
12
+ CancelledError,
13
+ PullProgress,
14
+ SpecPullService,
15
+ )
16
+ from shotgun.logging_config import get_logger
17
+ from shotgun.shotgun_web.exceptions import (
18
+ ForbiddenError,
19
+ NotFoundError,
20
+ UnauthorizedError,
21
+ )
22
+ from shotgun.tui.layout import COMPACT_HEIGHT_THRESHOLD
23
+ from shotgun.utils.file_system_utils import get_shotgun_base_path
24
+
25
+ logger = get_logger(__name__)
26
+
27
+
28
+ class SpecPullScreen(ModalScreen[bool]):
29
+ """Screen to pull a spec version with progress display.
30
+
31
+ Returns True if pull was successful, False otherwise.
32
+ """
33
+
34
+ DEFAULT_CSS = """
35
+ SpecPullScreen {
36
+ align: center middle;
37
+ background: rgba(0, 0, 0, 0.0);
38
+ }
39
+
40
+ SpecPullScreen > #dialog-container {
41
+ width: 80%;
42
+ max-width: 90;
43
+ height: auto;
44
+ border: wide $primary;
45
+ padding: 1 2;
46
+ layout: vertical;
47
+ background: $surface;
48
+ }
49
+
50
+ #dialog-title {
51
+ text-style: bold;
52
+ color: $text-accent;
53
+ padding-bottom: 1;
54
+ text-align: center;
55
+ }
56
+
57
+ #phase-label {
58
+ padding: 0;
59
+ }
60
+
61
+ #progress-bar {
62
+ width: 100%;
63
+ padding: 0;
64
+ }
65
+
66
+ #file-label {
67
+ color: $text-muted;
68
+ }
69
+
70
+ #error-label {
71
+ color: $error;
72
+ }
73
+
74
+ #success-label {
75
+ color: $success;
76
+ text-style: bold;
77
+ }
78
+
79
+ #dialog-buttons {
80
+ layout: horizontal;
81
+ align-horizontal: center;
82
+ height: auto;
83
+ }
84
+
85
+ #dialog-buttons Button {
86
+ margin: 0 1;
87
+ }
88
+
89
+ /* Hide elements initially */
90
+ #success-label {
91
+ display: none;
92
+ }
93
+
94
+ #error-label {
95
+ display: none;
96
+ }
97
+
98
+ /* Compact styles for short terminals */
99
+ SpecPullScreen.compact #dialog-container {
100
+ padding: 0 2;
101
+ max-height: 98%;
102
+ }
103
+
104
+ SpecPullScreen.compact #dialog-title {
105
+ padding-bottom: 0;
106
+ }
107
+ """
108
+
109
+ BINDINGS = [
110
+ ("escape", "cancel", "Cancel"),
111
+ ]
112
+
113
+ def __init__(self, version_id: str) -> None:
114
+ """Initialize the screen.
115
+
116
+ Args:
117
+ version_id: Version UUID to pull.
118
+ """
119
+ super().__init__()
120
+ self.version_id = version_id
121
+ self._success = False
122
+ self._download_worker: Worker[None] | None = None
123
+ self._cancelled = False
124
+
125
+ def compose(self) -> ComposeResult:
126
+ """Compose the screen widgets."""
127
+ with Container(id="dialog-container"):
128
+ yield Label("Pulling spec from cloud", id="dialog-title")
129
+
130
+ # Progress section
131
+ yield Static("Fetching version info...", id="phase-label")
132
+ yield ProgressBar(total=100, id="progress-bar")
133
+ yield Static("", id="file-label")
134
+
135
+ # Error section (hidden by default)
136
+ yield Static("", id="error-label")
137
+
138
+ # Success section (hidden by default)
139
+ yield Static("Spec pulled successfully!", id="success-label")
140
+
141
+ # Buttons
142
+ with Horizontal(id="dialog-buttons"):
143
+ yield Button("Cancel", id="cancel-btn")
144
+ yield Button("Continue", variant="primary", id="done-btn")
145
+
146
+ def on_mount(self) -> None:
147
+ """Start the download when screen is mounted."""
148
+ # Hide done button initially
149
+ self.query_one("#done-btn", Button).display = False
150
+
151
+ # Apply compact layout if starting in a short terminal
152
+ self._apply_compact_layout(self.app.size.height < COMPACT_HEIGHT_THRESHOLD)
153
+
154
+ # Start the download
155
+ self._start_download()
156
+
157
+ @on(Resize)
158
+ def handle_resize(self, event: Resize) -> None:
159
+ """Adjust layout based on terminal height."""
160
+ self._apply_compact_layout(event.size.height < COMPACT_HEIGHT_THRESHOLD)
161
+
162
+ def _apply_compact_layout(self, compact: bool) -> None:
163
+ """Apply or remove compact layout class for short terminals."""
164
+ if compact:
165
+ self.add_class("compact")
166
+ else:
167
+ self.remove_class("compact")
168
+
169
+ @work(exclusive=True)
170
+ async def _start_download(self) -> None:
171
+ """Run the download pipeline."""
172
+ worker = get_current_worker()
173
+ self._download_worker = worker
174
+
175
+ shotgun_dir = get_shotgun_base_path()
176
+ service = SpecPullService()
177
+
178
+ def on_progress(p: PullProgress) -> None:
179
+ pct = 0.0
180
+ if p.total_files and p.file_index is not None:
181
+ pct = ((p.file_index + 1) / p.total_files) * 100
182
+ self._update_phase(p.phase, progress=pct, current_file=p.current_file)
183
+
184
+ try:
185
+ result = await service.pull_version(
186
+ version_id=self.version_id,
187
+ shotgun_dir=shotgun_dir,
188
+ on_progress=on_progress,
189
+ is_cancelled=lambda: worker.is_cancelled,
190
+ )
191
+
192
+ if result.success:
193
+ self._update_title(f"Pulled: {result.spec_name}")
194
+ self._success = True
195
+ self._show_success()
196
+ else:
197
+ self._show_error(result.error or "Unknown error")
198
+
199
+ except CancelledError:
200
+ self._cancelled = True
201
+ self._show_cancelled()
202
+ except UnauthorizedError:
203
+ self._show_error("Not authenticated. Please try again.")
204
+ except NotFoundError:
205
+ self._show_error(f"Version not found: {self.version_id}")
206
+ except ForbiddenError:
207
+ self._show_error("You don't have access to this spec.")
208
+ except Exception as e:
209
+ logger.exception(f"Download failed: {type(e).__name__}: {e}")
210
+ error_msg = str(e) if str(e) else type(e).__name__
211
+ self._show_error(error_msg)
212
+
213
+ def _update_title(self, title: str) -> None:
214
+ """Update the dialog title."""
215
+ self.query_one("#dialog-title", Label).update(title)
216
+
217
+ def _update_phase(
218
+ self,
219
+ phase_text: str,
220
+ progress: float = 0,
221
+ current_file: str | None = None,
222
+ ) -> None:
223
+ """Update the progress UI."""
224
+ self.query_one("#phase-label", Static).update(phase_text)
225
+ self.query_one("#progress-bar", ProgressBar).update(progress=progress)
226
+
227
+ file_label = self.query_one("#file-label", Static)
228
+ if current_file:
229
+ file_label.update(f"Current: {current_file}")
230
+ else:
231
+ file_label.update("")
232
+
233
+ def _show_success(self) -> None:
234
+ """Show success state."""
235
+ self.query_one("#phase-label", Static).update("Download complete!")
236
+ self.query_one("#progress-bar", ProgressBar).update(progress=100)
237
+ self.query_one("#success-label", Static).display = True
238
+ self.query_one("#cancel-btn", Button).display = False
239
+ self.query_one("#done-btn", Button).display = True
240
+
241
+ def _show_error(self, error: str) -> None:
242
+ """Show error state."""
243
+ error_label = self.query_one("#error-label", Static)
244
+ error_label.update(f"Error: {error}")
245
+ error_label.display = True
246
+ self.query_one("#cancel-btn", Button).display = False
247
+ self.query_one("#done-btn", Button).display = True
248
+ self.query_one("#done-btn", Button).label = "Close"
249
+
250
+ def _show_cancelled(self) -> None:
251
+ """Show cancelled state."""
252
+ self.query_one("#phase-label", Static).update("Download cancelled")
253
+ self.query_one("#cancel-btn", Button).display = False
254
+ self.query_one("#done-btn", Button).display = True
255
+ self.query_one("#done-btn", Button).label = "Close"
256
+
257
+ @on(Button.Pressed, "#cancel-btn")
258
+ def _on_cancel(self, event: Button.Pressed) -> None:
259
+ """Handle cancel button."""
260
+ event.stop()
261
+ self._cancel_download()
262
+
263
+ @on(Button.Pressed, "#done-btn")
264
+ def _on_done(self, event: Button.Pressed) -> None:
265
+ """Handle done button."""
266
+ event.stop()
267
+ self.dismiss(self._success)
268
+
269
+ def action_cancel(self) -> None:
270
+ """Handle escape key."""
271
+ if (
272
+ self._success
273
+ or self._cancelled
274
+ or self.query_one("#error-label", Static).display
275
+ ):
276
+ # Already finished, just dismiss
277
+ self.dismiss(self._success)
278
+ else:
279
+ # Download in progress, cancel it
280
+ self._cancel_download()
281
+
282
+ def _cancel_download(self) -> None:
283
+ """Cancel the download."""
284
+ if self._download_worker and not self._download_worker.is_cancelled:
285
+ self._cancelled = True
286
+ self._download_worker.cancel()
@@ -2,14 +2,18 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import webbrowser
5
6
  from typing import TYPE_CHECKING, cast
6
7
 
7
8
  from textual import on
8
9
  from textual.app import ComposeResult
9
10
  from textual.containers import Container, Horizontal, Vertical
11
+ from textual.events import Resize
10
12
  from textual.screen import Screen
11
13
  from textual.widgets import Button, Markdown, Static
12
14
 
15
+ from shotgun.tui.layout import TINY_HEIGHT_THRESHOLD
16
+
13
17
  if TYPE_CHECKING:
14
18
  from ..app import ShotgunApp
15
19
 
@@ -100,6 +104,54 @@ class WelcomeScreen(Screen[None]):
100
104
  color: $warning;
101
105
  padding: 0 0 1 0;
102
106
  }
107
+
108
+ /* Tiny screen fallback */
109
+ #tiny-welcome-container {
110
+ display: none;
111
+ width: 100%;
112
+ height: auto;
113
+ padding: 0;
114
+ align: center middle;
115
+ }
116
+
117
+ #tiny-welcome-message {
118
+ text-align: center;
119
+ padding: 0;
120
+ }
121
+
122
+ #tiny-welcome-link {
123
+ text-align: center;
124
+ padding: 0;
125
+ color: $accent;
126
+ }
127
+
128
+ #tiny-welcome-buttons {
129
+ width: auto;
130
+ height: auto;
131
+ padding: 0;
132
+ align: center middle;
133
+ }
134
+
135
+ #tiny-welcome-buttons Button {
136
+ margin: 0 1;
137
+ }
138
+
139
+ /* Tiny mode - hide full welcome, show minimal */
140
+ WelcomeScreen.tiny #titlebox {
141
+ display: none;
142
+ }
143
+
144
+ WelcomeScreen.tiny #options-container {
145
+ display: none;
146
+ }
147
+
148
+ WelcomeScreen.tiny #migration-warning {
149
+ display: none;
150
+ }
151
+
152
+ WelcomeScreen.tiny #tiny-welcome-container {
153
+ display: block;
154
+ }
103
155
  """
104
156
 
105
157
  BINDINGS = [
@@ -107,6 +159,24 @@ class WelcomeScreen(Screen[None]):
107
159
  ]
108
160
 
109
161
  def compose(self) -> ComposeResult:
162
+ # Tiny screen fallback
163
+ with Container(id="tiny-welcome-container"):
164
+ yield Static(
165
+ "Welcome to Shotgun",
166
+ id="tiny-welcome-message",
167
+ )
168
+ yield Static(
169
+ "[@click=screen.open_usage_guide]View setup instructions[/]",
170
+ id="tiny-welcome-link",
171
+ markup=True,
172
+ )
173
+ with Horizontal(id="tiny-welcome-buttons"):
174
+ yield Button(
175
+ "Shotgun Account", id="tiny-shotgun-button", variant="primary"
176
+ )
177
+ yield Button("BYOK", id="tiny-byok-button", variant="success")
178
+
179
+ # Full welcome screen
110
180
  with Vertical(id="titlebox"):
111
181
  yield Static("Welcome to Shotgun", id="welcome-title")
112
182
  yield Static(
@@ -168,10 +238,29 @@ class WelcomeScreen(Screen[None]):
168
238
 
169
239
  def on_mount(self) -> None:
170
240
  """Focus the first button on mount."""
241
+ self._apply_layout_for_height(self.app.size.height)
171
242
  self.query_one("#shotgun-button", Button).focus()
172
243
  # Update BYOK button text asynchronously
173
244
  self.run_worker(self._update_byok_button_text(), exclusive=False)
174
245
 
246
+ @on(Resize)
247
+ def handle_resize(self, event: Resize) -> None:
248
+ """Adjust layout based on terminal height."""
249
+ self._apply_layout_for_height(event.size.height)
250
+
251
+ def _apply_layout_for_height(self, height: int) -> None:
252
+ """Apply appropriate layout based on terminal height."""
253
+ if height < TINY_HEIGHT_THRESHOLD:
254
+ self.add_class("tiny")
255
+ else:
256
+ self.remove_class("tiny")
257
+
258
+ def action_open_usage_guide(self) -> None:
259
+ """Open the usage guide in browser."""
260
+ webbrowser.open(
261
+ "https://github.com/shotgun-sh/shotgun?tab=readme-ov-file#-usage"
262
+ )
263
+
175
264
  async def _update_byok_button_text(self) -> None:
176
265
  """Update BYOK button text based on whether user has existing providers."""
177
266
  byok_button = self.query_one("#byok-button", Button)
@@ -180,11 +269,13 @@ class WelcomeScreen(Screen[None]):
180
269
  byok_button.label = "I'll stick with my BYOK setup"
181
270
 
182
271
  @on(Button.Pressed, "#shotgun-button")
272
+ @on(Button.Pressed, "#tiny-shotgun-button")
183
273
  def _on_shotgun_pressed(self) -> None:
184
274
  """Handle Shotgun Account button press."""
185
275
  self.run_worker(self._start_shotgun_auth(), exclusive=True)
186
276
 
187
277
  @on(Button.Pressed, "#byok-button")
278
+ @on(Button.Pressed, "#tiny-byok-button")
188
279
  def _on_byok_pressed(self) -> None:
189
280
  """Handle BYOK button press."""
190
281
  self.run_worker(self._start_byok_config(), exclusive=True)
@@ -10,8 +10,11 @@ from typing import TYPE_CHECKING
10
10
 
11
11
  import aiofiles.os
12
12
 
13
- from shotgun.agents.conversation_history import ConversationHistory, ConversationState
14
- from shotgun.agents.conversation_manager import ConversationManager
13
+ from shotgun.agents.conversation import (
14
+ ConversationHistory,
15
+ ConversationManager,
16
+ ConversationState,
17
+ )
15
18
  from shotgun.agents.models import AgentType
16
19
 
17
20
  if TYPE_CHECKING:
@@ -24,8 +24,8 @@ from shotgun.tui.screens.chat_screen.history.chat_history import ChatHistory
24
24
 
25
25
  if TYPE_CHECKING:
26
26
  from shotgun.agents.context_analyzer.models import ContextAnalysis
27
- from shotgun.agents.conversation_history import HintMessage
28
27
  from shotgun.tui.screens.chat import ChatScreen
28
+ from shotgun.tui.screens.chat_screen.hint_message import HintMessage
29
29
 
30
30
  logger = logging.getLogger(__name__)
31
31
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shotgun-sh
3
- Version: 0.2.23.dev1
3
+ Version: 0.2.29.dev2
4
4
  Summary: AI-powered research, planning, and task management CLI tool
5
5
  Project-URL: Homepage, https://shotgun.sh/
6
6
  Project-URL: Repository, https://github.com/shotgun-sh/shotgun