shotgun-sh 0.2.8.dev2__py3-none-any.whl → 0.3.3.dev1__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.
Files changed (175) hide show
  1. shotgun/agents/agent_manager.py +382 -60
  2. shotgun/agents/common.py +15 -9
  3. shotgun/agents/config/README.md +89 -0
  4. shotgun/agents/config/__init__.py +10 -1
  5. shotgun/agents/config/constants.py +0 -6
  6. shotgun/agents/config/manager.py +383 -82
  7. shotgun/agents/config/models.py +122 -18
  8. shotgun/agents/config/provider.py +81 -15
  9. shotgun/agents/config/streaming_test.py +119 -0
  10. shotgun/agents/context_analyzer/__init__.py +28 -0
  11. shotgun/agents/context_analyzer/analyzer.py +475 -0
  12. shotgun/agents/context_analyzer/constants.py +9 -0
  13. shotgun/agents/context_analyzer/formatter.py +115 -0
  14. shotgun/agents/context_analyzer/models.py +212 -0
  15. shotgun/agents/conversation/__init__.py +18 -0
  16. shotgun/agents/conversation/filters.py +164 -0
  17. shotgun/agents/conversation/history/chunking.py +278 -0
  18. shotgun/agents/{history → conversation/history}/compaction.py +36 -5
  19. shotgun/agents/{history → conversation/history}/constants.py +5 -0
  20. shotgun/agents/conversation/history/file_content_deduplication.py +216 -0
  21. shotgun/agents/{history → conversation/history}/history_processors.py +380 -8
  22. shotgun/agents/{history → conversation/history}/token_counting/anthropic.py +25 -1
  23. shotgun/agents/{history → conversation/history}/token_counting/base.py +14 -3
  24. shotgun/agents/{history → conversation/history}/token_counting/openai.py +11 -1
  25. shotgun/agents/{history → conversation/history}/token_counting/sentencepiece_counter.py +8 -0
  26. shotgun/agents/{history → conversation/history}/token_counting/tokenizer_cache.py +3 -1
  27. shotgun/agents/{history → conversation/history}/token_counting/utils.py +0 -3
  28. shotgun/agents/{conversation_manager.py → conversation/manager.py} +36 -20
  29. shotgun/agents/{conversation_history.py → conversation/models.py} +8 -92
  30. shotgun/agents/error/__init__.py +11 -0
  31. shotgun/agents/error/models.py +19 -0
  32. shotgun/agents/export.py +2 -2
  33. shotgun/agents/plan.py +2 -2
  34. shotgun/agents/research.py +3 -3
  35. shotgun/agents/runner.py +230 -0
  36. shotgun/agents/specify.py +2 -2
  37. shotgun/agents/tasks.py +2 -2
  38. shotgun/agents/tools/codebase/codebase_shell.py +6 -0
  39. shotgun/agents/tools/codebase/directory_lister.py +6 -0
  40. shotgun/agents/tools/codebase/file_read.py +11 -2
  41. shotgun/agents/tools/codebase/query_graph.py +6 -0
  42. shotgun/agents/tools/codebase/retrieve_code.py +6 -0
  43. shotgun/agents/tools/file_management.py +27 -7
  44. shotgun/agents/tools/registry.py +217 -0
  45. shotgun/agents/tools/web_search/__init__.py +8 -8
  46. shotgun/agents/tools/web_search/anthropic.py +8 -2
  47. shotgun/agents/tools/web_search/gemini.py +7 -1
  48. shotgun/agents/tools/web_search/openai.py +8 -2
  49. shotgun/agents/tools/web_search/utils.py +2 -2
  50. shotgun/agents/usage_manager.py +16 -11
  51. shotgun/api_endpoints.py +7 -3
  52. shotgun/build_constants.py +2 -2
  53. shotgun/cli/clear.py +53 -0
  54. shotgun/cli/compact.py +188 -0
  55. shotgun/cli/config.py +8 -5
  56. shotgun/cli/context.py +154 -0
  57. shotgun/cli/error_handler.py +24 -0
  58. shotgun/cli/export.py +34 -34
  59. shotgun/cli/feedback.py +4 -2
  60. shotgun/cli/models.py +1 -0
  61. shotgun/cli/plan.py +34 -34
  62. shotgun/cli/research.py +18 -10
  63. shotgun/cli/spec/__init__.py +5 -0
  64. shotgun/cli/spec/backup.py +81 -0
  65. shotgun/cli/spec/commands.py +132 -0
  66. shotgun/cli/spec/models.py +48 -0
  67. shotgun/cli/spec/pull_service.py +219 -0
  68. shotgun/cli/specify.py +20 -19
  69. shotgun/cli/tasks.py +34 -34
  70. shotgun/cli/update.py +16 -2
  71. shotgun/codebase/core/change_detector.py +5 -3
  72. shotgun/codebase/core/code_retrieval.py +4 -2
  73. shotgun/codebase/core/ingestor.py +163 -15
  74. shotgun/codebase/core/manager.py +13 -4
  75. shotgun/codebase/core/nl_query.py +1 -1
  76. shotgun/codebase/models.py +2 -0
  77. shotgun/exceptions.py +357 -0
  78. shotgun/llm_proxy/__init__.py +17 -0
  79. shotgun/llm_proxy/client.py +215 -0
  80. shotgun/llm_proxy/models.py +137 -0
  81. shotgun/logging_config.py +60 -27
  82. shotgun/main.py +77 -11
  83. shotgun/posthog_telemetry.py +38 -29
  84. shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +28 -2
  85. shotgun/prompts/agents/partials/interactive_mode.j2 +3 -3
  86. shotgun/prompts/agents/plan.j2 +16 -0
  87. shotgun/prompts/agents/research.j2 +16 -3
  88. shotgun/prompts/agents/specify.j2 +54 -1
  89. shotgun/prompts/agents/state/system_state.j2 +0 -2
  90. shotgun/prompts/agents/tasks.j2 +16 -0
  91. shotgun/prompts/history/chunk_summarization.j2 +34 -0
  92. shotgun/prompts/history/combine_summaries.j2 +53 -0
  93. shotgun/sdk/codebase.py +14 -3
  94. shotgun/sentry_telemetry.py +163 -16
  95. shotgun/settings.py +243 -0
  96. shotgun/shotgun_web/__init__.py +67 -1
  97. shotgun/shotgun_web/client.py +42 -1
  98. shotgun/shotgun_web/constants.py +46 -0
  99. shotgun/shotgun_web/exceptions.py +29 -0
  100. shotgun/shotgun_web/models.py +390 -0
  101. shotgun/shotgun_web/shared_specs/__init__.py +32 -0
  102. shotgun/shotgun_web/shared_specs/file_scanner.py +175 -0
  103. shotgun/shotgun_web/shared_specs/hasher.py +83 -0
  104. shotgun/shotgun_web/shared_specs/models.py +71 -0
  105. shotgun/shotgun_web/shared_specs/upload_pipeline.py +329 -0
  106. shotgun/shotgun_web/shared_specs/utils.py +34 -0
  107. shotgun/shotgun_web/specs_client.py +703 -0
  108. shotgun/shotgun_web/supabase_client.py +31 -0
  109. shotgun/telemetry.py +10 -33
  110. shotgun/tui/app.py +310 -46
  111. shotgun/tui/commands/__init__.py +1 -1
  112. shotgun/tui/components/context_indicator.py +179 -0
  113. shotgun/tui/components/mode_indicator.py +70 -0
  114. shotgun/tui/components/status_bar.py +48 -0
  115. shotgun/tui/containers.py +91 -0
  116. shotgun/tui/dependencies.py +39 -0
  117. shotgun/tui/layout.py +5 -0
  118. shotgun/tui/protocols.py +45 -0
  119. shotgun/tui/screens/chat/__init__.py +5 -0
  120. shotgun/tui/screens/chat/chat.tcss +54 -0
  121. shotgun/tui/screens/chat/chat_screen.py +1531 -0
  122. shotgun/tui/screens/chat/codebase_index_prompt_screen.py +243 -0
  123. shotgun/tui/screens/chat/codebase_index_selection.py +12 -0
  124. shotgun/tui/screens/chat/help_text.py +40 -0
  125. shotgun/tui/screens/chat/prompt_history.py +48 -0
  126. shotgun/tui/screens/chat.tcss +11 -0
  127. shotgun/tui/screens/chat_screen/command_providers.py +91 -4
  128. shotgun/tui/screens/chat_screen/hint_message.py +76 -1
  129. shotgun/tui/screens/chat_screen/history/__init__.py +22 -0
  130. shotgun/tui/screens/chat_screen/history/agent_response.py +66 -0
  131. shotgun/tui/screens/chat_screen/history/chat_history.py +115 -0
  132. shotgun/tui/screens/chat_screen/history/formatters.py +115 -0
  133. shotgun/tui/screens/chat_screen/history/partial_response.py +43 -0
  134. shotgun/tui/screens/chat_screen/history/user_question.py +42 -0
  135. shotgun/tui/screens/confirmation_dialog.py +191 -0
  136. shotgun/tui/screens/directory_setup.py +45 -41
  137. shotgun/tui/screens/feedback.py +14 -7
  138. shotgun/tui/screens/github_issue.py +111 -0
  139. shotgun/tui/screens/model_picker.py +77 -32
  140. shotgun/tui/screens/onboarding.py +580 -0
  141. shotgun/tui/screens/pipx_migration.py +205 -0
  142. shotgun/tui/screens/provider_config.py +116 -35
  143. shotgun/tui/screens/shared_specs/__init__.py +21 -0
  144. shotgun/tui/screens/shared_specs/create_spec_dialog.py +273 -0
  145. shotgun/tui/screens/shared_specs/models.py +56 -0
  146. shotgun/tui/screens/shared_specs/share_specs_dialog.py +390 -0
  147. shotgun/tui/screens/shared_specs/upload_progress_screen.py +452 -0
  148. shotgun/tui/screens/shotgun_auth.py +112 -18
  149. shotgun/tui/screens/spec_pull.py +288 -0
  150. shotgun/tui/screens/welcome.py +137 -11
  151. shotgun/tui/services/__init__.py +5 -0
  152. shotgun/tui/services/conversation_service.py +187 -0
  153. shotgun/tui/state/__init__.py +7 -0
  154. shotgun/tui/state/processing_state.py +185 -0
  155. shotgun/tui/utils/mode_progress.py +14 -7
  156. shotgun/tui/widgets/__init__.py +5 -0
  157. shotgun/tui/widgets/widget_coordinator.py +263 -0
  158. shotgun/utils/file_system_utils.py +22 -2
  159. shotgun/utils/marketing.py +110 -0
  160. shotgun/utils/update_checker.py +69 -14
  161. shotgun_sh-0.3.3.dev1.dist-info/METADATA +472 -0
  162. shotgun_sh-0.3.3.dev1.dist-info/RECORD +229 -0
  163. {shotgun_sh-0.2.8.dev2.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/WHEEL +1 -1
  164. {shotgun_sh-0.2.8.dev2.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/entry_points.txt +1 -0
  165. {shotgun_sh-0.2.8.dev2.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/licenses/LICENSE +1 -1
  166. shotgun/tui/screens/chat.py +0 -996
  167. shotgun/tui/screens/chat_screen/history.py +0 -335
  168. shotgun_sh-0.2.8.dev2.dist-info/METADATA +0 -126
  169. shotgun_sh-0.2.8.dev2.dist-info/RECORD +0 -155
  170. /shotgun/agents/{history → conversation/history}/__init__.py +0 -0
  171. /shotgun/agents/{history → conversation/history}/context_extraction.py +0 -0
  172. /shotgun/agents/{history → conversation/history}/history_building.py +0 -0
  173. /shotgun/agents/{history → conversation/history}/message_utils.py +0 -0
  174. /shotgun/agents/{history → conversation/history}/token_counting/__init__.py +0 -0
  175. /shotgun/agents/{history → conversation/history}/token_estimation.py +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/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
@@ -75,12 +50,14 @@ def setup_logfire_observability() -> bool:
75
50
 
76
51
  # Set user context using baggage for all logs and spans
77
52
  try:
53
+ import asyncio
54
+
78
55
  from opentelemetry import baggage, context
79
56
 
80
57
  from shotgun.agents.config import get_config_manager
81
58
 
82
59
  config_manager = get_config_manager()
83
- shotgun_instance_id = config_manager.get_shotgun_instance_id()
60
+ shotgun_instance_id = asyncio.run(config_manager.get_shotgun_instance_id())
84
61
 
85
62
  # Set shotgun_instance_id as baggage in global context - this will be included in all logs/spans
86
63
  ctx = baggage.set_baggage("shotgun_instance_id", shotgun_instance_id)
shotgun/tui/app.py CHANGED
@@ -5,16 +5,29 @@ 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.config import ConfigManager, get_config_manager
8
+ from shotgun.agents.agent_manager import AgentManager
9
+ from shotgun.agents.config import (
10
+ ConfigManager,
11
+ get_config_manager,
12
+ )
13
+ from shotgun.agents.models import AgentType
9
14
  from shotgun.logging_config import get_logger
15
+ from shotgun.tui.containers import TUIContainer
10
16
  from shotgun.tui.screens.splash import SplashScreen
11
- from shotgun.utils.file_system_utils import get_shotgun_base_path
12
- from shotgun.utils.update_checker import perform_auto_update_async
17
+ from shotgun.utils.file_system_utils import (
18
+ ensure_shotgun_directory_exists,
19
+ get_shotgun_base_path,
20
+ )
21
+ from shotgun.utils.update_checker import (
22
+ detect_installation_method,
23
+ perform_auto_update_async,
24
+ )
13
25
 
14
26
  from .screens.chat import ChatScreen
15
27
  from .screens.directory_setup import DirectorySetupScreen
16
- from .screens.feedback import FeedbackScreen
28
+ from .screens.github_issue import GitHubIssueScreen
17
29
  from .screens.model_picker import ModelPickerScreen
30
+ from .screens.pipx_migration import PipxMigrationScreen
18
31
  from .screens.provider_config import ProviderConfigScreen
19
32
  from .screens.welcome import WelcomeScreen
20
33
 
@@ -22,12 +35,13 @@ logger = get_logger(__name__)
22
35
 
23
36
 
24
37
  class ShotgunApp(App[None]):
38
+ # ChatScreen removed from SCREENS dict since it requires dependency injection
39
+ # and is instantiated manually in refresh_startup_screen()
40
+ # DirectorySetupScreen also removed since it requires error_message parameter
25
41
  SCREENS = {
26
- "chat": ChatScreen,
27
42
  "provider_config": ProviderConfigScreen,
28
43
  "model_picker": ModelPickerScreen,
29
- "directory_setup": DirectorySetupScreen,
30
- "feedback": FeedbackScreen,
44
+ "github_issue": GitHubIssueScreen,
31
45
  }
32
46
  BINDINGS = [
33
47
  Binding("ctrl+c", "quit", "Quit the app"),
@@ -36,12 +50,23 @@ class ShotgunApp(App[None]):
36
50
  CSS_PATH = "styles.tcss"
37
51
 
38
52
  def __init__(
39
- self, no_update_check: bool = False, continue_session: bool = False
53
+ self,
54
+ no_update_check: bool = False,
55
+ continue_session: bool = False,
56
+ force_reindex: bool = False,
57
+ show_pull_hint: bool = False,
58
+ pull_version_id: str | None = None,
40
59
  ) -> None:
41
60
  super().__init__()
42
61
  self.config_manager: ConfigManager = get_config_manager()
43
62
  self.no_update_check = no_update_check
44
63
  self.continue_session = continue_session
64
+ self.force_reindex = force_reindex
65
+ self.show_pull_hint = show_pull_hint
66
+ self.pull_version_id = pull_version_id
67
+
68
+ # Initialize dependency injection container
69
+ self.container = TUIContainer()
45
70
 
46
71
  # Start async update check and install
47
72
  if not no_update_check:
@@ -52,48 +77,157 @@ class ShotgunApp(App[None]):
52
77
  # Track TUI startup
53
78
  from shotgun.posthog_telemetry import track_event
54
79
 
55
- track_event("tui_started", {})
80
+ track_event(
81
+ "tui_started",
82
+ {
83
+ "installation_method": detect_installation_method(),
84
+ "terminal_width": self.size.width,
85
+ "terminal_height": self.size.height,
86
+ },
87
+ )
56
88
 
57
89
  self.push_screen(
58
90
  SplashScreen(), callback=lambda _arg: self.refresh_startup_screen()
59
91
  )
60
92
 
61
- def refresh_startup_screen(self) -> None:
93
+ def refresh_startup_screen(self, skip_pipx_check: bool = False) -> None:
62
94
  """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):
95
+ # Check for pipx installation and show migration modal first
96
+ if not skip_pipx_check:
97
+ installation_method = detect_installation_method()
98
+ if installation_method == "pipx":
99
+ if isinstance(self.screen, PipxMigrationScreen):
100
+ return
101
+
102
+ # Show pipx migration modal as a blocking modal screen
103
+ self.push_screen(
104
+ PipxMigrationScreen(),
105
+ callback=lambda _arg: self.refresh_startup_screen(
106
+ skip_pipx_check=True
107
+ ),
108
+ )
70
109
  return
71
110
 
72
- self.push_screen(
73
- WelcomeScreen(),
74
- callback=lambda _arg: self.refresh_startup_screen(),
75
- )
76
- return
111
+ # Run async config loading in worker
112
+ async def _check_config() -> None:
113
+ # Show welcome screen if no providers are configured OR if user hasn't seen it yet
114
+ # Note: If config migration fails, ConfigManager will auto-create fresh config
115
+ # and set migration_failed flag, which WelcomeScreen will display
116
+ config = await self.config_manager.load()
117
+
118
+ has_any_key = await self.config_manager.has_any_provider_key()
119
+ if not has_any_key or not config.shown_welcome_screen:
120
+ if isinstance(self.screen, WelcomeScreen):
121
+ return
122
+
123
+ self.push_screen(
124
+ WelcomeScreen(),
125
+ callback=lambda _arg: self.refresh_startup_screen(),
126
+ )
127
+ return
128
+
129
+ # Try to create .shotgun directory if it doesn't exist
130
+ if not self.check_local_shotgun_directory_exists():
131
+ try:
132
+ path = ensure_shotgun_directory_exists()
133
+ # Verify directory was created successfully
134
+ if not path.is_dir():
135
+ # Show error screen if creation failed
136
+ if isinstance(self.screen, DirectorySetupScreen):
137
+ return
138
+ self.push_screen(
139
+ DirectorySetupScreen(
140
+ error_message="Unable to create .shotgun directory due to filesystem conflict."
141
+ ),
142
+ callback=lambda _arg: self.refresh_startup_screen(),
143
+ )
144
+ return
145
+ except Exception as exc:
146
+ # Show error screen if creation failed with exception
147
+ if isinstance(self.screen, DirectorySetupScreen):
148
+ return
149
+ self.push_screen(
150
+ DirectorySetupScreen(error_message=str(exc)),
151
+ callback=lambda _arg: self.refresh_startup_screen(),
152
+ )
153
+ return
154
+
155
+ if isinstance(self.screen, ChatScreen):
156
+ return
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
77
161
 
78
- if not self.check_local_shotgun_directory_exists():
79
- if isinstance(self.screen, DirectorySetupScreen):
162
+ self.push_screen(
163
+ SpecPullScreen(self.pull_version_id),
164
+ callback=self._handle_pull_complete,
165
+ )
80
166
  return
81
167
 
82
- self.push_screen(
83
- DirectorySetupScreen(),
84
- callback=lambda _arg: self.refresh_startup_screen(),
168
+ # Create ChatScreen with all dependencies injected from container
169
+ # Get the default agent mode (RESEARCH)
170
+ agent_mode = AgentType.RESEARCH
171
+
172
+ # Create AgentDeps asynchronously (get_provider_model is now async)
173
+ from shotgun.tui.dependencies import create_default_tui_deps
174
+
175
+ agent_deps = await create_default_tui_deps()
176
+
177
+ # Create AgentManager with async initialization
178
+ agent_manager = AgentManager(deps=agent_deps, initial_type=agent_mode)
179
+
180
+ # Create ProcessingStateManager - we'll pass the screen after creation
181
+ # For now, create with None and the ChatScreen will set itself
182
+ chat_screen = ChatScreen(
183
+ agent_manager=agent_manager,
184
+ conversation_manager=self.container.conversation_manager(),
185
+ conversation_service=self.container.conversation_service(),
186
+ widget_coordinator=self.container.widget_coordinator_factory(
187
+ screen=None
188
+ ),
189
+ processing_state=self.container.processing_state_factory(
190
+ screen=None, # Will be set after ChatScreen is created
191
+ telemetry_context={"agent_mode": agent_mode.value},
192
+ ),
193
+ command_handler=self.container.command_handler(),
194
+ placeholder_hints=self.container.placeholder_hints(),
195
+ codebase_sdk=self.container.codebase_sdk(),
196
+ deps=agent_deps,
197
+ continue_session=self.continue_session,
198
+ force_reindex=self.force_reindex,
199
+ show_pull_hint=self.show_pull_hint,
85
200
  )
86
- return
87
201
 
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))
202
+ # Update the ProcessingStateManager and WidgetCoordinator with the actual ChatScreen instance
203
+ chat_screen.processing_state.screen = chat_screen
204
+ chat_screen.widget_coordinator.screen = chat_screen
205
+
206
+ self.push_screen(chat_screen)
207
+
208
+ # Run the async config check in a worker
209
+ self.run_worker(_check_config(), exclusive=False)
92
210
 
93
211
  def check_local_shotgun_directory_exists(self) -> bool:
94
212
  shotgun_dir = get_shotgun_base_path()
95
213
  return shotgun_dir.exists() and shotgun_dir.is_dir()
96
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
+
97
231
  async def action_quit(self) -> None:
98
232
  """Quit the application."""
99
233
  # Shut down PostHog client to prevent threading errors
@@ -105,28 +239,32 @@ class ShotgunApp(App[None]):
105
239
  def get_system_commands(self, screen: Screen[Any]) -> Iterable[SystemCommand]:
106
240
  return [
107
241
  SystemCommand(
108
- "Feedback", "Send us feedback or report a bug", self.action_feedback
242
+ "New Issue",
243
+ "Report a bug or request a feature on GitHub",
244
+ self.action_new_issue,
109
245
  )
110
- ] # we don't want any system commands
246
+ ]
111
247
 
112
- def action_feedback(self) -> None:
113
- """Open feedback screen and submit feedback."""
114
- from shotgun.posthog_telemetry import Feedback, submit_feedback_survey
248
+ def action_new_issue(self) -> None:
249
+ """Open GitHub issue screen to guide users to create an issue."""
250
+ self.push_screen(GitHubIssueScreen())
115
251
 
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
252
 
121
- self.push_screen(FeedbackScreen(), callback=handle_feedback)
122
-
123
-
124
- def run(no_update_check: bool = False, continue_session: bool = False) -> None:
253
+ def run(
254
+ no_update_check: bool = False,
255
+ continue_session: bool = False,
256
+ force_reindex: bool = False,
257
+ show_pull_hint: bool = False,
258
+ pull_version_id: str | None = None,
259
+ ) -> None:
125
260
  """Run the TUI application.
126
261
 
127
262
  Args:
128
263
  no_update_check: If True, disable automatic update checks.
129
264
  continue_session: If True, continue from previous conversation.
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.
130
268
  """
131
269
  # Clean up any corrupted databases BEFORE starting the TUI
132
270
  # This prevents crashes from corrupted databases during initialization
@@ -148,9 +286,135 @@ def run(no_update_check: bool = False, continue_session: bool = False) -> None:
148
286
  logger.error(f"Failed to cleanup corrupted databases: {e}")
149
287
  # Continue anyway - the TUI can still function
150
288
 
151
- app = ShotgunApp(no_update_check=no_update_check, continue_session=continue_session)
289
+ app = ShotgunApp(
290
+ no_update_check=no_update_check,
291
+ continue_session=continue_session,
292
+ force_reindex=force_reindex,
293
+ show_pull_hint=show_pull_hint,
294
+ pull_version_id=pull_version_id,
295
+ )
152
296
  app.run(inline_no_clear=True)
153
297
 
154
298
 
299
+ def serve(
300
+ host: str = "localhost",
301
+ port: int = 8000,
302
+ public_url: str | None = None,
303
+ no_update_check: bool = False,
304
+ continue_session: bool = False,
305
+ force_reindex: bool = False,
306
+ ) -> None:
307
+ """Serve the TUI application as a web application.
308
+
309
+ Args:
310
+ host: Host address for the web server.
311
+ port: Port number for the web server.
312
+ public_url: Public URL if behind a proxy.
313
+ no_update_check: If True, disable automatic update checks.
314
+ continue_session: If True, continue from previous conversation.
315
+ force_reindex: If True, force re-indexing of codebase (ignores existing index).
316
+ """
317
+ # Clean up any corrupted databases BEFORE starting the TUI
318
+ # This prevents crashes from corrupted databases during initialization
319
+ import asyncio
320
+
321
+ from textual_serve.server import Server
322
+
323
+ from shotgun.codebase.core.manager import CodebaseGraphManager
324
+ from shotgun.utils import get_shotgun_home
325
+
326
+ storage_dir = get_shotgun_home() / "codebases"
327
+ manager = CodebaseGraphManager(storage_dir)
328
+
329
+ try:
330
+ removed = asyncio.run(manager.cleanup_corrupted_databases())
331
+ if removed:
332
+ logger.info(
333
+ f"Cleaned up {len(removed)} corrupted database(s) before TUI startup"
334
+ )
335
+ except Exception as e:
336
+ logger.error(f"Failed to cleanup corrupted databases: {e}")
337
+ # Continue anyway - the TUI can still function
338
+
339
+ # Create a new event loop after asyncio.run() closes the previous one
340
+ # This is needed for the Server.serve() method
341
+ loop = asyncio.new_event_loop()
342
+ asyncio.set_event_loop(loop)
343
+
344
+ # Build the command string based on flags
345
+ command = "shotgun"
346
+ if no_update_check:
347
+ command += " --no-update-check"
348
+ if continue_session:
349
+ command += " --continue"
350
+ if force_reindex:
351
+ command += " --force-reindex"
352
+
353
+ # Create and start the server with hardcoded title and debug=False
354
+ server = Server(
355
+ command=command,
356
+ host=host,
357
+ port=port,
358
+ title="The Shotgun",
359
+ public_url=public_url,
360
+ )
361
+
362
+ # Set up graceful shutdown on SIGTERM/SIGINT
363
+ import signal
364
+ import sys
365
+
366
+ def signal_handler(_signum: int, _frame: Any) -> None:
367
+ """Handle shutdown signals gracefully."""
368
+ from shotgun.posthog_telemetry import shutdown
369
+
370
+ logger.info("Received shutdown signal, cleaning up...")
371
+ # Restore stdout/stderr before shutting down
372
+ sys.stdout = original_stdout
373
+ sys.stderr = original_stderr
374
+ shutdown()
375
+ sys.exit(0)
376
+
377
+ signal.signal(signal.SIGTERM, signal_handler)
378
+ signal.signal(signal.SIGINT, signal_handler)
379
+
380
+ # Suppress the textual-serve banner by redirecting stdout/stderr
381
+ import io
382
+
383
+ # Capture and suppress the banner, but show the actual serving URL
384
+ original_stdout = sys.stdout
385
+ original_stderr = sys.stderr
386
+
387
+ captured_output = io.StringIO()
388
+ sys.stdout = captured_output
389
+ sys.stderr = captured_output
390
+
391
+ try:
392
+ # This will print the banner to our captured output
393
+ import logging
394
+
395
+ # Temporarily set logging to ERROR level to suppress INFO messages
396
+ textual_serve_logger = logging.getLogger("textual_serve")
397
+ original_level = textual_serve_logger.level
398
+ textual_serve_logger.setLevel(logging.ERROR)
399
+
400
+ # Print our own message to the original stdout
401
+ sys.stdout = original_stdout
402
+ sys.stderr = original_stderr
403
+ print(f"Serving Shotgun TUI at http://{host}:{port}")
404
+ print("Press Ctrl+C to quit")
405
+
406
+ # Now suppress output again for the serve call
407
+ sys.stdout = captured_output
408
+ sys.stderr = captured_output
409
+
410
+ server.serve(debug=False)
411
+ finally:
412
+ # Restore original stdout/stderr
413
+ sys.stdout = original_stdout
414
+ sys.stderr = original_stderr
415
+ if "textual_serve_logger" in locals():
416
+ textual_serve_logger.setLevel(original_level)
417
+
418
+
155
419
  if __name__ == "__main__":
156
420
  run()
@@ -57,7 +57,7 @@ class CommandHandler:
57
57
  **Keyboard Shortcuts:**
58
58
 
59
59
  * `Enter` - Send message
60
- * `Ctrl+P` - Open command palette
60
+ * `Ctrl+P` - Open command palette (for usage, context, and other commands)
61
61
  * `Shift+Tab` - Cycle agent modes
62
62
  * `Ctrl+C` - Quit application
63
63