shotgun-sh 0.2.11.dev2__py3-none-any.whl → 0.2.11.dev7__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 (72) hide show
  1. shotgun/agents/agent_manager.py +194 -28
  2. shotgun/agents/common.py +14 -8
  3. shotgun/agents/config/manager.py +64 -33
  4. shotgun/agents/config/models.py +25 -1
  5. shotgun/agents/config/provider.py +2 -2
  6. shotgun/agents/context_analyzer/analyzer.py +2 -24
  7. shotgun/agents/conversation_manager.py +35 -19
  8. shotgun/agents/export.py +2 -2
  9. shotgun/agents/history/history_processors.py +99 -3
  10. shotgun/agents/history/token_counting/anthropic.py +17 -1
  11. shotgun/agents/history/token_counting/base.py +14 -3
  12. shotgun/agents/history/token_counting/openai.py +11 -1
  13. shotgun/agents/history/token_counting/sentencepiece_counter.py +8 -0
  14. shotgun/agents/history/token_counting/tokenizer_cache.py +3 -1
  15. shotgun/agents/history/token_counting/utils.py +0 -3
  16. shotgun/agents/plan.py +2 -2
  17. shotgun/agents/research.py +3 -3
  18. shotgun/agents/specify.py +2 -2
  19. shotgun/agents/tasks.py +2 -2
  20. shotgun/agents/tools/codebase/file_read.py +5 -2
  21. shotgun/agents/tools/file_management.py +11 -7
  22. shotgun/agents/tools/web_search/__init__.py +8 -8
  23. shotgun/agents/tools/web_search/anthropic.py +2 -2
  24. shotgun/agents/tools/web_search/gemini.py +1 -1
  25. shotgun/agents/tools/web_search/openai.py +1 -1
  26. shotgun/agents/tools/web_search/utils.py +2 -2
  27. shotgun/agents/usage_manager.py +16 -11
  28. shotgun/cli/clear.py +2 -1
  29. shotgun/cli/compact.py +3 -3
  30. shotgun/cli/config.py +8 -5
  31. shotgun/cli/context.py +2 -2
  32. shotgun/cli/export.py +1 -1
  33. shotgun/cli/feedback.py +4 -2
  34. shotgun/cli/plan.py +1 -1
  35. shotgun/cli/research.py +1 -1
  36. shotgun/cli/specify.py +1 -1
  37. shotgun/cli/tasks.py +1 -1
  38. shotgun/codebase/core/change_detector.py +5 -3
  39. shotgun/codebase/core/code_retrieval.py +4 -2
  40. shotgun/codebase/core/ingestor.py +10 -8
  41. shotgun/codebase/core/manager.py +3 -3
  42. shotgun/codebase/core/nl_query.py +1 -1
  43. shotgun/exceptions.py +32 -0
  44. shotgun/logging_config.py +10 -17
  45. shotgun/main.py +3 -1
  46. shotgun/posthog_telemetry.py +14 -4
  47. shotgun/sentry_telemetry.py +22 -2
  48. shotgun/telemetry.py +3 -1
  49. shotgun/tui/app.py +71 -65
  50. shotgun/tui/components/context_indicator.py +43 -0
  51. shotgun/tui/containers.py +15 -17
  52. shotgun/tui/dependencies.py +2 -2
  53. shotgun/tui/screens/chat/chat_screen.py +164 -40
  54. shotgun/tui/screens/chat/help_text.py +16 -15
  55. shotgun/tui/screens/chat_screen/command_providers.py +10 -0
  56. shotgun/tui/screens/feedback.py +4 -4
  57. shotgun/tui/screens/github_issue.py +102 -0
  58. shotgun/tui/screens/model_picker.py +21 -20
  59. shotgun/tui/screens/onboarding.py +431 -0
  60. shotgun/tui/screens/provider_config.py +50 -27
  61. shotgun/tui/screens/shotgun_auth.py +2 -2
  62. shotgun/tui/screens/welcome.py +14 -11
  63. shotgun/tui/services/conversation_service.py +16 -14
  64. shotgun/tui/utils/mode_progress.py +14 -7
  65. shotgun/tui/widgets/widget_coordinator.py +15 -0
  66. shotgun/utils/file_system_utils.py +19 -0
  67. shotgun/utils/marketing.py +110 -0
  68. {shotgun_sh-0.2.11.dev2.dist-info → shotgun_sh-0.2.11.dev7.dist-info}/METADATA +2 -1
  69. {shotgun_sh-0.2.11.dev2.dist-info → shotgun_sh-0.2.11.dev7.dist-info}/RECORD +72 -68
  70. {shotgun_sh-0.2.11.dev2.dist-info → shotgun_sh-0.2.11.dev7.dist-info}/WHEEL +0 -0
  71. {shotgun_sh-0.2.11.dev2.dist-info → shotgun_sh-0.2.11.dev7.dist-info}/entry_points.txt +0 -0
  72. {shotgun_sh-0.2.11.dev2.dist-info → shotgun_sh-0.2.11.dev7.dist-info}/licenses/LICENSE +0 -0
shotgun/logging_config.py CHANGED
@@ -3,11 +3,15 @@
3
3
  import logging
4
4
  import logging.handlers
5
5
  import sys
6
+ from datetime import datetime, timezone
6
7
  from pathlib import Path
7
8
 
8
9
  from shotgun.settings import settings
9
10
  from shotgun.utils.env_utils import is_truthy
10
11
 
12
+ # Generate a single timestamp for this run to be used across all loggers
13
+ _RUN_TIMESTAMP = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
14
+
11
15
 
12
16
  def get_log_directory() -> Path:
13
17
  """Get the log directory path, creating it if necessary.
@@ -66,10 +70,7 @@ def setup_logger(
66
70
  logger = logging.getLogger(name)
67
71
 
68
72
  # Check if we already have a file handler
69
- has_file_handler = any(
70
- isinstance(h, logging.handlers.TimedRotatingFileHandler)
71
- for h in logger.handlers
72
- )
73
+ has_file_handler = any(isinstance(h, logging.FileHandler) for h in logger.handlers)
73
74
 
74
75
  # If we already have a file handler, just return the logger
75
76
  if has_file_handler:
@@ -120,21 +121,16 @@ def setup_logger(
120
121
 
121
122
  if file_logging_enabled:
122
123
  try:
123
- # Create file handler with rotation
124
+ # Create file handler with ISO8601 timestamp for each run
124
125
  log_dir = get_log_directory()
125
- log_file = log_dir / "shotgun.log"
126
+ log_file = log_dir / f"shotgun-{_RUN_TIMESTAMP}.log"
126
127
 
127
- # Use TimedRotatingFileHandler - rotates daily and keeps 7 days of logs
128
- file_handler = logging.handlers.TimedRotatingFileHandler(
128
+ # Use regular FileHandler - each run gets its own isolated log file
129
+ file_handler = logging.FileHandler(
129
130
  filename=log_file,
130
- when="midnight", # Rotate at midnight
131
- interval=1, # Every 1 day
132
- backupCount=7, # Keep 7 days of logs
133
131
  encoding="utf-8",
134
132
  )
135
133
 
136
- # Also set max file size (10MB) using RotatingFileHandler as fallback
137
- # Note: We'll use TimedRotatingFileHandler which handles both time and size
138
134
  file_handler.setLevel(getattr(logging, log_level))
139
135
 
140
136
  # Use standard formatter for file (no colors)
@@ -189,10 +185,7 @@ def get_logger(name: str) -> logging.Logger:
189
185
  logger = logging.getLogger(name)
190
186
 
191
187
  # Check if we have a file handler already
192
- has_file_handler = any(
193
- isinstance(h, logging.handlers.TimedRotatingFileHandler)
194
- for h in logger.handlers
195
- )
188
+ has_file_handler = any(isinstance(h, logging.FileHandler) for h in logger.handlers)
196
189
 
197
190
  # If no file handler, set up the logger (will add file handler)
198
191
  if not has_file_handler:
shotgun/main.py CHANGED
@@ -56,8 +56,10 @@ logger.debug("Logfire observability enabled: %s", _logfire_enabled)
56
56
 
57
57
  # Initialize configuration
58
58
  try:
59
+ import asyncio
60
+
59
61
  config_manager = get_config_manager()
60
- config_manager.load() # Ensure config is loaded at startup
62
+ asyncio.run(config_manager.load()) # Ensure config is loaded at startup
61
63
  except Exception as e:
62
64
  logger.debug("Configuration initialization warning: %s", e)
63
65
 
@@ -59,8 +59,10 @@ def setup_posthog_observability() -> bool:
59
59
 
60
60
  # Set user context with anonymous shotgun instance ID from config
61
61
  try:
62
+ import asyncio
63
+
62
64
  config_manager = get_config_manager()
63
- shotgun_instance_id = config_manager.get_shotgun_instance_id()
65
+ shotgun_instance_id = asyncio.run(config_manager.get_shotgun_instance_id())
64
66
 
65
67
  # Identify the user in PostHog
66
68
  posthog.identify( # type: ignore[attr-defined]
@@ -107,9 +109,11 @@ def track_event(event_name: str, properties: dict[str, Any] | None = None) -> No
107
109
  return
108
110
 
109
111
  try:
112
+ import asyncio
113
+
110
114
  # Get shotgun instance ID for tracking
111
115
  config_manager = get_config_manager()
112
- shotgun_instance_id = config_manager.get_shotgun_instance_id()
116
+ shotgun_instance_id = asyncio.run(config_manager.get_shotgun_instance_id())
113
117
 
114
118
  # Add version and environment to properties
115
119
  if properties is None:
@@ -168,10 +172,16 @@ def submit_feedback_survey(feedback: Feedback) -> None:
168
172
  logger.debug("PostHog not initialized, skipping feedback survey")
169
173
  return
170
174
 
175
+ import asyncio
176
+
171
177
  config_manager = get_config_manager()
172
- config = config_manager.load()
178
+ config = asyncio.run(config_manager.load())
173
179
  conversation_manager = ConversationManager()
174
- conversation = conversation_manager.load()
180
+ conversation = None
181
+ try:
182
+ conversation = asyncio.run(conversation_manager.load())
183
+ except Exception as e:
184
+ logger.debug(f"Failed to load conversation history: {e}")
175
185
  last_10_messages = []
176
186
  if conversation is not None:
177
187
  last_10_messages = conversation.get_agent_messages()[:10]
@@ -1,5 +1,7 @@
1
1
  """Sentry observability setup for Shotgun."""
2
2
 
3
+ from typing import Any
4
+
3
5
  from shotgun import __version__
4
6
  from shotgun.logging_config import get_early_logger
5
7
  from shotgun.settings import settings
@@ -32,12 +34,27 @@ def setup_sentry_observability() -> bool:
32
34
  logger.debug("Using Sentry DSN from settings, proceeding with setup")
33
35
 
34
36
  # Determine environment based on version
35
- # Dev versions contain "dev", "rc", "alpha", or "beta"
37
+ # Dev versions contain "dev", "rc", "alpha", "beta"
36
38
  if any(marker in __version__ for marker in ["dev", "rc", "alpha", "beta"]):
37
39
  environment = "development"
38
40
  else:
39
41
  environment = "production"
40
42
 
43
+ def before_send(event: Any, hint: dict[str, Any]) -> Any:
44
+ """Filter out user-actionable errors from Sentry.
45
+
46
+ User-actionable errors (like context size limits) are expected conditions
47
+ that users need to resolve, not bugs that need tracking.
48
+ """
49
+ if "exc_info" in hint:
50
+ exc_type, exc_value, tb = hint["exc_info"]
51
+ from shotgun.exceptions import ErrorNotPickedUpBySentry
52
+
53
+ if isinstance(exc_value, ErrorNotPickedUpBySentry):
54
+ # Don't send to Sentry - this is user-actionable, not a bug
55
+ return None
56
+ return event
57
+
41
58
  # Initialize Sentry
42
59
  sentry_sdk.init(
43
60
  dsn=dsn,
@@ -46,14 +63,17 @@ def setup_sentry_observability() -> bool:
46
63
  send_default_pii=False, # Privacy-first: never send PII
47
64
  traces_sample_rate=0.1 if environment == "production" else 1.0,
48
65
  profiles_sample_rate=0.1 if environment == "production" else 1.0,
66
+ before_send=before_send,
49
67
  )
50
68
 
51
69
  # Set user context with anonymous shotgun instance ID from config
52
70
  try:
71
+ import asyncio
72
+
53
73
  from shotgun.agents.config import get_config_manager
54
74
 
55
75
  config_manager = get_config_manager()
56
- shotgun_instance_id = config_manager.get_shotgun_instance_id()
76
+ shotgun_instance_id = asyncio.run(config_manager.get_shotgun_instance_id())
57
77
  sentry_sdk.set_user({"id": shotgun_instance_id})
58
78
  logger.debug("Sentry user context set with anonymous ID")
59
79
  except Exception as e:
shotgun/telemetry.py CHANGED
@@ -50,12 +50,14 @@ def setup_logfire_observability() -> bool:
50
50
 
51
51
  # Set user context using baggage for all logs and spans
52
52
  try:
53
+ import asyncio
54
+
53
55
  from opentelemetry import baggage, context
54
56
 
55
57
  from shotgun.agents.config import get_config_manager
56
58
 
57
59
  config_manager = get_config_manager()
58
- shotgun_instance_id = config_manager.get_shotgun_instance_id()
60
+ shotgun_instance_id = asyncio.run(config_manager.get_shotgun_instance_id())
59
61
 
60
62
  # Set shotgun_instance_id as baggage in global context - this will be included in all logs/spans
61
63
  ctx = baggage.set_baggage("shotgun_instance_id", shotgun_instance_id)
shotgun/tui/app.py CHANGED
@@ -5,6 +5,7 @@ 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
9
10
  from shotgun.agents.models import AgentType
10
11
  from shotgun.logging_config import get_logger
@@ -18,7 +19,7 @@ from shotgun.utils.update_checker import (
18
19
 
19
20
  from .screens.chat import ChatScreen
20
21
  from .screens.directory_setup import DirectorySetupScreen
21
- from .screens.feedback import FeedbackScreen
22
+ from .screens.github_issue import GitHubIssueScreen
22
23
  from .screens.model_picker import ModelPickerScreen
23
24
  from .screens.pipx_migration import PipxMigrationScreen
24
25
  from .screens.provider_config import ProviderConfigScreen
@@ -34,7 +35,7 @@ class ShotgunApp(App[None]):
34
35
  "provider_config": ProviderConfigScreen,
35
36
  "model_picker": ModelPickerScreen,
36
37
  "directory_setup": DirectorySetupScreen,
37
- "feedback": FeedbackScreen,
38
+ "github_issue": GitHubIssueScreen,
38
39
  }
39
40
  BINDINGS = [
40
41
  Binding("ctrl+c", "quit", "Quit the app"),
@@ -95,65 +96,75 @@ class ShotgunApp(App[None]):
95
96
  )
96
97
  return
97
98
 
98
- # Show welcome screen if no providers are configured OR if user hasn't seen it yet
99
- config = self.config_manager.load()
100
- if (
101
- not self.config_manager.has_any_provider_key()
102
- or not config.shown_welcome_screen
103
- ):
104
- if isinstance(self.screen, WelcomeScreen):
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
+ )
105
112
  return
106
113
 
107
- self.push_screen(
108
- WelcomeScreen(),
109
- callback=lambda _arg: self.refresh_startup_screen(),
110
- )
111
- return
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
112
123
 
113
- if not self.check_local_shotgun_directory_exists():
114
- if isinstance(self.screen, DirectorySetupScreen):
124
+ if isinstance(self.screen, ChatScreen):
115
125
  return
116
126
 
117
- self.push_screen(
118
- DirectorySetupScreen(),
119
- 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,
120
158
  )
121
- return
122
-
123
- if isinstance(self.screen, ChatScreen):
124
- return
125
-
126
- # Create ChatScreen with all dependencies injected from container
127
- # Get the default agent mode (RESEARCH)
128
- agent_mode = AgentType.RESEARCH
129
-
130
- # Create AgentManager with the correct mode
131
- agent_manager = self.container.agent_manager_factory(initial_type=agent_mode)
132
-
133
- # Create ProcessingStateManager - we'll pass the screen after creation
134
- # For now, create with None and the ChatScreen will set itself
135
- chat_screen = ChatScreen(
136
- agent_manager=agent_manager,
137
- conversation_manager=self.container.conversation_manager(),
138
- conversation_service=self.container.conversation_service(),
139
- widget_coordinator=self.container.widget_coordinator_factory(screen=None),
140
- processing_state=self.container.processing_state_factory(
141
- screen=None, # Will be set after ChatScreen is created
142
- telemetry_context={"agent_mode": agent_mode.value},
143
- ),
144
- command_handler=self.container.command_handler(),
145
- placeholder_hints=self.container.placeholder_hints(),
146
- codebase_sdk=self.container.codebase_sdk(),
147
- deps=self.container.agent_deps(),
148
- continue_session=self.continue_session,
149
- force_reindex=self.force_reindex,
150
- )
151
159
 
152
- # Update the ProcessingStateManager and WidgetCoordinator with the actual ChatScreen instance
153
- chat_screen.processing_state.screen = chat_screen
154
- chat_screen.widget_coordinator.screen = chat_screen
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
155
163
 
156
- self.push_screen(chat_screen)
164
+ self.push_screen(chat_screen)
165
+
166
+ # Run the async config check in a worker
167
+ self.run_worker(_check_config(), exclusive=False)
157
168
 
158
169
  def check_local_shotgun_directory_exists(self) -> bool:
159
170
  shotgun_dir = get_shotgun_base_path()
@@ -170,20 +181,15 @@ class ShotgunApp(App[None]):
170
181
  def get_system_commands(self, screen: Screen[Any]) -> Iterable[SystemCommand]:
171
182
  return [
172
183
  SystemCommand(
173
- "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,
174
187
  )
175
- ] # we don't want any system commands
176
-
177
- def action_feedback(self) -> None:
178
- """Open feedback screen and submit feedback."""
179
- from shotgun.posthog_telemetry import Feedback, submit_feedback_survey
180
-
181
- def handle_feedback(feedback: Feedback | None) -> None:
182
- if feedback is not None:
183
- submit_feedback_survey(feedback)
184
- self.notify("Feedback sent. Thank you!")
188
+ ]
185
189
 
186
- self.push_screen(FeedbackScreen(), callback=handle_feedback)
190
+ def action_new_issue(self) -> None:
191
+ """Open GitHub issue screen to guide users to create an issue."""
192
+ self.push_screen(GitHubIssueScreen())
187
193
 
188
194
 
189
195
  def run(
@@ -1,6 +1,7 @@
1
1
  """Context window indicator component for showing model usage."""
2
2
 
3
3
  from textual.reactive import reactive
4
+ from textual.timer import Timer
4
5
  from textual.widgets import Static
5
6
 
6
7
  from shotgun.agents.config.models import MODEL_SPECS, ModelName
@@ -20,6 +21,10 @@ class ContextIndicator(Static):
20
21
 
21
22
  context_analysis: reactive[ContextAnalysis | None] = reactive(None)
22
23
  model_name: reactive[ModelName | None] = reactive(None)
24
+ is_streaming: reactive[bool] = reactive(False)
25
+
26
+ _animation_frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
27
+ _animation_index = 0
23
28
 
24
29
  def __init__(
25
30
  self,
@@ -29,6 +34,7 @@ class ContextIndicator(Static):
29
34
  classes: str | None = None,
30
35
  ) -> None:
31
36
  super().__init__(name=name, id=id, classes=classes)
37
+ self._animation_timer: Timer | None = None
32
38
 
33
39
  def update_context(
34
40
  self, analysis: ContextAnalysis | None, model: ModelName | None
@@ -43,6 +49,38 @@ class ContextIndicator(Static):
43
49
  self.model_name = model
44
50
  self._refresh_display()
45
51
 
52
+ def set_streaming(self, streaming: bool) -> None:
53
+ """Enable or disable streaming animation.
54
+
55
+ Args:
56
+ streaming: Whether to show streaming animation
57
+ """
58
+ self.is_streaming = streaming
59
+ if streaming:
60
+ self._start_animation()
61
+ else:
62
+ self._stop_animation()
63
+
64
+ def _start_animation(self) -> None:
65
+ """Start the pulsing animation."""
66
+ if self._animation_timer is None:
67
+ self._animation_timer = self.set_interval(0.1, self._animate_frame)
68
+
69
+ def _stop_animation(self) -> None:
70
+ """Stop the pulsing animation."""
71
+ if self._animation_timer is not None:
72
+ self._animation_timer.stop()
73
+ self._animation_timer = None
74
+ self._animation_index = 0
75
+ self._refresh_display()
76
+
77
+ def _animate_frame(self) -> None:
78
+ """Advance the animation frame."""
79
+ self._animation_index = (self._animation_index + 1) % len(
80
+ self._animation_frames
81
+ )
82
+ self._refresh_display()
83
+
46
84
  def _get_percentage_color(self, percentage: float) -> str:
47
85
  """Get color for percentage based on threshold.
48
86
 
@@ -112,6 +150,11 @@ class ContextIndicator(Static):
112
150
  f"[{color}]{percentage}% ({current_tokens}/{max_tokens})[/]",
113
151
  ]
114
152
 
153
+ # Add streaming animation indicator if streaming
154
+ if self.is_streaming:
155
+ animation_char = self._animation_frames[self._animation_index]
156
+ parts.append(f"[bold cyan]{animation_char}[/]")
157
+
115
158
  # Add model name if available
116
159
  if self.model_name:
117
160
  model_spec = MODEL_SPECS.get(self.model_name)
shotgun/tui/containers.py CHANGED
@@ -5,10 +5,8 @@ from typing import TYPE_CHECKING
5
5
  from dependency_injector import containers, providers
6
6
  from pydantic_ai import RunContext
7
7
 
8
- from shotgun.agents.agent_manager import AgentManager
9
- from shotgun.agents.config import get_provider_model
10
8
  from shotgun.agents.conversation_manager import ConversationManager
11
- from shotgun.agents.models import AgentDeps, AgentType
9
+ from shotgun.agents.models import AgentDeps
12
10
  from shotgun.sdk.codebase import CodebaseSDK
13
11
  from shotgun.tui.commands import CommandHandler
14
12
  from shotgun.tui.filtered_codebase_service import FilteredCodebaseService
@@ -35,13 +33,19 @@ class TUIContainer(containers.DeclarativeContainer):
35
33
 
36
34
  This container manages the lifecycle and dependencies of all TUI components,
37
35
  ensuring consistent configuration and facilitating testing.
36
+
37
+ Note: model_config and agent_deps are created lazily via async factory methods
38
+ since get_provider_model() is now async.
38
39
  """
39
40
 
40
41
  # Configuration
41
42
  config = providers.Configuration()
42
43
 
43
44
  # Core dependencies
44
- model_config = providers.Singleton(get_provider_model)
45
+ # TODO: Figure out a better solution for async dependency injection
46
+ # model_config is now loaded lazily via create_default_tui_deps()
47
+ # because get_provider_model() is async. This breaks the DI pattern
48
+ # and should be refactored to support async factories properly.
45
49
 
46
50
  storage_dir = providers.Singleton(lambda: get_shotgun_home() / "codebases")
47
51
 
@@ -51,15 +55,10 @@ class TUIContainer(containers.DeclarativeContainer):
51
55
 
52
56
  system_prompt_fn = providers.Object(_placeholder_system_prompt)
53
57
 
54
- # AgentDeps singleton
55
- agent_deps = providers.Singleton(
56
- AgentDeps,
57
- interactive_mode=True,
58
- is_tui_context=True,
59
- llm_model=model_config,
60
- codebase_service=codebase_service,
61
- system_prompt_fn=system_prompt_fn,
62
- )
58
+ # TODO: Figure out a better solution for async dependency injection
59
+ # AgentDeps is now created via async create_default_tui_deps()
60
+ # instead of using DI container's Singleton provider because it requires
61
+ # async model_config initialization
63
62
 
64
63
  # Service singletons
65
64
  codebase_sdk = providers.Singleton(CodebaseSDK)
@@ -74,10 +73,9 @@ class TUIContainer(containers.DeclarativeContainer):
74
73
  ConversationService, conversation_manager=conversation_manager
75
74
  )
76
75
 
77
- # Factory for AgentManager (needs agent_type parameter)
78
- agent_manager_factory = providers.Factory(
79
- AgentManager, deps=agent_deps, initial_type=providers.Object(AgentType.RESEARCH)
80
- )
76
+ # TODO: Figure out a better solution for async dependency injection
77
+ # AgentManager factory removed - create via async initialization
78
+ # since it requires async agent creation
81
79
 
82
80
  # Factory for ProcessingStateManager (needs ChatScreen reference)
83
81
  processing_state_factory = providers.Factory(
@@ -8,7 +8,7 @@ from shotgun.tui.filtered_codebase_service import FilteredCodebaseService
8
8
  from shotgun.utils import get_shotgun_home
9
9
 
10
10
 
11
- def create_default_tui_deps() -> AgentDeps:
11
+ async def create_default_tui_deps() -> AgentDeps:
12
12
  """Create default AgentDeps for TUI components.
13
13
 
14
14
  This creates a standard AgentDeps configuration suitable for interactive
@@ -21,7 +21,7 @@ def create_default_tui_deps() -> AgentDeps:
21
21
  Returns:
22
22
  Configured AgentDeps instance ready for TUI use.
23
23
  """
24
- model_config = get_provider_model()
24
+ model_config = await get_provider_model()
25
25
  storage_dir = get_shotgun_home() / "codebases"
26
26
  codebase_service = FilteredCodebaseService(storage_dir)
27
27