shotgun-sh 0.2.11__py3-none-any.whl → 0.2.11.dev2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of shotgun-sh might be problematic. Click here for more details.

Files changed (73) hide show
  1. shotgun/agents/agent_manager.py +28 -194
  2. shotgun/agents/common.py +8 -14
  3. shotgun/agents/config/manager.py +33 -64
  4. shotgun/agents/config/models.py +1 -25
  5. shotgun/agents/config/provider.py +2 -2
  6. shotgun/agents/context_analyzer/analyzer.py +24 -2
  7. shotgun/agents/conversation_manager.py +19 -35
  8. shotgun/agents/export.py +2 -2
  9. shotgun/agents/history/history_processors.py +3 -99
  10. shotgun/agents/history/token_counting/anthropic.py +1 -17
  11. shotgun/agents/history/token_counting/base.py +3 -14
  12. shotgun/agents/history/token_counting/openai.py +1 -11
  13. shotgun/agents/history/token_counting/sentencepiece_counter.py +0 -8
  14. shotgun/agents/history/token_counting/tokenizer_cache.py +1 -3
  15. shotgun/agents/history/token_counting/utils.py +3 -0
  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 +2 -5
  21. shotgun/agents/tools/file_management.py +7 -11
  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 +11 -16
  28. shotgun/build_constants.py +2 -2
  29. shotgun/cli/clear.py +1 -2
  30. shotgun/cli/compact.py +3 -3
  31. shotgun/cli/config.py +5 -8
  32. shotgun/cli/context.py +2 -2
  33. shotgun/cli/export.py +1 -1
  34. shotgun/cli/feedback.py +2 -4
  35. shotgun/cli/plan.py +1 -1
  36. shotgun/cli/research.py +1 -1
  37. shotgun/cli/specify.py +1 -1
  38. shotgun/cli/tasks.py +1 -1
  39. shotgun/codebase/core/change_detector.py +3 -5
  40. shotgun/codebase/core/code_retrieval.py +2 -4
  41. shotgun/codebase/core/ingestor.py +8 -10
  42. shotgun/codebase/core/manager.py +3 -3
  43. shotgun/codebase/core/nl_query.py +1 -1
  44. shotgun/logging_config.py +17 -10
  45. shotgun/main.py +1 -3
  46. shotgun/posthog_telemetry.py +4 -14
  47. shotgun/sentry_telemetry.py +2 -22
  48. shotgun/telemetry.py +1 -3
  49. shotgun/tui/app.py +65 -71
  50. shotgun/tui/components/context_indicator.py +0 -43
  51. shotgun/tui/containers.py +17 -15
  52. shotgun/tui/dependencies.py +2 -2
  53. shotgun/tui/screens/chat/chat_screen.py +40 -164
  54. shotgun/tui/screens/chat/help_text.py +15 -16
  55. shotgun/tui/screens/chat_screen/command_providers.py +0 -10
  56. shotgun/tui/screens/feedback.py +4 -4
  57. shotgun/tui/screens/model_picker.py +20 -21
  58. shotgun/tui/screens/provider_config.py +27 -50
  59. shotgun/tui/screens/shotgun_auth.py +2 -2
  60. shotgun/tui/screens/welcome.py +11 -14
  61. shotgun/tui/services/conversation_service.py +14 -16
  62. shotgun/tui/utils/mode_progress.py +7 -14
  63. shotgun/tui/widgets/widget_coordinator.py +0 -15
  64. shotgun/utils/file_system_utils.py +0 -19
  65. {shotgun_sh-0.2.11.dist-info → shotgun_sh-0.2.11.dev2.dist-info}/METADATA +1 -2
  66. {shotgun_sh-0.2.11.dist-info → shotgun_sh-0.2.11.dev2.dist-info}/RECORD +69 -73
  67. shotgun/exceptions.py +0 -32
  68. shotgun/tui/screens/github_issue.py +0 -102
  69. shotgun/tui/screens/onboarding.py +0 -431
  70. shotgun/utils/marketing.py +0 -110
  71. {shotgun_sh-0.2.11.dist-info → shotgun_sh-0.2.11.dev2.dist-info}/WHEEL +0 -0
  72. {shotgun_sh-0.2.11.dist-info → shotgun_sh-0.2.11.dev2.dist-info}/entry_points.txt +0 -0
  73. {shotgun_sh-0.2.11.dist-info → shotgun_sh-0.2.11.dev2.dist-info}/licenses/LICENSE +0 -0
shotgun/logging_config.py CHANGED
@@ -3,15 +3,11 @@
3
3
  import logging
4
4
  import logging.handlers
5
5
  import sys
6
- from datetime import datetime, timezone
7
6
  from pathlib import Path
8
7
 
9
8
  from shotgun.settings import settings
10
9
  from shotgun.utils.env_utils import is_truthy
11
10
 
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
-
15
11
 
16
12
  def get_log_directory() -> Path:
17
13
  """Get the log directory path, creating it if necessary.
@@ -70,7 +66,10 @@ def setup_logger(
70
66
  logger = logging.getLogger(name)
71
67
 
72
68
  # Check if we already have a file handler
73
- has_file_handler = any(isinstance(h, logging.FileHandler) for h in logger.handlers)
69
+ has_file_handler = any(
70
+ isinstance(h, logging.handlers.TimedRotatingFileHandler)
71
+ for h in logger.handlers
72
+ )
74
73
 
75
74
  # If we already have a file handler, just return the logger
76
75
  if has_file_handler:
@@ -121,16 +120,21 @@ def setup_logger(
121
120
 
122
121
  if file_logging_enabled:
123
122
  try:
124
- # Create file handler with ISO8601 timestamp for each run
123
+ # Create file handler with rotation
125
124
  log_dir = get_log_directory()
126
- log_file = log_dir / f"shotgun-{_RUN_TIMESTAMP}.log"
125
+ log_file = log_dir / "shotgun.log"
127
126
 
128
- # Use regular FileHandler - each run gets its own isolated log file
129
- file_handler = logging.FileHandler(
127
+ # Use TimedRotatingFileHandler - rotates daily and keeps 7 days of logs
128
+ file_handler = logging.handlers.TimedRotatingFileHandler(
130
129
  filename=log_file,
130
+ when="midnight", # Rotate at midnight
131
+ interval=1, # Every 1 day
132
+ backupCount=7, # Keep 7 days of logs
131
133
  encoding="utf-8",
132
134
  )
133
135
 
136
+ # Also set max file size (10MB) using RotatingFileHandler as fallback
137
+ # Note: We'll use TimedRotatingFileHandler which handles both time and size
134
138
  file_handler.setLevel(getattr(logging, log_level))
135
139
 
136
140
  # Use standard formatter for file (no colors)
@@ -185,7 +189,10 @@ def get_logger(name: str) -> logging.Logger:
185
189
  logger = logging.getLogger(name)
186
190
 
187
191
  # Check if we have a file handler already
188
- has_file_handler = any(isinstance(h, logging.FileHandler) for h in logger.handlers)
192
+ has_file_handler = any(
193
+ isinstance(h, logging.handlers.TimedRotatingFileHandler)
194
+ for h in logger.handlers
195
+ )
189
196
 
190
197
  # If no file handler, set up the logger (will add file handler)
191
198
  if not has_file_handler:
shotgun/main.py CHANGED
@@ -56,10 +56,8 @@ logger.debug("Logfire observability enabled: %s", _logfire_enabled)
56
56
 
57
57
  # Initialize configuration
58
58
  try:
59
- import asyncio
60
-
61
59
  config_manager = get_config_manager()
62
- asyncio.run(config_manager.load()) # Ensure config is loaded at startup
60
+ config_manager.load() # Ensure config is loaded at startup
63
61
  except Exception as e:
64
62
  logger.debug("Configuration initialization warning: %s", e)
65
63
 
@@ -59,10 +59,8 @@ 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
-
64
62
  config_manager = get_config_manager()
65
- shotgun_instance_id = asyncio.run(config_manager.get_shotgun_instance_id())
63
+ shotgun_instance_id = config_manager.get_shotgun_instance_id()
66
64
 
67
65
  # Identify the user in PostHog
68
66
  posthog.identify( # type: ignore[attr-defined]
@@ -109,11 +107,9 @@ def track_event(event_name: str, properties: dict[str, Any] | None = None) -> No
109
107
  return
110
108
 
111
109
  try:
112
- import asyncio
113
-
114
110
  # Get shotgun instance ID for tracking
115
111
  config_manager = get_config_manager()
116
- shotgun_instance_id = asyncio.run(config_manager.get_shotgun_instance_id())
112
+ shotgun_instance_id = config_manager.get_shotgun_instance_id()
117
113
 
118
114
  # Add version and environment to properties
119
115
  if properties is None:
@@ -172,16 +168,10 @@ def submit_feedback_survey(feedback: Feedback) -> None:
172
168
  logger.debug("PostHog not initialized, skipping feedback survey")
173
169
  return
174
170
 
175
- import asyncio
176
-
177
171
  config_manager = get_config_manager()
178
- config = asyncio.run(config_manager.load())
172
+ config = config_manager.load()
179
173
  conversation_manager = ConversationManager()
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}")
174
+ conversation = conversation_manager.load()
185
175
  last_10_messages = []
186
176
  if conversation is not None:
187
177
  last_10_messages = conversation.get_agent_messages()[:10]
@@ -1,7 +1,5 @@
1
1
  """Sentry observability setup for Shotgun."""
2
2
 
3
- from typing import Any
4
-
5
3
  from shotgun import __version__
6
4
  from shotgun.logging_config import get_early_logger
7
5
  from shotgun.settings import settings
@@ -34,27 +32,12 @@ def setup_sentry_observability() -> bool:
34
32
  logger.debug("Using Sentry DSN from settings, proceeding with setup")
35
33
 
36
34
  # Determine environment based on version
37
- # Dev versions contain "dev", "rc", "alpha", "beta"
35
+ # Dev versions contain "dev", "rc", "alpha", or "beta"
38
36
  if any(marker in __version__ for marker in ["dev", "rc", "alpha", "beta"]):
39
37
  environment = "development"
40
38
  else:
41
39
  environment = "production"
42
40
 
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
-
58
41
  # Initialize Sentry
59
42
  sentry_sdk.init(
60
43
  dsn=dsn,
@@ -63,17 +46,14 @@ def setup_sentry_observability() -> bool:
63
46
  send_default_pii=False, # Privacy-first: never send PII
64
47
  traces_sample_rate=0.1 if environment == "production" else 1.0,
65
48
  profiles_sample_rate=0.1 if environment == "production" else 1.0,
66
- before_send=before_send,
67
49
  )
68
50
 
69
51
  # Set user context with anonymous shotgun instance ID from config
70
52
  try:
71
- import asyncio
72
-
73
53
  from shotgun.agents.config import get_config_manager
74
54
 
75
55
  config_manager = get_config_manager()
76
- shotgun_instance_id = asyncio.run(config_manager.get_shotgun_instance_id())
56
+ shotgun_instance_id = config_manager.get_shotgun_instance_id()
77
57
  sentry_sdk.set_user({"id": shotgun_instance_id})
78
58
  logger.debug("Sentry user context set with anonymous ID")
79
59
  except Exception as e:
shotgun/telemetry.py CHANGED
@@ -50,14 +50,12 @@ 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
-
55
53
  from opentelemetry import baggage, context
56
54
 
57
55
  from shotgun.agents.config import get_config_manager
58
56
 
59
57
  config_manager = get_config_manager()
60
- shotgun_instance_id = asyncio.run(config_manager.get_shotgun_instance_id())
58
+ shotgun_instance_id = config_manager.get_shotgun_instance_id()
61
59
 
62
60
  # Set shotgun_instance_id as baggage in global context - this will be included in all logs/spans
63
61
  ctx = baggage.set_baggage("shotgun_instance_id", shotgun_instance_id)
shotgun/tui/app.py CHANGED
@@ -5,7 +5,6 @@ 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
9
8
  from shotgun.agents.config import ConfigManager, get_config_manager
10
9
  from shotgun.agents.models import AgentType
11
10
  from shotgun.logging_config import get_logger
@@ -19,7 +18,7 @@ from shotgun.utils.update_checker import (
19
18
 
20
19
  from .screens.chat import ChatScreen
21
20
  from .screens.directory_setup import DirectorySetupScreen
22
- from .screens.github_issue import GitHubIssueScreen
21
+ from .screens.feedback import FeedbackScreen
23
22
  from .screens.model_picker import ModelPickerScreen
24
23
  from .screens.pipx_migration import PipxMigrationScreen
25
24
  from .screens.provider_config import ProviderConfigScreen
@@ -35,7 +34,7 @@ class ShotgunApp(App[None]):
35
34
  "provider_config": ProviderConfigScreen,
36
35
  "model_picker": ModelPickerScreen,
37
36
  "directory_setup": DirectorySetupScreen,
38
- "github_issue": GitHubIssueScreen,
37
+ "feedback": FeedbackScreen,
39
38
  }
40
39
  BINDINGS = [
41
40
  Binding("ctrl+c", "quit", "Quit the app"),
@@ -96,75 +95,65 @@ class ShotgunApp(App[None]):
96
95
  )
97
96
  return
98
97
 
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
- )
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):
112
105
  return
113
106
 
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
107
+ self.push_screen(
108
+ WelcomeScreen(),
109
+ callback=lambda _arg: self.refresh_startup_screen(),
110
+ )
111
+ return
123
112
 
124
- if isinstance(self.screen, ChatScreen):
113
+ if not self.check_local_shotgun_directory_exists():
114
+ if isinstance(self.screen, DirectorySetupScreen):
125
115
  return
126
116
 
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,
117
+ self.push_screen(
118
+ DirectorySetupScreen(),
119
+ callback=lambda _arg: self.refresh_startup_screen(),
158
120
  )
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
+ )
159
151
 
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)
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
165
155
 
166
- # Run the async config check in a worker
167
- self.run_worker(_check_config(), exclusive=False)
156
+ self.push_screen(chat_screen)
168
157
 
169
158
  def check_local_shotgun_directory_exists(self) -> bool:
170
159
  shotgun_dir = get_shotgun_base_path()
@@ -181,15 +170,20 @@ class ShotgunApp(App[None]):
181
170
  def get_system_commands(self, screen: Screen[Any]) -> Iterable[SystemCommand]:
182
171
  return [
183
172
  SystemCommand(
184
- "New Issue",
185
- "Report a bug or request a feature on GitHub",
186
- self.action_new_issue,
173
+ "Feedback", "Send us feedback or report a bug", self.action_feedback
187
174
  )
188
- ]
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!")
189
185
 
190
- def action_new_issue(self) -> None:
191
- """Open GitHub issue screen to guide users to create an issue."""
192
- self.push_screen(GitHubIssueScreen())
186
+ self.push_screen(FeedbackScreen(), callback=handle_feedback)
193
187
 
194
188
 
195
189
  def run(
@@ -1,7 +1,6 @@
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
5
4
  from textual.widgets import Static
6
5
 
7
6
  from shotgun.agents.config.models import MODEL_SPECS, ModelName
@@ -21,10 +20,6 @@ class ContextIndicator(Static):
21
20
 
22
21
  context_analysis: reactive[ContextAnalysis | None] = reactive(None)
23
22
  model_name: reactive[ModelName | None] = reactive(None)
24
- is_streaming: reactive[bool] = reactive(False)
25
-
26
- _animation_frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
27
- _animation_index = 0
28
23
 
29
24
  def __init__(
30
25
  self,
@@ -34,7 +29,6 @@ class ContextIndicator(Static):
34
29
  classes: str | None = None,
35
30
  ) -> None:
36
31
  super().__init__(name=name, id=id, classes=classes)
37
- self._animation_timer: Timer | None = None
38
32
 
39
33
  def update_context(
40
34
  self, analysis: ContextAnalysis | None, model: ModelName | None
@@ -49,38 +43,6 @@ class ContextIndicator(Static):
49
43
  self.model_name = model
50
44
  self._refresh_display()
51
45
 
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
-
84
46
  def _get_percentage_color(self, percentage: float) -> str:
85
47
  """Get color for percentage based on threshold.
86
48
 
@@ -150,11 +112,6 @@ class ContextIndicator(Static):
150
112
  f"[{color}]{percentage}% ({current_tokens}/{max_tokens})[/]",
151
113
  ]
152
114
 
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
-
158
115
  # Add model name if available
159
116
  if self.model_name:
160
117
  model_spec = MODEL_SPECS.get(self.model_name)
shotgun/tui/containers.py CHANGED
@@ -5,8 +5,10 @@ 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
8
10
  from shotgun.agents.conversation_manager import ConversationManager
9
- from shotgun.agents.models import AgentDeps
11
+ from shotgun.agents.models import AgentDeps, AgentType
10
12
  from shotgun.sdk.codebase import CodebaseSDK
11
13
  from shotgun.tui.commands import CommandHandler
12
14
  from shotgun.tui.filtered_codebase_service import FilteredCodebaseService
@@ -33,19 +35,13 @@ class TUIContainer(containers.DeclarativeContainer):
33
35
 
34
36
  This container manages the lifecycle and dependencies of all TUI components,
35
37
  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.
39
38
  """
40
39
 
41
40
  # Configuration
42
41
  config = providers.Configuration()
43
42
 
44
43
  # Core dependencies
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.
44
+ model_config = providers.Singleton(get_provider_model)
49
45
 
50
46
  storage_dir = providers.Singleton(lambda: get_shotgun_home() / "codebases")
51
47
 
@@ -55,10 +51,15 @@ class TUIContainer(containers.DeclarativeContainer):
55
51
 
56
52
  system_prompt_fn = providers.Object(_placeholder_system_prompt)
57
53
 
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
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
+ )
62
63
 
63
64
  # Service singletons
64
65
  codebase_sdk = providers.Singleton(CodebaseSDK)
@@ -73,9 +74,10 @@ class TUIContainer(containers.DeclarativeContainer):
73
74
  ConversationService, conversation_manager=conversation_manager
74
75
  )
75
76
 
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
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
+ )
79
81
 
80
82
  # Factory for ProcessingStateManager (needs ChatScreen reference)
81
83
  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
- async def create_default_tui_deps() -> AgentDeps:
11
+ 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 @@ async def create_default_tui_deps() -> AgentDeps:
21
21
  Returns:
22
22
  Configured AgentDeps instance ready for TUI use.
23
23
  """
24
- model_config = await get_provider_model()
24
+ model_config = get_provider_model()
25
25
  storage_dir = get_shotgun_home() / "codebases"
26
26
  codebase_service = FilteredCodebaseService(storage_dir)
27
27