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.

Files changed (132) hide show
  1. shotgun/agents/agent_manager.py +664 -75
  2. shotgun/agents/common.py +76 -70
  3. shotgun/agents/config/constants.py +0 -6
  4. shotgun/agents/config/manager.py +78 -36
  5. shotgun/agents/config/models.py +41 -1
  6. shotgun/agents/config/provider.py +70 -15
  7. shotgun/agents/context_analyzer/__init__.py +28 -0
  8. shotgun/agents/context_analyzer/analyzer.py +471 -0
  9. shotgun/agents/context_analyzer/constants.py +9 -0
  10. shotgun/agents/context_analyzer/formatter.py +115 -0
  11. shotgun/agents/context_analyzer/models.py +212 -0
  12. shotgun/agents/conversation_history.py +125 -2
  13. shotgun/agents/conversation_manager.py +57 -19
  14. shotgun/agents/export.py +6 -7
  15. shotgun/agents/history/compaction.py +9 -4
  16. shotgun/agents/history/context_extraction.py +93 -6
  17. shotgun/agents/history/history_processors.py +14 -2
  18. shotgun/agents/history/token_counting/anthropic.py +49 -11
  19. shotgun/agents/history/token_counting/base.py +14 -3
  20. shotgun/agents/history/token_counting/openai.py +8 -0
  21. shotgun/agents/history/token_counting/sentencepiece_counter.py +8 -0
  22. shotgun/agents/history/token_counting/tokenizer_cache.py +3 -1
  23. shotgun/agents/history/token_counting/utils.py +0 -3
  24. shotgun/agents/models.py +50 -2
  25. shotgun/agents/plan.py +6 -7
  26. shotgun/agents/research.py +7 -8
  27. shotgun/agents/specify.py +6 -7
  28. shotgun/agents/tasks.py +6 -7
  29. shotgun/agents/tools/__init__.py +0 -2
  30. shotgun/agents/tools/codebase/codebase_shell.py +6 -0
  31. shotgun/agents/tools/codebase/directory_lister.py +6 -0
  32. shotgun/agents/tools/codebase/file_read.py +11 -2
  33. shotgun/agents/tools/codebase/query_graph.py +6 -0
  34. shotgun/agents/tools/codebase/retrieve_code.py +6 -0
  35. shotgun/agents/tools/file_management.py +82 -16
  36. shotgun/agents/tools/registry.py +217 -0
  37. shotgun/agents/tools/web_search/__init__.py +30 -18
  38. shotgun/agents/tools/web_search/anthropic.py +26 -5
  39. shotgun/agents/tools/web_search/gemini.py +23 -11
  40. shotgun/agents/tools/web_search/openai.py +22 -13
  41. shotgun/agents/tools/web_search/utils.py +2 -2
  42. shotgun/agents/usage_manager.py +16 -11
  43. shotgun/api_endpoints.py +7 -3
  44. shotgun/build_constants.py +1 -1
  45. shotgun/cli/clear.py +53 -0
  46. shotgun/cli/compact.py +186 -0
  47. shotgun/cli/config.py +8 -5
  48. shotgun/cli/context.py +111 -0
  49. shotgun/cli/export.py +1 -1
  50. shotgun/cli/feedback.py +4 -2
  51. shotgun/cli/models.py +1 -0
  52. shotgun/cli/plan.py +1 -1
  53. shotgun/cli/research.py +1 -1
  54. shotgun/cli/specify.py +1 -1
  55. shotgun/cli/tasks.py +1 -1
  56. shotgun/cli/update.py +16 -2
  57. shotgun/codebase/core/change_detector.py +5 -3
  58. shotgun/codebase/core/code_retrieval.py +4 -2
  59. shotgun/codebase/core/ingestor.py +10 -8
  60. shotgun/codebase/core/manager.py +13 -4
  61. shotgun/codebase/core/nl_query.py +1 -1
  62. shotgun/llm_proxy/__init__.py +5 -2
  63. shotgun/llm_proxy/clients.py +12 -7
  64. shotgun/logging_config.py +18 -27
  65. shotgun/main.py +73 -11
  66. shotgun/posthog_telemetry.py +23 -7
  67. shotgun/prompts/agents/export.j2 +18 -1
  68. shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +5 -1
  69. shotgun/prompts/agents/partials/interactive_mode.j2 +24 -7
  70. shotgun/prompts/agents/plan.j2 +1 -1
  71. shotgun/prompts/agents/research.j2 +1 -1
  72. shotgun/prompts/agents/specify.j2 +270 -3
  73. shotgun/prompts/agents/state/system_state.j2 +4 -0
  74. shotgun/prompts/agents/tasks.j2 +1 -1
  75. shotgun/prompts/loader.py +2 -2
  76. shotgun/prompts/tools/web_search.j2 +14 -0
  77. shotgun/sentry_telemetry.py +7 -16
  78. shotgun/settings.py +238 -0
  79. shotgun/telemetry.py +18 -33
  80. shotgun/tui/app.py +243 -43
  81. shotgun/tui/commands/__init__.py +1 -1
  82. shotgun/tui/components/context_indicator.py +179 -0
  83. shotgun/tui/components/mode_indicator.py +70 -0
  84. shotgun/tui/components/status_bar.py +48 -0
  85. shotgun/tui/containers.py +91 -0
  86. shotgun/tui/dependencies.py +39 -0
  87. shotgun/tui/protocols.py +45 -0
  88. shotgun/tui/screens/chat/__init__.py +5 -0
  89. shotgun/tui/screens/chat/chat.tcss +54 -0
  90. shotgun/tui/screens/chat/chat_screen.py +1202 -0
  91. shotgun/tui/screens/chat/codebase_index_prompt_screen.py +64 -0
  92. shotgun/tui/screens/chat/codebase_index_selection.py +12 -0
  93. shotgun/tui/screens/chat/help_text.py +40 -0
  94. shotgun/tui/screens/chat/prompt_history.py +48 -0
  95. shotgun/tui/screens/chat.tcss +11 -0
  96. shotgun/tui/screens/chat_screen/command_providers.py +78 -2
  97. shotgun/tui/screens/chat_screen/history/__init__.py +22 -0
  98. shotgun/tui/screens/chat_screen/history/agent_response.py +66 -0
  99. shotgun/tui/screens/chat_screen/history/chat_history.py +116 -0
  100. shotgun/tui/screens/chat_screen/history/formatters.py +115 -0
  101. shotgun/tui/screens/chat_screen/history/partial_response.py +43 -0
  102. shotgun/tui/screens/chat_screen/history/user_question.py +42 -0
  103. shotgun/tui/screens/confirmation_dialog.py +151 -0
  104. shotgun/tui/screens/feedback.py +4 -4
  105. shotgun/tui/screens/github_issue.py +102 -0
  106. shotgun/tui/screens/model_picker.py +49 -24
  107. shotgun/tui/screens/onboarding.py +431 -0
  108. shotgun/tui/screens/pipx_migration.py +153 -0
  109. shotgun/tui/screens/provider_config.py +50 -27
  110. shotgun/tui/screens/shotgun_auth.py +2 -2
  111. shotgun/tui/screens/welcome.py +32 -10
  112. shotgun/tui/services/__init__.py +5 -0
  113. shotgun/tui/services/conversation_service.py +184 -0
  114. shotgun/tui/state/__init__.py +7 -0
  115. shotgun/tui/state/processing_state.py +185 -0
  116. shotgun/tui/utils/mode_progress.py +14 -7
  117. shotgun/tui/widgets/__init__.py +5 -0
  118. shotgun/tui/widgets/widget_coordinator.py +262 -0
  119. shotgun/utils/datetime_utils.py +77 -0
  120. shotgun/utils/file_system_utils.py +22 -2
  121. shotgun/utils/marketing.py +110 -0
  122. shotgun/utils/update_checker.py +69 -14
  123. shotgun_sh-0.2.11.dev5.dist-info/METADATA +130 -0
  124. shotgun_sh-0.2.11.dev5.dist-info/RECORD +193 -0
  125. {shotgun_sh-0.2.3.dev2.dist-info → shotgun_sh-0.2.11.dev5.dist-info}/entry_points.txt +1 -0
  126. {shotgun_sh-0.2.3.dev2.dist-info → shotgun_sh-0.2.11.dev5.dist-info}/licenses/LICENSE +1 -1
  127. shotgun/agents/tools/user_interaction.py +0 -37
  128. shotgun/tui/screens/chat.py +0 -804
  129. shotgun/tui/screens/chat_screen/history.py +0 -352
  130. shotgun_sh-0.2.3.dev2.dist-info/METADATA +0 -467
  131. shotgun_sh-0.2.3.dev2.dist-info/RECORD +0 -154
  132. {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.utils.env_utils import is_falsy, is_truthy
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
- # Try to get Logfire configuration from build constants first, fall back to env vars
19
- logfire_enabled = None
20
- logfire_token = None
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 is_truthy(logfire_enabled):
47
- logger.debug("Logfire observability disabled via LOGFIRE_ENABLED")
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("LOGFIRE_TOKEN not set, Logfire observability disabled")
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 perform_auto_update_async
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.feedback import FeedbackScreen
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
- "feedback": FeedbackScreen,
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, no_update_check: bool = False, continue_session: bool = False
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("tui_started", {})
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
- # Show welcome screen if no providers are configured OR if user hasn't seen it yet
64
- config = self.config_manager.load()
65
- if (
66
- not self.config_manager.has_any_provider_key()
67
- or not config.shown_welcome_screen
68
- ):
69
- if isinstance(self.screen, WelcomeScreen):
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
- self.push_screen(
73
- WelcomeScreen(),
74
- callback=lambda _arg: self.refresh_startup_screen(),
75
- )
76
- return
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
- if not self.check_local_shotgun_directory_exists():
79
- if isinstance(self.screen, DirectorySetupScreen):
124
+ if isinstance(self.screen, ChatScreen):
80
125
  return
81
126
 
82
- self.push_screen(
83
- DirectorySetupScreen(),
84
- callback=lambda _arg: self.refresh_startup_screen(),
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
- if isinstance(self.screen, ChatScreen):
89
- return
90
- # Pass continue_session flag to ChatScreen
91
- self.push_screen(ChatScreen(continue_session=self.continue_session))
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
- "Feedback", "Send us feedback or report a bug", self.action_feedback
184
+ "New Issue",
185
+ "Report a bug or request a feature on GitHub",
186
+ self.action_new_issue,
109
187
  )
110
- ] # we don't want any system commands
188
+ ]
111
189
 
112
- def action_feedback(self) -> None:
113
- """Open feedback screen and submit feedback."""
114
- from shotgun.posthog_telemetry import Feedback, submit_feedback_survey
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
- self.push_screen(FeedbackScreen(), callback=handle_feedback)
122
-
123
-
124
- def run(no_update_check: bool = False, continue_session: bool = False) -> None:
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(no_update_check=no_update_check, continue_session=continue_session)
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()