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,390 @@
1
+ """Dialog for selecting an existing spec or creating a new one."""
2
+
3
+ from datetime import datetime, timezone
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, ListItem, ListView, Static
11
+
12
+ from shotgun.logging_config import get_logger
13
+ from shotgun.shotgun_web.exceptions import (
14
+ ForbiddenError,
15
+ NotFoundError,
16
+ UnauthorizedError,
17
+ )
18
+ from shotgun.shotgun_web.models import SpecResponse, WorkspaceNotFoundError
19
+ from shotgun.shotgun_web.specs_client import SpecsClient
20
+ from shotgun.tui.layout import COMPACT_HEIGHT_THRESHOLD
21
+ from shotgun.tui.screens.shared_specs.models import (
22
+ ShareSpecsAction,
23
+ ShareSpecsResult,
24
+ )
25
+
26
+ logger = get_logger(__name__)
27
+
28
+
29
+ def _relative_time(dt: datetime | None) -> str:
30
+ """Convert datetime to relative time string (e.g., '2 days ago')."""
31
+ if dt is None:
32
+ return "unknown"
33
+ now = datetime.now(timezone.utc)
34
+ # Make sure dt is timezone-aware
35
+ if dt.tzinfo is None:
36
+ dt = dt.replace(tzinfo=timezone.utc)
37
+
38
+ diff = now - dt
39
+ seconds = diff.total_seconds()
40
+
41
+ if seconds < 60:
42
+ return "just now"
43
+ elif seconds < 3600:
44
+ minutes = int(seconds / 60)
45
+ return f"{minutes} min{'s' if minutes != 1 else ''} ago"
46
+ elif seconds < 86400:
47
+ hours = int(seconds / 3600)
48
+ return f"{hours} hour{'s' if hours != 1 else ''} ago"
49
+ elif seconds < 604800:
50
+ days = int(seconds / 86400)
51
+ return f"{days} day{'s' if days != 1 else ''} ago"
52
+ elif seconds < 2592000:
53
+ weeks = int(seconds / 604800)
54
+ return f"{weeks} week{'s' if weeks != 1 else ''} ago"
55
+ else:
56
+ months = int(seconds / 2592000)
57
+ return f"{months} month{'s' if months != 1 else ''} ago"
58
+
59
+
60
+ class ShareSpecsDialog(ModalScreen[ShareSpecsResult | None]):
61
+ """Dialog for selecting an existing spec or creating a new one.
62
+
63
+ Shows a list of existing specs in the workspace with:
64
+ - Name (bold)
65
+ - Latest version label or "No versions"
66
+ - Created date (relative time)
67
+ - Visibility badge (Team/Public)
68
+
69
+ Plus a "Create new spec" option at the top.
70
+
71
+ Returns ShareSpecsResult or None if cancelled.
72
+ """
73
+
74
+ DEFAULT_CSS = """
75
+ ShareSpecsDialog {
76
+ align: center middle;
77
+ background: rgba(0, 0, 0, 0.0);
78
+ }
79
+
80
+ ShareSpecsDialog > #dialog-container {
81
+ width: 80%;
82
+ max-width: 90;
83
+ height: auto;
84
+ max-height: 80%;
85
+ border: wide $primary;
86
+ padding: 1 2;
87
+ layout: vertical;
88
+ background: $surface;
89
+ }
90
+
91
+ #dialog-title {
92
+ text-style: bold;
93
+ color: $text-accent;
94
+ padding-bottom: 1;
95
+ text-align: center;
96
+ }
97
+
98
+ #dialog-subtitle {
99
+ color: $text-muted;
100
+ padding-bottom: 1;
101
+ text-align: center;
102
+ }
103
+
104
+ #spec-list {
105
+ height: auto;
106
+ max-height: 20;
107
+ border: solid $border;
108
+ padding: 0 1;
109
+ }
110
+
111
+ #spec-list > ListItem {
112
+ padding: 1 0;
113
+ }
114
+
115
+ #spec-list > ListItem.--highlight {
116
+ background: $primary-darken-2;
117
+ }
118
+
119
+ .spec-item-label {
120
+ width: 100%;
121
+ }
122
+
123
+ .spec-name {
124
+ text-style: bold;
125
+ }
126
+
127
+ .spec-meta {
128
+ color: $text-muted;
129
+ }
130
+
131
+ .visibility-badge-public {
132
+ color: $success;
133
+ }
134
+
135
+ .visibility-badge-team {
136
+ color: $warning;
137
+ }
138
+
139
+ #loading-label {
140
+ text-align: center;
141
+ padding: 2;
142
+ color: $text-muted;
143
+ }
144
+
145
+ #error-label {
146
+ text-align: center;
147
+ padding: 2;
148
+ color: $error;
149
+ }
150
+
151
+ #dialog-buttons {
152
+ layout: horizontal;
153
+ align-horizontal: right;
154
+ height: auto;
155
+ padding-top: 1;
156
+ }
157
+
158
+ #dialog-buttons Button {
159
+ margin-left: 1;
160
+ }
161
+
162
+ /* Compact styles for short terminals */
163
+ ShareSpecsDialog.compact #dialog-container {
164
+ padding: 0 2;
165
+ max-height: 98%;
166
+ }
167
+
168
+ ShareSpecsDialog.compact #dialog-title {
169
+ padding-bottom: 0;
170
+ }
171
+
172
+ ShareSpecsDialog.compact #dialog-subtitle {
173
+ padding-bottom: 0;
174
+ }
175
+
176
+ ShareSpecsDialog.compact #spec-list {
177
+ max-height: 10;
178
+ }
179
+ """
180
+
181
+ BINDINGS = [
182
+ ("escape", "cancel", "Cancel"),
183
+ ]
184
+
185
+ def __init__(self) -> None:
186
+ """Initialize the dialog."""
187
+ super().__init__()
188
+ self.workspace_id: str | None = None
189
+ self._specs: list[SpecResponse] = []
190
+ self._loading = True
191
+ self._error: str | None = None
192
+
193
+ def compose(self) -> ComposeResult:
194
+ """Compose the dialog widgets."""
195
+ with Container(id="dialog-container"):
196
+ yield Label("Share Specs", id="dialog-title")
197
+ yield Static(
198
+ "Select an existing spec to add a new version, or create a new spec.",
199
+ id="dialog-subtitle",
200
+ )
201
+ yield Static("Loading specs...", id="loading-label")
202
+ yield Static("", id="error-label")
203
+ yield ListView(id="spec-list")
204
+ with Horizontal(id="dialog-buttons"):
205
+ yield Button("Cancel", id="cancel")
206
+
207
+ def on_mount(self) -> None:
208
+ """Load specs when dialog is mounted."""
209
+ # Hide list and error initially
210
+ self.query_one("#spec-list", ListView).display = False
211
+ self.query_one("#error-label", Static).display = False
212
+
213
+ # Apply compact layout if starting in a short terminal
214
+ self._apply_compact_layout(self.app.size.height < COMPACT_HEIGHT_THRESHOLD)
215
+
216
+ # Load specs
217
+ self._load_specs()
218
+
219
+ @on(Resize)
220
+ def handle_resize(self, event: Resize) -> None:
221
+ """Adjust layout based on terminal height."""
222
+ self._apply_compact_layout(event.size.height < COMPACT_HEIGHT_THRESHOLD)
223
+
224
+ def _apply_compact_layout(self, compact: bool) -> None:
225
+ """Apply or remove compact layout class for short terminals."""
226
+ if compact:
227
+ self.add_class("compact")
228
+ else:
229
+ self.remove_class("compact")
230
+
231
+ def _update_loading_message(self, message: str) -> None:
232
+ """Update the loading label message."""
233
+ self.query_one("#loading-label", Static).update(message)
234
+
235
+ @work
236
+ async def _load_specs(self) -> None:
237
+ """Fetch workspace, check permissions, and load specs."""
238
+ try:
239
+ client = SpecsClient()
240
+
241
+ # Step 1: Get workspace
242
+ self._update_loading_message("Connecting to workspace...")
243
+ try:
244
+ self.workspace_id = await client.get_or_fetch_workspace_id()
245
+ except UnauthorizedError:
246
+ self._loading = False
247
+ self._error = "Not authenticated. Run 'shotgun auth' to login."
248
+ self._show_error()
249
+ return
250
+ except WorkspaceNotFoundError:
251
+ self._loading = False
252
+ self._error = "No workspaces available. Please create one at shotgun.sh"
253
+ self._show_error()
254
+ return
255
+
256
+ # Step 2: Check permissions
257
+ self._update_loading_message("Checking permissions...")
258
+ try:
259
+ permissions = await client.check_permissions(self.workspace_id)
260
+ if not permissions.can_create_specs:
261
+ self._loading = False
262
+ self._error = "You need editor access to share specs"
263
+ self._show_error()
264
+ return
265
+ except NotFoundError:
266
+ # Permissions endpoint not available yet - skip check
267
+ logger.debug("Permissions endpoint not available, skipping check")
268
+ except UnauthorizedError:
269
+ self._loading = False
270
+ self._error = "Not authenticated. Run 'shotgun auth' to login."
271
+ self._show_error()
272
+ return
273
+ except ForbiddenError:
274
+ self._loading = False
275
+ self._error = "You don't have access to this workspace"
276
+ self._show_error()
277
+ return
278
+
279
+ # Step 3: Load specs
280
+ self._update_loading_message("Loading specs...")
281
+ response = await client.list_specs(self.workspace_id)
282
+ self._specs = response.specs
283
+ self._loading = False
284
+ self._populate_list()
285
+ except Exception as e:
286
+ logger.exception(f"Failed to load specs: {type(e).__name__}: {e}")
287
+ self._loading = False
288
+ self._error = str(e) if str(e) else type(e).__name__
289
+ self._show_error()
290
+
291
+ def _populate_list(self) -> None:
292
+ """Populate the ListView with specs."""
293
+ # Hide loading label
294
+ self.query_one("#loading-label", Static).display = False
295
+
296
+ # Show and populate list
297
+ list_view = self.query_one("#spec-list", ListView)
298
+ list_view.display = True
299
+
300
+ # Clear existing items
301
+ list_view.clear()
302
+
303
+ # Add "Create new spec" option first
304
+ create_label = Label(
305
+ "[bold]+ Create new spec[/]\nStart a fresh spec for your .shotgun/ files",
306
+ classes="spec-item-label",
307
+ )
308
+ list_view.append(ListItem(create_label, id="create-new"))
309
+
310
+ # Add existing specs
311
+ for spec in self._specs:
312
+ label = self._format_spec_label(spec)
313
+ list_view.append(ListItem(label, id=f"spec-{spec.id}"))
314
+
315
+ # Focus the list
316
+ list_view.focus()
317
+
318
+ def _format_spec_label(self, spec: SpecResponse) -> Label:
319
+ """Format a spec as a label for display."""
320
+ # Name
321
+ name = spec.name
322
+
323
+ # Latest version info
324
+ if spec.latest_version and spec.latest_version.label:
325
+ version_info = spec.latest_version.label
326
+ elif spec.latest_version:
327
+ version_info = "Latest version"
328
+ else:
329
+ version_info = "No versions"
330
+
331
+ # Visibility badge
332
+ if spec.is_public:
333
+ visibility = "[green]Public[/]"
334
+ else:
335
+ visibility = "[yellow]Team[/]"
336
+
337
+ # Relative time
338
+ time_ago = _relative_time(spec.created_on)
339
+
340
+ # Format the label
341
+ text = f"[bold]{name}[/] · {visibility}\n{version_info} · {time_ago}"
342
+ return Label(text, classes="spec-item-label")
343
+
344
+ def _show_error(self) -> None:
345
+ """Show error message."""
346
+ self.query_one("#loading-label", Static).display = False
347
+ error_label = self.query_one("#error-label", Static)
348
+ error_label.update(f"Failed to load specs: {self._error}")
349
+ error_label.display = True
350
+
351
+ @on(ListView.Selected)
352
+ def _on_spec_selected(self, event: ListView.Selected) -> None:
353
+ """Handle spec selection."""
354
+ item = event.item
355
+ if item is None or item.id is None:
356
+ return
357
+
358
+ if item.id == "create-new":
359
+ self.dismiss(
360
+ ShareSpecsResult(
361
+ action=ShareSpecsAction.CREATE,
362
+ workspace_id=self.workspace_id,
363
+ )
364
+ )
365
+ elif item.id.startswith("spec-"):
366
+ spec_id = item.id.removeprefix("spec-")
367
+ # Find the spec to get the name
368
+ spec_name = None
369
+ for spec in self._specs:
370
+ if spec.id == spec_id:
371
+ spec_name = spec.name
372
+ break
373
+ self.dismiss(
374
+ ShareSpecsResult(
375
+ action=ShareSpecsAction.ADD_VERSION,
376
+ workspace_id=self.workspace_id,
377
+ spec_id=spec_id,
378
+ spec_name=spec_name,
379
+ )
380
+ )
381
+
382
+ @on(Button.Pressed, "#cancel")
383
+ def _on_cancel(self, event: Button.Pressed) -> None:
384
+ """Handle cancel button."""
385
+ event.stop()
386
+ self.dismiss(None)
387
+
388
+ def action_cancel(self) -> None:
389
+ """Handle escape key."""
390
+ self.dismiss(None)