shotgun-sh 0.2.3.dev2__py3-none-any.whl → 0.2.11.dev5__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 +664 -75
- shotgun/agents/common.py +76 -70
- shotgun/agents/config/constants.py +0 -6
- shotgun/agents/config/manager.py +78 -36
- shotgun/agents/config/models.py +41 -1
- shotgun/agents/config/provider.py +70 -15
- shotgun/agents/context_analyzer/__init__.py +28 -0
- shotgun/agents/context_analyzer/analyzer.py +471 -0
- shotgun/agents/context_analyzer/constants.py +9 -0
- shotgun/agents/context_analyzer/formatter.py +115 -0
- shotgun/agents/context_analyzer/models.py +212 -0
- shotgun/agents/conversation_history.py +125 -2
- shotgun/agents/conversation_manager.py +57 -19
- shotgun/agents/export.py +6 -7
- shotgun/agents/history/compaction.py +9 -4
- shotgun/agents/history/context_extraction.py +93 -6
- shotgun/agents/history/history_processors.py +14 -2
- shotgun/agents/history/token_counting/anthropic.py +49 -11
- shotgun/agents/history/token_counting/base.py +14 -3
- shotgun/agents/history/token_counting/openai.py +8 -0
- shotgun/agents/history/token_counting/sentencepiece_counter.py +8 -0
- shotgun/agents/history/token_counting/tokenizer_cache.py +3 -1
- shotgun/agents/history/token_counting/utils.py +0 -3
- shotgun/agents/models.py +50 -2
- shotgun/agents/plan.py +6 -7
- shotgun/agents/research.py +7 -8
- shotgun/agents/specify.py +6 -7
- shotgun/agents/tasks.py +6 -7
- shotgun/agents/tools/__init__.py +0 -2
- shotgun/agents/tools/codebase/codebase_shell.py +6 -0
- shotgun/agents/tools/codebase/directory_lister.py +6 -0
- shotgun/agents/tools/codebase/file_read.py +11 -2
- shotgun/agents/tools/codebase/query_graph.py +6 -0
- shotgun/agents/tools/codebase/retrieve_code.py +6 -0
- shotgun/agents/tools/file_management.py +82 -16
- shotgun/agents/tools/registry.py +217 -0
- shotgun/agents/tools/web_search/__init__.py +30 -18
- shotgun/agents/tools/web_search/anthropic.py +26 -5
- shotgun/agents/tools/web_search/gemini.py +23 -11
- shotgun/agents/tools/web_search/openai.py +22 -13
- shotgun/agents/tools/web_search/utils.py +2 -2
- shotgun/agents/usage_manager.py +16 -11
- shotgun/api_endpoints.py +7 -3
- shotgun/build_constants.py +1 -1
- shotgun/cli/clear.py +53 -0
- shotgun/cli/compact.py +186 -0
- shotgun/cli/config.py +8 -5
- shotgun/cli/context.py +111 -0
- shotgun/cli/export.py +1 -1
- shotgun/cli/feedback.py +4 -2
- shotgun/cli/models.py +1 -0
- shotgun/cli/plan.py +1 -1
- shotgun/cli/research.py +1 -1
- shotgun/cli/specify.py +1 -1
- shotgun/cli/tasks.py +1 -1
- shotgun/cli/update.py +16 -2
- shotgun/codebase/core/change_detector.py +5 -3
- shotgun/codebase/core/code_retrieval.py +4 -2
- shotgun/codebase/core/ingestor.py +10 -8
- shotgun/codebase/core/manager.py +13 -4
- shotgun/codebase/core/nl_query.py +1 -1
- shotgun/llm_proxy/__init__.py +5 -2
- shotgun/llm_proxy/clients.py +12 -7
- shotgun/logging_config.py +18 -27
- shotgun/main.py +73 -11
- shotgun/posthog_telemetry.py +23 -7
- shotgun/prompts/agents/export.j2 +18 -1
- shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +5 -1
- shotgun/prompts/agents/partials/interactive_mode.j2 +24 -7
- shotgun/prompts/agents/plan.j2 +1 -1
- shotgun/prompts/agents/research.j2 +1 -1
- shotgun/prompts/agents/specify.j2 +270 -3
- shotgun/prompts/agents/state/system_state.j2 +4 -0
- shotgun/prompts/agents/tasks.j2 +1 -1
- shotgun/prompts/loader.py +2 -2
- shotgun/prompts/tools/web_search.j2 +14 -0
- shotgun/sentry_telemetry.py +7 -16
- shotgun/settings.py +238 -0
- shotgun/telemetry.py +18 -33
- shotgun/tui/app.py +243 -43
- shotgun/tui/commands/__init__.py +1 -1
- shotgun/tui/components/context_indicator.py +179 -0
- shotgun/tui/components/mode_indicator.py +70 -0
- shotgun/tui/components/status_bar.py +48 -0
- shotgun/tui/containers.py +91 -0
- shotgun/tui/dependencies.py +39 -0
- shotgun/tui/protocols.py +45 -0
- shotgun/tui/screens/chat/__init__.py +5 -0
- shotgun/tui/screens/chat/chat.tcss +54 -0
- shotgun/tui/screens/chat/chat_screen.py +1202 -0
- shotgun/tui/screens/chat/codebase_index_prompt_screen.py +64 -0
- shotgun/tui/screens/chat/codebase_index_selection.py +12 -0
- shotgun/tui/screens/chat/help_text.py +40 -0
- shotgun/tui/screens/chat/prompt_history.py +48 -0
- shotgun/tui/screens/chat.tcss +11 -0
- shotgun/tui/screens/chat_screen/command_providers.py +78 -2
- shotgun/tui/screens/chat_screen/history/__init__.py +22 -0
- shotgun/tui/screens/chat_screen/history/agent_response.py +66 -0
- shotgun/tui/screens/chat_screen/history/chat_history.py +116 -0
- shotgun/tui/screens/chat_screen/history/formatters.py +115 -0
- shotgun/tui/screens/chat_screen/history/partial_response.py +43 -0
- shotgun/tui/screens/chat_screen/history/user_question.py +42 -0
- shotgun/tui/screens/confirmation_dialog.py +151 -0
- shotgun/tui/screens/feedback.py +4 -4
- shotgun/tui/screens/github_issue.py +102 -0
- shotgun/tui/screens/model_picker.py +49 -24
- shotgun/tui/screens/onboarding.py +431 -0
- shotgun/tui/screens/pipx_migration.py +153 -0
- shotgun/tui/screens/provider_config.py +50 -27
- shotgun/tui/screens/shotgun_auth.py +2 -2
- shotgun/tui/screens/welcome.py +32 -10
- shotgun/tui/services/__init__.py +5 -0
- shotgun/tui/services/conversation_service.py +184 -0
- shotgun/tui/state/__init__.py +7 -0
- shotgun/tui/state/processing_state.py +185 -0
- shotgun/tui/utils/mode_progress.py +14 -7
- shotgun/tui/widgets/__init__.py +5 -0
- shotgun/tui/widgets/widget_coordinator.py +262 -0
- shotgun/utils/datetime_utils.py +77 -0
- shotgun/utils/file_system_utils.py +22 -2
- shotgun/utils/marketing.py +110 -0
- shotgun/utils/update_checker.py +69 -14
- shotgun_sh-0.2.11.dev5.dist-info/METADATA +130 -0
- shotgun_sh-0.2.11.dev5.dist-info/RECORD +193 -0
- {shotgun_sh-0.2.3.dev2.dist-info → shotgun_sh-0.2.11.dev5.dist-info}/entry_points.txt +1 -0
- {shotgun_sh-0.2.3.dev2.dist-info → shotgun_sh-0.2.11.dev5.dist-info}/licenses/LICENSE +1 -1
- shotgun/agents/tools/user_interaction.py +0 -37
- shotgun/tui/screens/chat.py +0 -804
- shotgun/tui/screens/chat_screen/history.py +0 -352
- shotgun_sh-0.2.3.dev2.dist-info/METADATA +0 -467
- shotgun_sh-0.2.3.dev2.dist-info/RECORD +0 -154
- {shotgun_sh-0.2.3.dev2.dist-info → shotgun_sh-0.2.11.dev5.dist-info}/WHEEL +0 -0
shotgun/settings.py
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""Centralized application settings using Pydantic Settings.
|
|
2
|
+
|
|
3
|
+
All environment variables use the SHOTGUN_ prefix to avoid conflicts with other tools.
|
|
4
|
+
Settings are loaded with the following priority:
|
|
5
|
+
1. Environment variables (highest priority)
|
|
6
|
+
2. Build constants (embedded at build time)
|
|
7
|
+
3. Default values (lowest priority)
|
|
8
|
+
|
|
9
|
+
Example usage:
|
|
10
|
+
from shotgun.settings import settings
|
|
11
|
+
|
|
12
|
+
# Access telemetry settings
|
|
13
|
+
if settings.telemetry.sentry_dsn:
|
|
14
|
+
sentry_sdk.init(dsn=settings.telemetry.sentry_dsn)
|
|
15
|
+
|
|
16
|
+
# Access logging settings
|
|
17
|
+
logger.setLevel(settings.logging.log_level)
|
|
18
|
+
|
|
19
|
+
# Access API settings
|
|
20
|
+
response = httpx.get(settings.api.web_base_url)
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
from pydantic import Field, field_validator
|
|
26
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _get_build_constant(name: str, default: Any = None) -> Any:
|
|
30
|
+
"""Get a value from build_constants.py, falling back to default.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
name: The constant name to retrieve (e.g., "SENTRY_DSN")
|
|
34
|
+
default: Default value if constant not found
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
The constant value, or default if not found/import fails
|
|
38
|
+
"""
|
|
39
|
+
try:
|
|
40
|
+
from shotgun import build_constants
|
|
41
|
+
|
|
42
|
+
return getattr(build_constants, name, default)
|
|
43
|
+
except ImportError:
|
|
44
|
+
return default
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class TelemetrySettings(BaseSettings):
|
|
48
|
+
"""Telemetry and observability settings.
|
|
49
|
+
|
|
50
|
+
These settings control error tracking (Sentry), analytics (PostHog),
|
|
51
|
+
and observability (Logfire) integrations.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
sentry_dsn: str = Field(
|
|
55
|
+
default_factory=lambda: _get_build_constant("SENTRY_DSN", ""),
|
|
56
|
+
description="Sentry DSN for error tracking",
|
|
57
|
+
)
|
|
58
|
+
posthog_api_key: str = Field(
|
|
59
|
+
default_factory=lambda: _get_build_constant("POSTHOG_API_KEY", ""),
|
|
60
|
+
description="PostHog API key for analytics",
|
|
61
|
+
)
|
|
62
|
+
posthog_project_id: str = Field(
|
|
63
|
+
default_factory=lambda: _get_build_constant("POSTHOG_PROJECT_ID", ""),
|
|
64
|
+
description="PostHog project ID",
|
|
65
|
+
)
|
|
66
|
+
logfire_enabled: bool = Field(
|
|
67
|
+
default_factory=lambda: _get_build_constant("LOGFIRE_ENABLED", False),
|
|
68
|
+
description="Enable Logfire observability (dev builds only)",
|
|
69
|
+
)
|
|
70
|
+
logfire_token: str = Field(
|
|
71
|
+
default_factory=lambda: _get_build_constant("LOGFIRE_TOKEN", ""),
|
|
72
|
+
description="Logfire authentication token",
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
model_config = SettingsConfigDict(
|
|
76
|
+
env_prefix="SHOTGUN_",
|
|
77
|
+
env_file=".env",
|
|
78
|
+
env_file_encoding="utf-8",
|
|
79
|
+
extra="ignore",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
@field_validator("logfire_enabled", mode="before")
|
|
83
|
+
@classmethod
|
|
84
|
+
def parse_bool(cls, v: Any) -> bool:
|
|
85
|
+
"""Parse boolean values from strings (matches is_truthy behavior)."""
|
|
86
|
+
if isinstance(v, bool):
|
|
87
|
+
return v
|
|
88
|
+
if isinstance(v, str):
|
|
89
|
+
return v.lower() in ("true", "1", "yes")
|
|
90
|
+
return bool(v)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class LoggingSettings(BaseSettings):
|
|
94
|
+
"""Logging configuration settings.
|
|
95
|
+
|
|
96
|
+
Controls log level, console output, and file logging behavior.
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
log_level: str = Field(
|
|
100
|
+
default="INFO",
|
|
101
|
+
description="Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)",
|
|
102
|
+
)
|
|
103
|
+
logging_to_console: bool = Field(
|
|
104
|
+
default=False,
|
|
105
|
+
description="Enable console logging output",
|
|
106
|
+
)
|
|
107
|
+
logging_to_file: bool = Field(
|
|
108
|
+
default=True,
|
|
109
|
+
description="Enable file logging output",
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
model_config = SettingsConfigDict(
|
|
113
|
+
env_prefix="SHOTGUN_",
|
|
114
|
+
env_file=".env",
|
|
115
|
+
env_file_encoding="utf-8",
|
|
116
|
+
extra="ignore",
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
@field_validator("log_level")
|
|
120
|
+
@classmethod
|
|
121
|
+
def validate_log_level(cls, v: str) -> str:
|
|
122
|
+
"""Validate log level is one of the allowed values."""
|
|
123
|
+
v = v.upper()
|
|
124
|
+
valid_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
|
|
125
|
+
if v not in valid_levels:
|
|
126
|
+
return "INFO" # Default to INFO if invalid
|
|
127
|
+
return v
|
|
128
|
+
|
|
129
|
+
@field_validator("logging_to_console", "logging_to_file", mode="before")
|
|
130
|
+
@classmethod
|
|
131
|
+
def parse_bool(cls, v: Any) -> bool:
|
|
132
|
+
"""Parse boolean values from strings (matches is_truthy behavior)."""
|
|
133
|
+
if isinstance(v, bool):
|
|
134
|
+
return v
|
|
135
|
+
if isinstance(v, str):
|
|
136
|
+
return v.lower() in ("true", "1", "yes")
|
|
137
|
+
return bool(v)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class ApiSettings(BaseSettings):
|
|
141
|
+
"""API endpoint settings.
|
|
142
|
+
|
|
143
|
+
Configuration for Shotgun backend services.
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
web_base_url: str = Field(
|
|
147
|
+
default="https://api-219702594231.us-east4.run.app",
|
|
148
|
+
description="Shotgun Web API base URL (authentication/subscription)",
|
|
149
|
+
)
|
|
150
|
+
account_llm_base_url: str = Field(
|
|
151
|
+
default="https://litellm-219702594231.us-east4.run.app",
|
|
152
|
+
description="Shotgun's LiteLLM proxy base URL (AI model requests)",
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
model_config = SettingsConfigDict(
|
|
156
|
+
env_prefix="SHOTGUN_",
|
|
157
|
+
env_file=".env",
|
|
158
|
+
env_file_encoding="utf-8",
|
|
159
|
+
extra="ignore",
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class DevelopmentSettings(BaseSettings):
|
|
164
|
+
"""Development and testing settings.
|
|
165
|
+
|
|
166
|
+
These settings are primarily used for testing and development purposes.
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
home: str | None = Field(
|
|
170
|
+
default=None,
|
|
171
|
+
description="Override Shotgun home directory (for testing)",
|
|
172
|
+
)
|
|
173
|
+
pipx_simulate: bool = Field(
|
|
174
|
+
default=False,
|
|
175
|
+
description="Simulate pipx installation (for testing)",
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
model_config = SettingsConfigDict(
|
|
179
|
+
env_prefix="SHOTGUN_",
|
|
180
|
+
env_file=".env",
|
|
181
|
+
env_file_encoding="utf-8",
|
|
182
|
+
extra="ignore",
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
@field_validator("pipx_simulate", mode="before")
|
|
186
|
+
@classmethod
|
|
187
|
+
def parse_bool(cls, v: Any) -> bool:
|
|
188
|
+
"""Parse boolean values from strings (matches is_truthy behavior)."""
|
|
189
|
+
if isinstance(v, bool):
|
|
190
|
+
return v
|
|
191
|
+
if isinstance(v, str):
|
|
192
|
+
return v.lower() in ("true", "1", "yes")
|
|
193
|
+
return bool(v)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class Settings(BaseSettings):
|
|
197
|
+
"""Main application settings with SHOTGUN_ prefix.
|
|
198
|
+
|
|
199
|
+
This is the main settings class that composes all other settings groups.
|
|
200
|
+
Access settings via the global `settings` singleton instance.
|
|
201
|
+
|
|
202
|
+
Example:
|
|
203
|
+
from shotgun.settings import settings
|
|
204
|
+
|
|
205
|
+
# Telemetry settings
|
|
206
|
+
settings.telemetry.sentry_dsn
|
|
207
|
+
settings.telemetry.posthog_api_key
|
|
208
|
+
settings.telemetry.logfire_enabled
|
|
209
|
+
|
|
210
|
+
# Logging settings
|
|
211
|
+
settings.logging.log_level
|
|
212
|
+
settings.logging.logging_to_console
|
|
213
|
+
|
|
214
|
+
# API settings
|
|
215
|
+
settings.api.web_base_url
|
|
216
|
+
settings.api.account_llm_base_url
|
|
217
|
+
|
|
218
|
+
# Development settings
|
|
219
|
+
settings.dev.home
|
|
220
|
+
settings.dev.pipx_simulate
|
|
221
|
+
"""
|
|
222
|
+
|
|
223
|
+
telemetry: TelemetrySettings = Field(default_factory=TelemetrySettings)
|
|
224
|
+
logging: LoggingSettings = Field(default_factory=LoggingSettings)
|
|
225
|
+
api: ApiSettings = Field(default_factory=ApiSettings)
|
|
226
|
+
dev: DevelopmentSettings = Field(default_factory=DevelopmentSettings)
|
|
227
|
+
|
|
228
|
+
model_config = SettingsConfigDict(
|
|
229
|
+
env_prefix="SHOTGUN_",
|
|
230
|
+
env_file=".env",
|
|
231
|
+
env_file_encoding="utf-8",
|
|
232
|
+
extra="ignore",
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
# Global settings singleton
|
|
237
|
+
# Import this in your modules: from shotgun.settings import settings
|
|
238
|
+
settings = Settings()
|
shotgun/telemetry.py
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
"""Observability setup for Logfire."""
|
|
2
2
|
|
|
3
|
-
import os
|
|
4
|
-
|
|
5
3
|
from shotgun.logging_config import get_early_logger
|
|
6
|
-
from shotgun.
|
|
4
|
+
from shotgun.settings import settings
|
|
7
5
|
|
|
8
6
|
# Use early logger to prevent automatic StreamHandler creation
|
|
9
7
|
logger = get_early_logger(__name__)
|
|
@@ -15,36 +13,13 @@ def setup_logfire_observability() -> bool:
|
|
|
15
13
|
Returns:
|
|
16
14
|
True if Logfire was successfully set up, False otherwise
|
|
17
15
|
"""
|
|
18
|
-
#
|
|
19
|
-
logfire_enabled =
|
|
20
|
-
logfire_token =
|
|
21
|
-
|
|
22
|
-
try:
|
|
23
|
-
from shotgun.build_constants import LOGFIRE_ENABLED, LOGFIRE_TOKEN
|
|
24
|
-
|
|
25
|
-
# Use build constants if they're not empty
|
|
26
|
-
if LOGFIRE_ENABLED:
|
|
27
|
-
logfire_enabled = LOGFIRE_ENABLED
|
|
28
|
-
if LOGFIRE_TOKEN:
|
|
29
|
-
logfire_token = LOGFIRE_TOKEN
|
|
30
|
-
except ImportError:
|
|
31
|
-
# No build constants available
|
|
32
|
-
pass
|
|
33
|
-
|
|
34
|
-
# Fall back to environment variables if not set from build constants
|
|
35
|
-
if not logfire_enabled:
|
|
36
|
-
logfire_enabled = os.getenv("LOGFIRE_ENABLED", "false")
|
|
37
|
-
if not logfire_token:
|
|
38
|
-
logfire_token = os.getenv("LOGFIRE_TOKEN")
|
|
39
|
-
|
|
40
|
-
# Allow environment variable to override and disable Logfire
|
|
41
|
-
env_override = os.getenv("LOGFIRE_ENABLED")
|
|
42
|
-
if env_override and is_falsy(env_override):
|
|
43
|
-
logfire_enabled = env_override
|
|
16
|
+
# Get Logfire configuration from settings (handles build constants + env vars)
|
|
17
|
+
logfire_enabled = settings.telemetry.logfire_enabled
|
|
18
|
+
logfire_token = settings.telemetry.logfire_token
|
|
44
19
|
|
|
45
20
|
# Check if Logfire observability is enabled
|
|
46
|
-
if not
|
|
47
|
-
logger.debug("Logfire observability disabled
|
|
21
|
+
if not logfire_enabled:
|
|
22
|
+
logger.debug("Logfire observability disabled")
|
|
48
23
|
return False
|
|
49
24
|
|
|
50
25
|
try:
|
|
@@ -52,7 +27,7 @@ def setup_logfire_observability() -> bool:
|
|
|
52
27
|
|
|
53
28
|
# Check for Logfire token
|
|
54
29
|
if not logfire_token:
|
|
55
|
-
logger.warning("
|
|
30
|
+
logger.warning("Logfire token not set, Logfire observability disabled")
|
|
56
31
|
return False
|
|
57
32
|
|
|
58
33
|
# Configure Logfire
|
|
@@ -65,14 +40,24 @@ def setup_logfire_observability() -> bool:
|
|
|
65
40
|
# Instrument Pydantic AI for better observability
|
|
66
41
|
logfire.instrument_pydantic_ai()
|
|
67
42
|
|
|
43
|
+
# Add LogfireLoggingHandler to root logger so logfire logs also go to file
|
|
44
|
+
import logging
|
|
45
|
+
|
|
46
|
+
root_logger = logging.getLogger()
|
|
47
|
+
logfire_handler = logfire.LogfireLoggingHandler()
|
|
48
|
+
root_logger.addHandler(logfire_handler)
|
|
49
|
+
logger.debug("Added LogfireLoggingHandler to root logger for file integration")
|
|
50
|
+
|
|
68
51
|
# Set user context using baggage for all logs and spans
|
|
69
52
|
try:
|
|
53
|
+
import asyncio
|
|
54
|
+
|
|
70
55
|
from opentelemetry import baggage, context
|
|
71
56
|
|
|
72
57
|
from shotgun.agents.config import get_config_manager
|
|
73
58
|
|
|
74
59
|
config_manager = get_config_manager()
|
|
75
|
-
shotgun_instance_id = config_manager.get_shotgun_instance_id()
|
|
60
|
+
shotgun_instance_id = asyncio.run(config_manager.get_shotgun_instance_id())
|
|
76
61
|
|
|
77
62
|
# Set shotgun_instance_id as baggage in global context - this will be included in all logs/spans
|
|
78
63
|
ctx = baggage.set_baggage("shotgun_instance_id", shotgun_instance_id)
|
shotgun/tui/app.py
CHANGED
|
@@ -5,16 +5,23 @@ from textual.app import App, SystemCommand
|
|
|
5
5
|
from textual.binding import Binding
|
|
6
6
|
from textual.screen import Screen
|
|
7
7
|
|
|
8
|
+
from shotgun.agents.agent_manager import AgentManager
|
|
8
9
|
from shotgun.agents.config import ConfigManager, get_config_manager
|
|
10
|
+
from shotgun.agents.models import AgentType
|
|
9
11
|
from shotgun.logging_config import get_logger
|
|
12
|
+
from shotgun.tui.containers import TUIContainer
|
|
10
13
|
from shotgun.tui.screens.splash import SplashScreen
|
|
11
14
|
from shotgun.utils.file_system_utils import get_shotgun_base_path
|
|
12
|
-
from shotgun.utils.update_checker import
|
|
15
|
+
from shotgun.utils.update_checker import (
|
|
16
|
+
detect_installation_method,
|
|
17
|
+
perform_auto_update_async,
|
|
18
|
+
)
|
|
13
19
|
|
|
14
20
|
from .screens.chat import ChatScreen
|
|
15
21
|
from .screens.directory_setup import DirectorySetupScreen
|
|
16
|
-
from .screens.
|
|
22
|
+
from .screens.github_issue import GitHubIssueScreen
|
|
17
23
|
from .screens.model_picker import ModelPickerScreen
|
|
24
|
+
from .screens.pipx_migration import PipxMigrationScreen
|
|
18
25
|
from .screens.provider_config import ProviderConfigScreen
|
|
19
26
|
from .screens.welcome import WelcomeScreen
|
|
20
27
|
|
|
@@ -22,12 +29,13 @@ logger = get_logger(__name__)
|
|
|
22
29
|
|
|
23
30
|
|
|
24
31
|
class ShotgunApp(App[None]):
|
|
32
|
+
# ChatScreen removed from SCREENS dict since it requires dependency injection
|
|
33
|
+
# and is instantiated manually in refresh_startup_screen()
|
|
25
34
|
SCREENS = {
|
|
26
|
-
"chat": ChatScreen,
|
|
27
35
|
"provider_config": ProviderConfigScreen,
|
|
28
36
|
"model_picker": ModelPickerScreen,
|
|
29
37
|
"directory_setup": DirectorySetupScreen,
|
|
30
|
-
"
|
|
38
|
+
"github_issue": GitHubIssueScreen,
|
|
31
39
|
}
|
|
32
40
|
BINDINGS = [
|
|
33
41
|
Binding("ctrl+c", "quit", "Quit the app"),
|
|
@@ -36,12 +44,19 @@ class ShotgunApp(App[None]):
|
|
|
36
44
|
CSS_PATH = "styles.tcss"
|
|
37
45
|
|
|
38
46
|
def __init__(
|
|
39
|
-
self,
|
|
47
|
+
self,
|
|
48
|
+
no_update_check: bool = False,
|
|
49
|
+
continue_session: bool = False,
|
|
50
|
+
force_reindex: bool = False,
|
|
40
51
|
) -> None:
|
|
41
52
|
super().__init__()
|
|
42
53
|
self.config_manager: ConfigManager = get_config_manager()
|
|
43
54
|
self.no_update_check = no_update_check
|
|
44
55
|
self.continue_session = continue_session
|
|
56
|
+
self.force_reindex = force_reindex
|
|
57
|
+
|
|
58
|
+
# Initialize dependency injection container
|
|
59
|
+
self.container = TUIContainer()
|
|
45
60
|
|
|
46
61
|
# Start async update check and install
|
|
47
62
|
if not no_update_check:
|
|
@@ -52,43 +67,104 @@ class ShotgunApp(App[None]):
|
|
|
52
67
|
# Track TUI startup
|
|
53
68
|
from shotgun.posthog_telemetry import track_event
|
|
54
69
|
|
|
55
|
-
track_event(
|
|
70
|
+
track_event(
|
|
71
|
+
"tui_started",
|
|
72
|
+
{
|
|
73
|
+
"installation_method": detect_installation_method(),
|
|
74
|
+
},
|
|
75
|
+
)
|
|
56
76
|
|
|
57
77
|
self.push_screen(
|
|
58
78
|
SplashScreen(), callback=lambda _arg: self.refresh_startup_screen()
|
|
59
79
|
)
|
|
60
80
|
|
|
61
|
-
def refresh_startup_screen(self) -> None:
|
|
81
|
+
def refresh_startup_screen(self, skip_pipx_check: bool = False) -> None:
|
|
62
82
|
"""Push the appropriate screen based on configured providers."""
|
|
63
|
-
#
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
83
|
+
# Check for pipx installation and show migration modal first
|
|
84
|
+
if not skip_pipx_check:
|
|
85
|
+
installation_method = detect_installation_method()
|
|
86
|
+
if installation_method == "pipx":
|
|
87
|
+
if isinstance(self.screen, PipxMigrationScreen):
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
# Show pipx migration modal as a blocking modal screen
|
|
91
|
+
self.push_screen(
|
|
92
|
+
PipxMigrationScreen(),
|
|
93
|
+
callback=lambda _arg: self.refresh_startup_screen(
|
|
94
|
+
skip_pipx_check=True
|
|
95
|
+
),
|
|
96
|
+
)
|
|
70
97
|
return
|
|
71
98
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
)
|
|
76
|
-
|
|
99
|
+
# Run async config loading in worker
|
|
100
|
+
async def _check_config() -> None:
|
|
101
|
+
# Show welcome screen if no providers are configured OR if user hasn't seen it yet
|
|
102
|
+
config = await self.config_manager.load()
|
|
103
|
+
has_any_key = await self.config_manager.has_any_provider_key()
|
|
104
|
+
if not has_any_key or not config.shown_welcome_screen:
|
|
105
|
+
if isinstance(self.screen, WelcomeScreen):
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
self.push_screen(
|
|
109
|
+
WelcomeScreen(),
|
|
110
|
+
callback=lambda _arg: self.refresh_startup_screen(),
|
|
111
|
+
)
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
if not self.check_local_shotgun_directory_exists():
|
|
115
|
+
if isinstance(self.screen, DirectorySetupScreen):
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
self.push_screen(
|
|
119
|
+
DirectorySetupScreen(),
|
|
120
|
+
callback=lambda _arg: self.refresh_startup_screen(),
|
|
121
|
+
)
|
|
122
|
+
return
|
|
77
123
|
|
|
78
|
-
|
|
79
|
-
if isinstance(self.screen, DirectorySetupScreen):
|
|
124
|
+
if isinstance(self.screen, ChatScreen):
|
|
80
125
|
return
|
|
81
126
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
127
|
+
# Create ChatScreen with all dependencies injected from container
|
|
128
|
+
# Get the default agent mode (RESEARCH)
|
|
129
|
+
agent_mode = AgentType.RESEARCH
|
|
130
|
+
|
|
131
|
+
# Create AgentDeps asynchronously (get_provider_model is now async)
|
|
132
|
+
from shotgun.tui.dependencies import create_default_tui_deps
|
|
133
|
+
|
|
134
|
+
agent_deps = await create_default_tui_deps()
|
|
135
|
+
|
|
136
|
+
# Create AgentManager with async initialization
|
|
137
|
+
agent_manager = AgentManager(deps=agent_deps, initial_type=agent_mode)
|
|
138
|
+
|
|
139
|
+
# Create ProcessingStateManager - we'll pass the screen after creation
|
|
140
|
+
# For now, create with None and the ChatScreen will set itself
|
|
141
|
+
chat_screen = ChatScreen(
|
|
142
|
+
agent_manager=agent_manager,
|
|
143
|
+
conversation_manager=self.container.conversation_manager(),
|
|
144
|
+
conversation_service=self.container.conversation_service(),
|
|
145
|
+
widget_coordinator=self.container.widget_coordinator_factory(
|
|
146
|
+
screen=None
|
|
147
|
+
),
|
|
148
|
+
processing_state=self.container.processing_state_factory(
|
|
149
|
+
screen=None, # Will be set after ChatScreen is created
|
|
150
|
+
telemetry_context={"agent_mode": agent_mode.value},
|
|
151
|
+
),
|
|
152
|
+
command_handler=self.container.command_handler(),
|
|
153
|
+
placeholder_hints=self.container.placeholder_hints(),
|
|
154
|
+
codebase_sdk=self.container.codebase_sdk(),
|
|
155
|
+
deps=agent_deps,
|
|
156
|
+
continue_session=self.continue_session,
|
|
157
|
+
force_reindex=self.force_reindex,
|
|
85
158
|
)
|
|
86
|
-
return
|
|
87
159
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
160
|
+
# Update the ProcessingStateManager and WidgetCoordinator with the actual ChatScreen instance
|
|
161
|
+
chat_screen.processing_state.screen = chat_screen
|
|
162
|
+
chat_screen.widget_coordinator.screen = chat_screen
|
|
163
|
+
|
|
164
|
+
self.push_screen(chat_screen)
|
|
165
|
+
|
|
166
|
+
# Run the async config check in a worker
|
|
167
|
+
self.run_worker(_check_config(), exclusive=False)
|
|
92
168
|
|
|
93
169
|
def check_local_shotgun_directory_exists(self) -> bool:
|
|
94
170
|
shotgun_dir = get_shotgun_base_path()
|
|
@@ -105,28 +181,28 @@ class ShotgunApp(App[None]):
|
|
|
105
181
|
def get_system_commands(self, screen: Screen[Any]) -> Iterable[SystemCommand]:
|
|
106
182
|
return [
|
|
107
183
|
SystemCommand(
|
|
108
|
-
"
|
|
184
|
+
"New Issue",
|
|
185
|
+
"Report a bug or request a feature on GitHub",
|
|
186
|
+
self.action_new_issue,
|
|
109
187
|
)
|
|
110
|
-
]
|
|
188
|
+
]
|
|
111
189
|
|
|
112
|
-
def
|
|
113
|
-
"""Open
|
|
114
|
-
|
|
190
|
+
def action_new_issue(self) -> None:
|
|
191
|
+
"""Open GitHub issue screen to guide users to create an issue."""
|
|
192
|
+
self.push_screen(GitHubIssueScreen())
|
|
115
193
|
|
|
116
|
-
def handle_feedback(feedback: Feedback | None) -> None:
|
|
117
|
-
if feedback is not None:
|
|
118
|
-
submit_feedback_survey(feedback)
|
|
119
|
-
self.notify("Feedback sent. Thank you!")
|
|
120
194
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
195
|
+
def run(
|
|
196
|
+
no_update_check: bool = False,
|
|
197
|
+
continue_session: bool = False,
|
|
198
|
+
force_reindex: bool = False,
|
|
199
|
+
) -> None:
|
|
125
200
|
"""Run the TUI application.
|
|
126
201
|
|
|
127
202
|
Args:
|
|
128
203
|
no_update_check: If True, disable automatic update checks.
|
|
129
204
|
continue_session: If True, continue from previous conversation.
|
|
205
|
+
force_reindex: If True, force re-indexing of codebase (ignores existing index).
|
|
130
206
|
"""
|
|
131
207
|
# Clean up any corrupted databases BEFORE starting the TUI
|
|
132
208
|
# This prevents crashes from corrupted databases during initialization
|
|
@@ -148,9 +224,133 @@ def run(no_update_check: bool = False, continue_session: bool = False) -> None:
|
|
|
148
224
|
logger.error(f"Failed to cleanup corrupted databases: {e}")
|
|
149
225
|
# Continue anyway - the TUI can still function
|
|
150
226
|
|
|
151
|
-
app = ShotgunApp(
|
|
227
|
+
app = ShotgunApp(
|
|
228
|
+
no_update_check=no_update_check,
|
|
229
|
+
continue_session=continue_session,
|
|
230
|
+
force_reindex=force_reindex,
|
|
231
|
+
)
|
|
152
232
|
app.run(inline_no_clear=True)
|
|
153
233
|
|
|
154
234
|
|
|
235
|
+
def serve(
|
|
236
|
+
host: str = "localhost",
|
|
237
|
+
port: int = 8000,
|
|
238
|
+
public_url: str | None = None,
|
|
239
|
+
no_update_check: bool = False,
|
|
240
|
+
continue_session: bool = False,
|
|
241
|
+
force_reindex: bool = False,
|
|
242
|
+
) -> None:
|
|
243
|
+
"""Serve the TUI application as a web application.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
host: Host address for the web server.
|
|
247
|
+
port: Port number for the web server.
|
|
248
|
+
public_url: Public URL if behind a proxy.
|
|
249
|
+
no_update_check: If True, disable automatic update checks.
|
|
250
|
+
continue_session: If True, continue from previous conversation.
|
|
251
|
+
force_reindex: If True, force re-indexing of codebase (ignores existing index).
|
|
252
|
+
"""
|
|
253
|
+
# Clean up any corrupted databases BEFORE starting the TUI
|
|
254
|
+
# This prevents crashes from corrupted databases during initialization
|
|
255
|
+
import asyncio
|
|
256
|
+
|
|
257
|
+
from textual_serve.server import Server
|
|
258
|
+
|
|
259
|
+
from shotgun.codebase.core.manager import CodebaseGraphManager
|
|
260
|
+
from shotgun.utils import get_shotgun_home
|
|
261
|
+
|
|
262
|
+
storage_dir = get_shotgun_home() / "codebases"
|
|
263
|
+
manager = CodebaseGraphManager(storage_dir)
|
|
264
|
+
|
|
265
|
+
try:
|
|
266
|
+
removed = asyncio.run(manager.cleanup_corrupted_databases())
|
|
267
|
+
if removed:
|
|
268
|
+
logger.info(
|
|
269
|
+
f"Cleaned up {len(removed)} corrupted database(s) before TUI startup"
|
|
270
|
+
)
|
|
271
|
+
except Exception as e:
|
|
272
|
+
logger.error(f"Failed to cleanup corrupted databases: {e}")
|
|
273
|
+
# Continue anyway - the TUI can still function
|
|
274
|
+
|
|
275
|
+
# Create a new event loop after asyncio.run() closes the previous one
|
|
276
|
+
# This is needed for the Server.serve() method
|
|
277
|
+
loop = asyncio.new_event_loop()
|
|
278
|
+
asyncio.set_event_loop(loop)
|
|
279
|
+
|
|
280
|
+
# Build the command string based on flags
|
|
281
|
+
command = "shotgun"
|
|
282
|
+
if no_update_check:
|
|
283
|
+
command += " --no-update-check"
|
|
284
|
+
if continue_session:
|
|
285
|
+
command += " --continue"
|
|
286
|
+
if force_reindex:
|
|
287
|
+
command += " --force-reindex"
|
|
288
|
+
|
|
289
|
+
# Create and start the server with hardcoded title and debug=False
|
|
290
|
+
server = Server(
|
|
291
|
+
command=command,
|
|
292
|
+
host=host,
|
|
293
|
+
port=port,
|
|
294
|
+
title="The Shotgun",
|
|
295
|
+
public_url=public_url,
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
# Set up graceful shutdown on SIGTERM/SIGINT
|
|
299
|
+
import signal
|
|
300
|
+
import sys
|
|
301
|
+
|
|
302
|
+
def signal_handler(_signum: int, _frame: Any) -> None:
|
|
303
|
+
"""Handle shutdown signals gracefully."""
|
|
304
|
+
from shotgun.posthog_telemetry import shutdown
|
|
305
|
+
|
|
306
|
+
logger.info("Received shutdown signal, cleaning up...")
|
|
307
|
+
# Restore stdout/stderr before shutting down
|
|
308
|
+
sys.stdout = original_stdout
|
|
309
|
+
sys.stderr = original_stderr
|
|
310
|
+
shutdown()
|
|
311
|
+
sys.exit(0)
|
|
312
|
+
|
|
313
|
+
signal.signal(signal.SIGTERM, signal_handler)
|
|
314
|
+
signal.signal(signal.SIGINT, signal_handler)
|
|
315
|
+
|
|
316
|
+
# Suppress the textual-serve banner by redirecting stdout/stderr
|
|
317
|
+
import io
|
|
318
|
+
|
|
319
|
+
# Capture and suppress the banner, but show the actual serving URL
|
|
320
|
+
original_stdout = sys.stdout
|
|
321
|
+
original_stderr = sys.stderr
|
|
322
|
+
|
|
323
|
+
captured_output = io.StringIO()
|
|
324
|
+
sys.stdout = captured_output
|
|
325
|
+
sys.stderr = captured_output
|
|
326
|
+
|
|
327
|
+
try:
|
|
328
|
+
# This will print the banner to our captured output
|
|
329
|
+
import logging
|
|
330
|
+
|
|
331
|
+
# Temporarily set logging to ERROR level to suppress INFO messages
|
|
332
|
+
textual_serve_logger = logging.getLogger("textual_serve")
|
|
333
|
+
original_level = textual_serve_logger.level
|
|
334
|
+
textual_serve_logger.setLevel(logging.ERROR)
|
|
335
|
+
|
|
336
|
+
# Print our own message to the original stdout
|
|
337
|
+
sys.stdout = original_stdout
|
|
338
|
+
sys.stderr = original_stderr
|
|
339
|
+
print(f"Serving Shotgun TUI at http://{host}:{port}")
|
|
340
|
+
print("Press Ctrl+C to quit")
|
|
341
|
+
|
|
342
|
+
# Now suppress output again for the serve call
|
|
343
|
+
sys.stdout = captured_output
|
|
344
|
+
sys.stderr = captured_output
|
|
345
|
+
|
|
346
|
+
server.serve(debug=False)
|
|
347
|
+
finally:
|
|
348
|
+
# Restore original stdout/stderr
|
|
349
|
+
sys.stdout = original_stdout
|
|
350
|
+
sys.stderr = original_stderr
|
|
351
|
+
if "textual_serve_logger" in locals():
|
|
352
|
+
textual_serve_logger.setLevel(original_level)
|
|
353
|
+
|
|
354
|
+
|
|
155
355
|
if __name__ == "__main__":
|
|
156
356
|
run()
|