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,31 @@
|
|
|
1
|
+
"""Supabase Storage download utilities."""
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
|
|
5
|
+
from shotgun.logging_config import get_logger
|
|
6
|
+
|
|
7
|
+
logger = get_logger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
async def download_file_from_url(download_url: str) -> bytes:
|
|
11
|
+
"""Download a file from a presigned Supabase Storage URL.
|
|
12
|
+
|
|
13
|
+
The API returns presigned URLs with embedded tokens that don't require
|
|
14
|
+
any authentication headers.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
download_url: Presigned Supabase Storage URL
|
|
18
|
+
(e.g., "https://...supabase.co/storage/v1/object/sign/...?token=...")
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
File contents as bytes
|
|
22
|
+
|
|
23
|
+
Raises:
|
|
24
|
+
httpx.HTTPStatusError: If download fails
|
|
25
|
+
"""
|
|
26
|
+
logger.debug("Downloading file from: %s", download_url)
|
|
27
|
+
|
|
28
|
+
async with httpx.AsyncClient(timeout=60.0) as client:
|
|
29
|
+
response = await client.get(download_url)
|
|
30
|
+
response.raise_for_status()
|
|
31
|
+
return response.content
|
shotgun/tui/app.py
CHANGED
|
@@ -54,12 +54,16 @@ class ShotgunApp(App[None]):
|
|
|
54
54
|
no_update_check: bool = False,
|
|
55
55
|
continue_session: bool = False,
|
|
56
56
|
force_reindex: bool = False,
|
|
57
|
+
show_pull_hint: bool = False,
|
|
58
|
+
pull_version_id: str | None = None,
|
|
57
59
|
) -> None:
|
|
58
60
|
super().__init__()
|
|
59
61
|
self.config_manager: ConfigManager = get_config_manager()
|
|
60
62
|
self.no_update_check = no_update_check
|
|
61
63
|
self.continue_session = continue_session
|
|
62
64
|
self.force_reindex = force_reindex
|
|
65
|
+
self.show_pull_hint = show_pull_hint
|
|
66
|
+
self.pull_version_id = pull_version_id
|
|
63
67
|
|
|
64
68
|
# Initialize dependency injection container
|
|
65
69
|
self.container = TUIContainer()
|
|
@@ -77,6 +81,8 @@ class ShotgunApp(App[None]):
|
|
|
77
81
|
"tui_started",
|
|
78
82
|
{
|
|
79
83
|
"installation_method": detect_installation_method(),
|
|
84
|
+
"terminal_width": self.size.width,
|
|
85
|
+
"terminal_height": self.size.height,
|
|
80
86
|
},
|
|
81
87
|
)
|
|
82
88
|
|
|
@@ -149,6 +155,16 @@ class ShotgunApp(App[None]):
|
|
|
149
155
|
if isinstance(self.screen, ChatScreen):
|
|
150
156
|
return
|
|
151
157
|
|
|
158
|
+
# If we have a version to pull, show pull screen first
|
|
159
|
+
if self.pull_version_id:
|
|
160
|
+
from .screens.spec_pull import SpecPullScreen
|
|
161
|
+
|
|
162
|
+
self.push_screen(
|
|
163
|
+
SpecPullScreen(self.pull_version_id),
|
|
164
|
+
callback=self._handle_pull_complete,
|
|
165
|
+
)
|
|
166
|
+
return
|
|
167
|
+
|
|
152
168
|
# Create ChatScreen with all dependencies injected from container
|
|
153
169
|
# Get the default agent mode (RESEARCH)
|
|
154
170
|
agent_mode = AgentType.RESEARCH
|
|
@@ -180,6 +196,7 @@ class ShotgunApp(App[None]):
|
|
|
180
196
|
deps=agent_deps,
|
|
181
197
|
continue_session=self.continue_session,
|
|
182
198
|
force_reindex=self.force_reindex,
|
|
199
|
+
show_pull_hint=self.show_pull_hint,
|
|
183
200
|
)
|
|
184
201
|
|
|
185
202
|
# Update the ProcessingStateManager and WidgetCoordinator with the actual ChatScreen instance
|
|
@@ -195,6 +212,22 @@ class ShotgunApp(App[None]):
|
|
|
195
212
|
shotgun_dir = get_shotgun_base_path()
|
|
196
213
|
return shotgun_dir.exists() and shotgun_dir.is_dir()
|
|
197
214
|
|
|
215
|
+
def _handle_pull_complete(self, success: bool | None) -> None:
|
|
216
|
+
"""Handle completion of spec pull screen.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
success: Whether the pull was successful, or None if dismissed.
|
|
220
|
+
"""
|
|
221
|
+
# Clear version_id so we don't pull again on next refresh
|
|
222
|
+
self.pull_version_id = None
|
|
223
|
+
|
|
224
|
+
if success:
|
|
225
|
+
# Enable hint for ChatScreen
|
|
226
|
+
self.show_pull_hint = True
|
|
227
|
+
|
|
228
|
+
# Continue to ChatScreen
|
|
229
|
+
self.refresh_startup_screen()
|
|
230
|
+
|
|
198
231
|
async def action_quit(self) -> None:
|
|
199
232
|
"""Quit the application."""
|
|
200
233
|
# Shut down PostHog client to prevent threading errors
|
|
@@ -221,6 +254,8 @@ def run(
|
|
|
221
254
|
no_update_check: bool = False,
|
|
222
255
|
continue_session: bool = False,
|
|
223
256
|
force_reindex: bool = False,
|
|
257
|
+
show_pull_hint: bool = False,
|
|
258
|
+
pull_version_id: str | None = None,
|
|
224
259
|
) -> None:
|
|
225
260
|
"""Run the TUI application.
|
|
226
261
|
|
|
@@ -228,6 +263,8 @@ def run(
|
|
|
228
263
|
no_update_check: If True, disable automatic update checks.
|
|
229
264
|
continue_session: If True, continue from previous conversation.
|
|
230
265
|
force_reindex: If True, force re-indexing of codebase (ignores existing index).
|
|
266
|
+
show_pull_hint: If True, show hint about recently pulled spec.
|
|
267
|
+
pull_version_id: If provided, pull this spec version before showing ChatScreen.
|
|
231
268
|
"""
|
|
232
269
|
# Clean up any corrupted databases BEFORE starting the TUI
|
|
233
270
|
# This prevents crashes from corrupted databases during initialization
|
|
@@ -253,6 +290,8 @@ def run(
|
|
|
253
290
|
no_update_check=no_update_check,
|
|
254
291
|
continue_session=continue_session,
|
|
255
292
|
force_reindex=force_reindex,
|
|
293
|
+
show_pull_hint=show_pull_hint,
|
|
294
|
+
pull_version_id=pull_version_id,
|
|
256
295
|
)
|
|
257
296
|
app.run(inline_no_clear=True)
|
|
258
297
|
|
shotgun/tui/containers.py
CHANGED
|
@@ -5,7 +5,7 @@ from typing import TYPE_CHECKING
|
|
|
5
5
|
from dependency_injector import containers, providers
|
|
6
6
|
from pydantic_ai import RunContext
|
|
7
7
|
|
|
8
|
-
from shotgun.agents.
|
|
8
|
+
from shotgun.agents.conversation import ConversationManager
|
|
9
9
|
from shotgun.agents.models import AgentDeps
|
|
10
10
|
from shotgun.sdk.codebase import CodebaseSDK
|
|
11
11
|
from shotgun.tui.commands import CommandHandler
|
shotgun/tui/layout.py
ADDED
|
@@ -36,9 +36,11 @@ from shotgun.agents.agent_manager import (
|
|
|
36
36
|
)
|
|
37
37
|
from shotgun.agents.config import get_config_manager
|
|
38
38
|
from shotgun.agents.config.models import MODEL_SPECS
|
|
39
|
-
from shotgun.agents.
|
|
40
|
-
from shotgun.agents.history.compaction import apply_persistent_compaction
|
|
41
|
-
from shotgun.agents.history.token_estimation import
|
|
39
|
+
from shotgun.agents.conversation import ConversationManager
|
|
40
|
+
from shotgun.agents.conversation.history.compaction import apply_persistent_compaction
|
|
41
|
+
from shotgun.agents.conversation.history.token_estimation import (
|
|
42
|
+
estimate_tokens_from_messages,
|
|
43
|
+
)
|
|
42
44
|
from shotgun.agents.models import (
|
|
43
45
|
AgentDeps,
|
|
44
46
|
AgentType,
|
|
@@ -83,16 +85,50 @@ from shotgun.tui.screens.chat_screen.hint_message import HintMessage
|
|
|
83
85
|
from shotgun.tui.screens.chat_screen.history import ChatHistory
|
|
84
86
|
from shotgun.tui.screens.confirmation_dialog import ConfirmationDialog
|
|
85
87
|
from shotgun.tui.screens.onboarding import OnboardingModal
|
|
88
|
+
from shotgun.tui.screens.shared_specs import (
|
|
89
|
+
CreateSpecDialog,
|
|
90
|
+
ShareSpecsAction,
|
|
91
|
+
ShareSpecsDialog,
|
|
92
|
+
UploadProgressScreen,
|
|
93
|
+
)
|
|
86
94
|
from shotgun.tui.services.conversation_service import ConversationService
|
|
87
95
|
from shotgun.tui.state.processing_state import ProcessingStateManager
|
|
88
96
|
from shotgun.tui.utils.mode_progress import PlaceholderHints
|
|
89
97
|
from shotgun.tui.widgets.widget_coordinator import WidgetCoordinator
|
|
90
98
|
from shotgun.utils import get_shotgun_home
|
|
99
|
+
from shotgun.utils.file_system_utils import get_shotgun_base_path
|
|
91
100
|
from shotgun.utils.marketing import MarketingManager
|
|
92
101
|
|
|
93
102
|
logger = logging.getLogger(__name__)
|
|
94
103
|
|
|
95
104
|
|
|
105
|
+
def _format_duration(seconds: float) -> str:
|
|
106
|
+
"""Format duration in natural language."""
|
|
107
|
+
if seconds < 60:
|
|
108
|
+
return f"{int(seconds)} seconds"
|
|
109
|
+
minutes = int(seconds // 60)
|
|
110
|
+
secs = int(seconds % 60)
|
|
111
|
+
if secs == 0:
|
|
112
|
+
return f"{minutes} minute{'s' if minutes != 1 else ''}"
|
|
113
|
+
return f"{minutes} minute{'s' if minutes != 1 else ''} {secs} seconds"
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _format_count(count: int) -> str:
|
|
117
|
+
"""Format count in natural language (e.g., '5 thousand')."""
|
|
118
|
+
if count < 1000:
|
|
119
|
+
return str(count)
|
|
120
|
+
elif count < 1_000_000:
|
|
121
|
+
thousands = count / 1000
|
|
122
|
+
if thousands == int(thousands):
|
|
123
|
+
return f"{int(thousands)} thousand"
|
|
124
|
+
return f"{thousands:.1f} thousand"
|
|
125
|
+
else:
|
|
126
|
+
millions = count / 1_000_000
|
|
127
|
+
if millions == int(millions):
|
|
128
|
+
return f"{int(millions)} million"
|
|
129
|
+
return f"{millions:.1f} million"
|
|
130
|
+
|
|
131
|
+
|
|
96
132
|
class ChatScreen(Screen[None]):
|
|
97
133
|
CSS_PATH = "chat.tcss"
|
|
98
134
|
|
|
@@ -138,6 +174,7 @@ class ChatScreen(Screen[None]):
|
|
|
138
174
|
deps: AgentDeps,
|
|
139
175
|
continue_session: bool = False,
|
|
140
176
|
force_reindex: bool = False,
|
|
177
|
+
show_pull_hint: bool = False,
|
|
141
178
|
) -> None:
|
|
142
179
|
"""Initialize the ChatScreen.
|
|
143
180
|
|
|
@@ -156,6 +193,7 @@ class ChatScreen(Screen[None]):
|
|
|
156
193
|
deps: AgentDeps configuration for agent dependencies
|
|
157
194
|
continue_session: Whether to continue a previous session
|
|
158
195
|
force_reindex: Whether to force reindexing of codebases
|
|
196
|
+
show_pull_hint: Whether to show hint about recently pulled spec
|
|
159
197
|
"""
|
|
160
198
|
super().__init__()
|
|
161
199
|
|
|
@@ -171,6 +209,7 @@ class ChatScreen(Screen[None]):
|
|
|
171
209
|
self.processing_state = processing_state
|
|
172
210
|
self.continue_session = continue_session
|
|
173
211
|
self.force_reindex = force_reindex
|
|
212
|
+
self.show_pull_hint = show_pull_hint
|
|
174
213
|
|
|
175
214
|
def on_mount(self) -> None:
|
|
176
215
|
# Use widget coordinator to focus input
|
|
@@ -186,6 +225,10 @@ class ChatScreen(Screen[None]):
|
|
|
186
225
|
if self.continue_session:
|
|
187
226
|
self.call_later(self._check_and_load_conversation)
|
|
188
227
|
|
|
228
|
+
# Show pull hint if launching after spec pull
|
|
229
|
+
if self.show_pull_hint:
|
|
230
|
+
self.call_later(self._show_pull_hint)
|
|
231
|
+
|
|
189
232
|
self.call_later(self.check_if_codebase_is_indexed)
|
|
190
233
|
# Initial update of context indicator
|
|
191
234
|
self.update_context_indicator()
|
|
@@ -655,6 +698,38 @@ class ChatScreen(Screen[None]):
|
|
|
655
698
|
hint = HintMessage(message=markdown)
|
|
656
699
|
self.agent_manager.add_hint_message(hint)
|
|
657
700
|
|
|
701
|
+
def _show_pull_hint(self) -> None:
|
|
702
|
+
"""Show hint about recently pulled spec from meta.json."""
|
|
703
|
+
# Import at runtime to avoid circular import (CLI -> TUI dependency)
|
|
704
|
+
from shotgun.cli.spec.models import SpecMeta
|
|
705
|
+
|
|
706
|
+
shotgun_dir = get_shotgun_base_path()
|
|
707
|
+
meta_path = shotgun_dir / "meta.json"
|
|
708
|
+
if not meta_path.exists():
|
|
709
|
+
return
|
|
710
|
+
|
|
711
|
+
try:
|
|
712
|
+
meta: SpecMeta = SpecMeta.model_validate_json(meta_path.read_text())
|
|
713
|
+
# Only show if pulled within last 60 seconds
|
|
714
|
+
age_seconds = (datetime.now(timezone.utc) - meta.pulled_at).total_seconds()
|
|
715
|
+
if age_seconds > 60:
|
|
716
|
+
return
|
|
717
|
+
|
|
718
|
+
hint_parts = [f"You just pulled **{meta.spec_name}** from the cloud."]
|
|
719
|
+
if meta.web_url:
|
|
720
|
+
hint_parts.append(f"[View in browser]({meta.web_url})")
|
|
721
|
+
hint_parts.append(
|
|
722
|
+
f"The specs are now located at `{shotgun_dir}` so Shotgun has access to them."
|
|
723
|
+
)
|
|
724
|
+
if meta.backup_path:
|
|
725
|
+
hint_parts.append(
|
|
726
|
+
f"Previous files were backed up to: `{meta.backup_path}`"
|
|
727
|
+
)
|
|
728
|
+
self.mount_hint("\n\n".join(hint_parts))
|
|
729
|
+
except Exception:
|
|
730
|
+
# Ignore errors reading meta.json - this is optional UI feedback
|
|
731
|
+
logger.debug("Failed to read meta.json for pull hint", exc_info=True)
|
|
732
|
+
|
|
658
733
|
def mount_hint_with_email(
|
|
659
734
|
self, markdown_before: str, email: str, markdown_after: str = ""
|
|
660
735
|
) -> None:
|
|
@@ -1035,6 +1110,71 @@ class ChatScreen(Screen[None]):
|
|
|
1035
1110
|
)
|
|
1036
1111
|
)
|
|
1037
1112
|
|
|
1113
|
+
def share_specs_command(self) -> None:
|
|
1114
|
+
"""Launch the share specs workflow."""
|
|
1115
|
+
self.call_later(lambda: self._start_share_specs_flow())
|
|
1116
|
+
|
|
1117
|
+
@work
|
|
1118
|
+
async def _start_share_specs_flow(self) -> None:
|
|
1119
|
+
"""Main workflow for sharing specs to workspace."""
|
|
1120
|
+
# 1. Check preconditions (instant check, no API call)
|
|
1121
|
+
shotgun_dir = Path.cwd() / ".shotgun"
|
|
1122
|
+
if not shotgun_dir.exists():
|
|
1123
|
+
self.mount_hint("No .shotgun/ directory found in current directory")
|
|
1124
|
+
return
|
|
1125
|
+
|
|
1126
|
+
# 2. Show spec selection dialog (handles workspace fetch, permissions, and spec loading)
|
|
1127
|
+
result = await self.app.push_screen_wait(ShareSpecsDialog())
|
|
1128
|
+
if result is None or result.action is None:
|
|
1129
|
+
return # User cancelled or error
|
|
1130
|
+
|
|
1131
|
+
workspace_id = result.workspace_id
|
|
1132
|
+
if not workspace_id:
|
|
1133
|
+
self.mount_hint("Failed to get workspace")
|
|
1134
|
+
return
|
|
1135
|
+
|
|
1136
|
+
# 3. Handle create vs add version
|
|
1137
|
+
if result.action == ShareSpecsAction.CREATE:
|
|
1138
|
+
# Show create spec dialog
|
|
1139
|
+
create_result = await self.app.push_screen_wait(CreateSpecDialog())
|
|
1140
|
+
if create_result is None:
|
|
1141
|
+
return # User cancelled
|
|
1142
|
+
|
|
1143
|
+
# Pass spec creation info to UploadProgressScreen
|
|
1144
|
+
# It will create the spec/version and then upload
|
|
1145
|
+
upload_result = await self.app.push_screen_wait(
|
|
1146
|
+
UploadProgressScreen(
|
|
1147
|
+
workspace_id,
|
|
1148
|
+
spec_name=create_result.name,
|
|
1149
|
+
spec_description=create_result.description,
|
|
1150
|
+
spec_is_public=create_result.is_public,
|
|
1151
|
+
)
|
|
1152
|
+
)
|
|
1153
|
+
|
|
1154
|
+
else: # add_version
|
|
1155
|
+
spec_id = result.spec_id
|
|
1156
|
+
if not spec_id:
|
|
1157
|
+
self.mount_hint("No spec selected")
|
|
1158
|
+
return
|
|
1159
|
+
|
|
1160
|
+
# Pass spec_id to UploadProgressScreen
|
|
1161
|
+
# It will create the version and then upload
|
|
1162
|
+
upload_result = await self.app.push_screen_wait(
|
|
1163
|
+
UploadProgressScreen(workspace_id, spec_id=spec_id)
|
|
1164
|
+
)
|
|
1165
|
+
|
|
1166
|
+
# 7. Show result
|
|
1167
|
+
if upload_result and upload_result.success:
|
|
1168
|
+
if upload_result.web_url:
|
|
1169
|
+
self.mount_hint(
|
|
1170
|
+
f"Specs shared successfully!\n\nView at: {upload_result.web_url}"
|
|
1171
|
+
)
|
|
1172
|
+
else:
|
|
1173
|
+
self.mount_hint("Specs shared successfully!")
|
|
1174
|
+
elif upload_result and upload_result.cancelled:
|
|
1175
|
+
self.mount_hint("Upload cancelled")
|
|
1176
|
+
# Error case is handled by the upload screen
|
|
1177
|
+
|
|
1038
1178
|
def delete_codebase_from_palette(self, graph_id: str) -> None:
|
|
1039
1179
|
stack = getattr(self.app, "screen_stack", None)
|
|
1040
1180
|
if stack and isinstance(stack[-1], CommandPalette):
|
|
@@ -1080,6 +1220,8 @@ class ChatScreen(Screen[None]):
|
|
|
1080
1220
|
|
|
1081
1221
|
@work
|
|
1082
1222
|
async def index_codebase(self, selection: CodebaseIndexSelection) -> None:
|
|
1223
|
+
index_start_time = time.time()
|
|
1224
|
+
|
|
1083
1225
|
label = self.query_one("#indexing-job-display", Static)
|
|
1084
1226
|
label.update(
|
|
1085
1227
|
f"[$foreground-muted]Indexing codebase: [bold $text-accent]{selection.name}[/][/]"
|
|
@@ -1119,24 +1261,40 @@ class ChatScreen(Screen[None]):
|
|
|
1119
1261
|
|
|
1120
1262
|
def progress_callback(progress_info: IndexProgress) -> None:
|
|
1121
1263
|
"""Update progress state (timer renders it independently)."""
|
|
1122
|
-
# Calculate overall percentage
|
|
1264
|
+
# Calculate overall percentage with weights based on actual timing:
|
|
1265
|
+
# Structure: 0-2%, Definitions: 2-18%, Relationships: 18-20%
|
|
1266
|
+
# Flush nodes: 20-28%, Flush relationships: 28-100%
|
|
1123
1267
|
if progress_info.phase == ProgressPhase.STRUCTURE:
|
|
1124
|
-
# Phase 1: 0-
|
|
1125
|
-
overall_pct =
|
|
1268
|
+
# Phase 1: 0-2% (actual: ~0%)
|
|
1269
|
+
overall_pct = 2.0 if progress_info.phase_complete else 1.0
|
|
1126
1270
|
elif progress_info.phase == ProgressPhase.DEFINITIONS:
|
|
1127
|
-
# Phase 2:
|
|
1271
|
+
# Phase 2: 2-18% based on files processed (actual: ~16%)
|
|
1128
1272
|
if progress_info.total and progress_info.total > 0:
|
|
1129
|
-
phase_pct = (progress_info.current / progress_info.total) *
|
|
1130
|
-
overall_pct =
|
|
1273
|
+
phase_pct = (progress_info.current / progress_info.total) * 16.0
|
|
1274
|
+
overall_pct = 2.0 + phase_pct
|
|
1131
1275
|
else:
|
|
1132
|
-
overall_pct =
|
|
1276
|
+
overall_pct = 2.0
|
|
1133
1277
|
elif progress_info.phase == ProgressPhase.RELATIONSHIPS:
|
|
1134
|
-
# Phase 3:
|
|
1278
|
+
# Phase 3: 18-20% based on relationships processed (actual: ~0.3%)
|
|
1279
|
+
if progress_info.total and progress_info.total > 0:
|
|
1280
|
+
phase_pct = (progress_info.current / progress_info.total) * 2.0
|
|
1281
|
+
overall_pct = 18.0 + phase_pct
|
|
1282
|
+
else:
|
|
1283
|
+
overall_pct = 18.0
|
|
1284
|
+
elif progress_info.phase == ProgressPhase.FLUSH_NODES:
|
|
1285
|
+
# Phase 4: 20-28% based on nodes flushed (actual: ~7.5%)
|
|
1286
|
+
if progress_info.total and progress_info.total > 0:
|
|
1287
|
+
phase_pct = (progress_info.current / progress_info.total) * 8.0
|
|
1288
|
+
overall_pct = 20.0 + phase_pct
|
|
1289
|
+
else:
|
|
1290
|
+
overall_pct = 20.0
|
|
1291
|
+
elif progress_info.phase == ProgressPhase.FLUSH_RELATIONSHIPS:
|
|
1292
|
+
# Phase 5: 28-100% based on relationships flushed (actual: ~76%)
|
|
1135
1293
|
if progress_info.total and progress_info.total > 0:
|
|
1136
|
-
phase_pct = (progress_info.current / progress_info.total) *
|
|
1137
|
-
overall_pct =
|
|
1294
|
+
phase_pct = (progress_info.current / progress_info.total) * 72.0
|
|
1295
|
+
overall_pct = 28.0 + phase_pct
|
|
1138
1296
|
else:
|
|
1139
|
-
overall_pct =
|
|
1297
|
+
overall_pct = 28.0
|
|
1140
1298
|
else:
|
|
1141
1299
|
overall_pct = 0.0
|
|
1142
1300
|
|
|
@@ -1189,12 +1347,19 @@ class ChatScreen(Screen[None]):
|
|
|
1189
1347
|
)
|
|
1190
1348
|
label.refresh()
|
|
1191
1349
|
|
|
1350
|
+
# Calculate duration and format message
|
|
1351
|
+
duration = time.time() - index_start_time
|
|
1352
|
+
duration_str = _format_duration(duration)
|
|
1353
|
+
entity_count = result.node_count + result.relationship_count
|
|
1354
|
+
entity_str = _format_count(entity_count)
|
|
1355
|
+
|
|
1192
1356
|
logger.info(
|
|
1193
|
-
f"Successfully indexed codebase '{result.name}'
|
|
1357
|
+
f"Successfully indexed codebase '{result.name}' in {duration_str} "
|
|
1358
|
+
f"({entity_count} entities)"
|
|
1194
1359
|
)
|
|
1195
1360
|
self.agent_manager.add_hint_message(
|
|
1196
1361
|
HintMessage(
|
|
1197
|
-
message=f"✓ Indexed
|
|
1362
|
+
message=f"✓ Indexed '{result.name}' in {duration_str} ({entity_str} entities)"
|
|
1198
1363
|
)
|
|
1199
1364
|
)
|
|
1200
1365
|
break # Success - exit retry loop
|
|
@@ -1279,6 +1444,11 @@ class ChatScreen(Screen[None]):
|
|
|
1279
1444
|
# Stop context indicator animation
|
|
1280
1445
|
self.widget_coordinator.set_context_streaming(False)
|
|
1281
1446
|
|
|
1447
|
+
# Check for low balance after agent loop completes (only for Shotgun Account)
|
|
1448
|
+
# This runs after processing but doesn't interfere with Q&A mode
|
|
1449
|
+
if self.deps.llm_model.is_shotgun_account:
|
|
1450
|
+
await self._check_low_balance_warning()
|
|
1451
|
+
|
|
1282
1452
|
# Save conversation after each interaction
|
|
1283
1453
|
self._save_conversation()
|
|
1284
1454
|
|
|
@@ -1293,6 +1463,32 @@ class ChatScreen(Screen[None]):
|
|
|
1293
1463
|
exclusive=True,
|
|
1294
1464
|
)
|
|
1295
1465
|
|
|
1466
|
+
async def _check_low_balance_warning(self) -> None:
|
|
1467
|
+
"""Check account balance and show warning if $2.50 or less remaining.
|
|
1468
|
+
|
|
1469
|
+
This runs after every agent loop completion for Shotgun Account users.
|
|
1470
|
+
Errors are silently caught to avoid disrupting user workflow.
|
|
1471
|
+
"""
|
|
1472
|
+
try:
|
|
1473
|
+
from shotgun.llm_proxy import LiteLLMProxyClient
|
|
1474
|
+
|
|
1475
|
+
client = LiteLLMProxyClient(self.deps.llm_model.api_key)
|
|
1476
|
+
budget_info = await client.get_budget_info()
|
|
1477
|
+
|
|
1478
|
+
# Show warning if remaining balance is $2.50 or less
|
|
1479
|
+
if budget_info.remaining <= 2.50:
|
|
1480
|
+
warning_message = (
|
|
1481
|
+
f"⚠️ **Low Balance Warning**\n\n"
|
|
1482
|
+
f"Your Shotgun Account has **${budget_info.remaining:.2f}** remaining.\n\n"
|
|
1483
|
+
f"👉 **[Top Up Now at https://app.shotgun.sh/dashboard](https://app.shotgun.sh/dashboard)**"
|
|
1484
|
+
)
|
|
1485
|
+
self.agent_manager.add_hint_message(
|
|
1486
|
+
HintMessage(message=warning_message)
|
|
1487
|
+
)
|
|
1488
|
+
except Exception as e:
|
|
1489
|
+
# Silently log and continue - don't block user workflow
|
|
1490
|
+
logger.debug(f"Failed to check low balance warning: {e}")
|
|
1491
|
+
|
|
1296
1492
|
async def _check_and_load_conversation(self) -> None:
|
|
1297
1493
|
"""Check if conversation exists and load it if it does."""
|
|
1298
1494
|
if await self.conversation_manager.exists():
|