shotgun-sh 0.2.11.dev1__py3-none-any.whl → 0.2.11.dev3__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 (67) hide show
  1. shotgun/agents/agent_manager.py +150 -27
  2. shotgun/agents/common.py +14 -8
  3. shotgun/agents/config/manager.py +64 -33
  4. shotgun/agents/config/models.py +21 -1
  5. shotgun/agents/config/provider.py +2 -2
  6. shotgun/agents/context_analyzer/analyzer.py +2 -24
  7. shotgun/agents/conversation_manager.py +22 -13
  8. shotgun/agents/export.py +2 -2
  9. shotgun/agents/history/token_counting/anthropic.py +17 -1
  10. shotgun/agents/history/token_counting/base.py +14 -3
  11. shotgun/agents/history/token_counting/openai.py +8 -0
  12. shotgun/agents/history/token_counting/sentencepiece_counter.py +8 -0
  13. shotgun/agents/history/token_counting/tokenizer_cache.py +3 -1
  14. shotgun/agents/history/token_counting/utils.py +0 -3
  15. shotgun/agents/plan.py +2 -2
  16. shotgun/agents/research.py +3 -3
  17. shotgun/agents/specify.py +2 -2
  18. shotgun/agents/tasks.py +2 -2
  19. shotgun/agents/tools/codebase/file_read.py +5 -2
  20. shotgun/agents/tools/file_management.py +11 -7
  21. shotgun/agents/tools/web_search/__init__.py +8 -8
  22. shotgun/agents/tools/web_search/anthropic.py +2 -2
  23. shotgun/agents/tools/web_search/gemini.py +1 -1
  24. shotgun/agents/tools/web_search/openai.py +1 -1
  25. shotgun/agents/tools/web_search/utils.py +2 -2
  26. shotgun/agents/usage_manager.py +16 -11
  27. shotgun/cli/clear.py +2 -1
  28. shotgun/cli/compact.py +3 -3
  29. shotgun/cli/config.py +8 -5
  30. shotgun/cli/context.py +2 -2
  31. shotgun/cli/export.py +1 -1
  32. shotgun/cli/feedback.py +4 -2
  33. shotgun/cli/plan.py +1 -1
  34. shotgun/cli/research.py +1 -1
  35. shotgun/cli/specify.py +1 -1
  36. shotgun/cli/tasks.py +1 -1
  37. shotgun/codebase/core/change_detector.py +5 -3
  38. shotgun/codebase/core/code_retrieval.py +4 -2
  39. shotgun/codebase/core/ingestor.py +10 -8
  40. shotgun/codebase/core/manager.py +3 -3
  41. shotgun/codebase/core/nl_query.py +1 -1
  42. shotgun/logging_config.py +10 -17
  43. shotgun/main.py +3 -1
  44. shotgun/posthog_telemetry.py +14 -4
  45. shotgun/sentry_telemetry.py +3 -1
  46. shotgun/telemetry.py +3 -1
  47. shotgun/tui/app.py +62 -51
  48. shotgun/tui/components/context_indicator.py +43 -0
  49. shotgun/tui/containers.py +15 -17
  50. shotgun/tui/dependencies.py +2 -2
  51. shotgun/tui/screens/chat/chat_screen.py +75 -15
  52. shotgun/tui/screens/chat/help_text.py +16 -15
  53. shotgun/tui/screens/feedback.py +4 -4
  54. shotgun/tui/screens/model_picker.py +21 -20
  55. shotgun/tui/screens/provider_config.py +50 -27
  56. shotgun/tui/screens/shotgun_auth.py +2 -2
  57. shotgun/tui/screens/welcome.py +14 -11
  58. shotgun/tui/services/conversation_service.py +8 -8
  59. shotgun/tui/utils/mode_progress.py +14 -7
  60. shotgun/tui/widgets/widget_coordinator.py +15 -0
  61. shotgun/utils/file_system_utils.py +19 -0
  62. shotgun/utils/marketing.py +110 -0
  63. {shotgun_sh-0.2.11.dev1.dist-info → shotgun_sh-0.2.11.dev3.dist-info}/METADATA +2 -1
  64. {shotgun_sh-0.2.11.dev1.dist-info → shotgun_sh-0.2.11.dev3.dist-info}/RECORD +67 -66
  65. {shotgun_sh-0.2.11.dev1.dist-info → shotgun_sh-0.2.11.dev3.dist-info}/WHEEL +0 -0
  66. {shotgun_sh-0.2.11.dev1.dist-info → shotgun_sh-0.2.11.dev3.dist-info}/entry_points.txt +0 -0
  67. {shotgun_sh-0.2.11.dev1.dist-info → shotgun_sh-0.2.11.dev3.dist-info}/licenses/LICENSE +0 -0
shotgun/cli/clear.py CHANGED
@@ -1,5 +1,6 @@
1
1
  """Clear command for shotgun CLI."""
2
2
 
3
+ import asyncio
3
4
  from pathlib import Path
4
5
 
5
6
  import typer
@@ -37,7 +38,7 @@ def clear() -> None:
37
38
 
38
39
  # Clear the conversation
39
40
  manager = ConversationManager(conversation_file)
40
- manager.clear()
41
+ asyncio.run(manager.clear())
41
42
 
42
43
  console.print(
43
44
  "[green]✓[/green] Conversation cleared successfully", style="bold"
shotgun/cli/compact.py CHANGED
@@ -79,7 +79,7 @@ async def compact_conversation() -> dict[str, Any]:
79
79
 
80
80
  # Load conversation
81
81
  manager = ConversationManager(conversation_file)
82
- conversation = manager.load()
82
+ conversation = await manager.load()
83
83
 
84
84
  if not conversation:
85
85
  raise ValueError("Conversation file is empty or corrupted")
@@ -91,7 +91,7 @@ async def compact_conversation() -> dict[str, Any]:
91
91
  raise ValueError("No agent messages found in conversation")
92
92
 
93
93
  # Get model config
94
- model_config = get_provider_model()
94
+ model_config = await get_provider_model()
95
95
 
96
96
  # Calculate before metrics
97
97
  original_message_count = len(agent_messages)
@@ -133,7 +133,7 @@ async def compact_conversation() -> dict[str, Any]:
133
133
 
134
134
  # Save compacted conversation
135
135
  conversation.set_agent_messages(compacted_messages)
136
- manager.save(conversation)
136
+ await manager.save(conversation)
137
137
 
138
138
  logger.info(
139
139
  f"Compacted conversation: {original_message_count} → {compacted_message_count} messages "
shotgun/cli/config.py CHANGED
@@ -1,5 +1,6 @@
1
1
  """Configuration management CLI commands."""
2
2
 
3
+ import asyncio
3
4
  import json
4
5
  from typing import Annotated, Any
5
6
 
@@ -44,7 +45,7 @@ def init(
44
45
  console.print()
45
46
 
46
47
  # Initialize with defaults
47
- config_manager.initialize()
48
+ asyncio.run(config_manager.initialize())
48
49
 
49
50
  # Ask for provider
50
51
  provider_choices = ["openai", "anthropic", "google"]
@@ -76,7 +77,7 @@ def init(
76
77
 
77
78
  if api_key:
78
79
  # update_provider will automatically set selected_model for first provider
79
- config_manager.update_provider(provider, api_key=api_key)
80
+ asyncio.run(config_manager.update_provider(provider, api_key=api_key))
80
81
 
81
82
  console.print(
82
83
  f"\n✅ [bold green]Configuration saved to {config_manager.config_path}[/bold green]"
@@ -84,7 +85,7 @@ def init(
84
85
  console.print("🎯 You can now use Shotgun with your configured provider!")
85
86
 
86
87
  else:
87
- config_manager.initialize()
88
+ asyncio.run(config_manager.initialize())
88
89
  console.print(f"✅ Configuration initialized at {config_manager.config_path}")
89
90
 
90
91
 
@@ -112,7 +113,7 @@ def set(
112
113
 
113
114
  try:
114
115
  if api_key:
115
- config_manager.update_provider(provider, api_key=api_key)
116
+ asyncio.run(config_manager.update_provider(provider, api_key=api_key))
116
117
 
117
118
  console.print(f"✅ Configuration updated for {provider}")
118
119
 
@@ -133,8 +134,10 @@ def get(
133
134
  ] = False,
134
135
  ) -> None:
135
136
  """Display current configuration."""
137
+ import asyncio
138
+
136
139
  config_manager = get_config_manager()
137
- config = config_manager.load()
140
+ config = asyncio.run(config_manager.load())
138
141
 
139
142
  if json_output:
140
143
  # Convert to dict and mask secrets
shotgun/cli/context.py CHANGED
@@ -79,7 +79,7 @@ async def analyze_context() -> ContextAnalysisOutput:
79
79
 
80
80
  # Load conversation
81
81
  manager = ConversationManager(conversation_file)
82
- conversation = manager.load()
82
+ conversation = await manager.load()
83
83
 
84
84
  if not conversation:
85
85
  raise ValueError("Conversation file is empty or corrupted")
@@ -91,7 +91,7 @@ async def analyze_context() -> ContextAnalysisOutput:
91
91
  raise ValueError("No agent messages found in conversation")
92
92
 
93
93
  # Get model config (use default provider settings)
94
- model_config = get_provider_model()
94
+ model_config = await get_provider_model()
95
95
 
96
96
  # Debug: Log the model being used
97
97
  logger.debug(f"Using model: {model_config.name.value}")
shotgun/cli/export.py CHANGED
@@ -63,7 +63,7 @@ def export(
63
63
  )
64
64
 
65
65
  # Create the export agent with deps and provider
66
- agent, deps = create_export_agent(agent_runtime_options, provider)
66
+ agent, deps = asyncio.run(create_export_agent(agent_runtime_options, provider))
67
67
 
68
68
  # Start export process
69
69
  logger.info("🎯 Starting export...")
shotgun/cli/feedback.py CHANGED
@@ -28,9 +28,11 @@ def send_feedback(
28
28
  ],
29
29
  ) -> None:
30
30
  """Initialize Shotgun configuration."""
31
+ import asyncio
32
+
31
33
  config_manager = get_config_manager()
32
- config_manager.load()
33
- shotgun_instance_id = config_manager.get_shotgun_instance_id()
34
+ asyncio.run(config_manager.load())
35
+ shotgun_instance_id = asyncio.run(config_manager.get_shotgun_instance_id())
34
36
 
35
37
  if not description:
36
38
  console.print(
shotgun/cli/plan.py CHANGED
@@ -55,7 +55,7 @@ def plan(
55
55
  )
56
56
 
57
57
  # Create the plan agent with deps and provider
58
- agent, deps = create_plan_agent(agent_runtime_options, provider)
58
+ agent, deps = asyncio.run(create_plan_agent(agent_runtime_options, provider))
59
59
 
60
60
  # Start planning process
61
61
  logger.info("🎯 Starting planning...")
shotgun/cli/research.py CHANGED
@@ -73,7 +73,7 @@ async def async_research(
73
73
  agent_runtime_options = AgentRuntimeOptions(interactive_mode=not non_interactive)
74
74
 
75
75
  # Create the research agent with deps and provider
76
- agent, deps = create_research_agent(agent_runtime_options, provider)
76
+ agent, deps = await create_research_agent(agent_runtime_options, provider)
77
77
 
78
78
  # Start research process
79
79
  logger.info("🔬 Starting research...")
shotgun/cli/specify.py CHANGED
@@ -51,7 +51,7 @@ def specify(
51
51
  )
52
52
 
53
53
  # Create the specify agent with deps and provider
54
- agent, deps = create_specify_agent(agent_runtime_options, provider)
54
+ agent, deps = asyncio.run(create_specify_agent(agent_runtime_options, provider))
55
55
 
56
56
  # Start specification process
57
57
  logger.info("📋 Starting specification generation...")
shotgun/cli/tasks.py CHANGED
@@ -60,7 +60,7 @@ def tasks(
60
60
  )
61
61
 
62
62
  # Create the tasks agent with deps and provider
63
- agent, deps = create_tasks_agent(agent_runtime_options, provider)
63
+ agent, deps = asyncio.run(create_tasks_agent(agent_runtime_options, provider))
64
64
 
65
65
  # Start task creation process
66
66
  logger.info("🎯 Starting task creation...")
@@ -6,6 +6,7 @@ from enum import Enum
6
6
  from pathlib import Path
7
7
  from typing import Any, cast
8
8
 
9
+ import aiofiles
9
10
  import kuzu
10
11
 
11
12
  from shotgun.logging_config import get_logger
@@ -301,7 +302,7 @@ class ChangeDetector:
301
302
  # Direct substring match
302
303
  return pattern in filepath
303
304
 
304
- def _calculate_file_hash(self, filepath: Path) -> str:
305
+ async def _calculate_file_hash(self, filepath: Path) -> str:
305
306
  """Calculate hash of file contents.
306
307
 
307
308
  Args:
@@ -311,8 +312,9 @@ class ChangeDetector:
311
312
  SHA256 hash of file contents
312
313
  """
313
314
  try:
314
- with open(filepath, "rb") as f:
315
- return hashlib.sha256(f.read()).hexdigest()
315
+ async with aiofiles.open(filepath, "rb") as f:
316
+ content = await f.read()
317
+ return hashlib.sha256(content).hexdigest()
316
318
  except Exception as e:
317
319
  logger.error(f"Failed to calculate hash for {filepath}: {e}")
318
320
  return ""
@@ -3,6 +3,7 @@
3
3
  from pathlib import Path
4
4
  from typing import TYPE_CHECKING
5
5
 
6
+ import aiofiles
6
7
  from pydantic import BaseModel
7
8
 
8
9
  from shotgun.logging_config import get_logger
@@ -141,8 +142,9 @@ async def retrieve_code_by_qualified_name(
141
142
 
142
143
  # Read the file and extract the snippet
143
144
  try:
144
- with full_path.open("r", encoding="utf-8") as f:
145
- all_lines = f.readlines()
145
+ async with aiofiles.open(full_path, encoding="utf-8") as f:
146
+ content = await f.read()
147
+ all_lines = content.splitlines(keepends=True)
146
148
 
147
149
  # Extract the relevant lines (1-indexed to 0-indexed)
148
150
  snippet_lines = all_lines[start_line - 1 : end_line]
@@ -1,5 +1,6 @@
1
1
  """Kuzu graph ingestor for building code knowledge graphs."""
2
2
 
3
+ import asyncio
3
4
  import hashlib
4
5
  import os
5
6
  import time
@@ -8,6 +9,7 @@ from collections import defaultdict
8
9
  from pathlib import Path
9
10
  from typing import Any
10
11
 
12
+ import aiofiles
11
13
  import kuzu
12
14
  from tree_sitter import Node, Parser, QueryCursor
13
15
 
@@ -619,7 +621,7 @@ class SimpleGraphBuilder:
619
621
  # Don't let progress callback errors crash the build
620
622
  logger.debug(f"Progress callback error: {e}")
621
623
 
622
- def run(self) -> None:
624
+ async def run(self) -> None:
623
625
  """Run the three-pass graph building process."""
624
626
  logger.info(f"Building graph for project: {self.project_name}")
625
627
 
@@ -629,7 +631,7 @@ class SimpleGraphBuilder:
629
631
 
630
632
  # Pass 2: Definitions
631
633
  logger.info("Pass 2: Processing files and extracting definitions...")
632
- self._process_files()
634
+ await self._process_files()
633
635
 
634
636
  # Pass 3: Relationships
635
637
  logger.info("Pass 3: Processing relationships (calls, imports)...")
@@ -771,7 +773,7 @@ class SimpleGraphBuilder:
771
773
  phase_complete=True,
772
774
  )
773
775
 
774
- def _process_files(self) -> None:
776
+ async def _process_files(self) -> None:
775
777
  """Second pass: Process files and extract definitions."""
776
778
  # First pass: Count total files
777
779
  total_files = 0
@@ -807,7 +809,7 @@ class SimpleGraphBuilder:
807
809
  lang_config = get_language_config(ext)
808
810
 
809
811
  if lang_config and lang_config.name in self.parsers:
810
- self._process_single_file(filepath, lang_config.name)
812
+ await self._process_single_file(filepath, lang_config.name)
811
813
  file_count += 1
812
814
 
813
815
  # Report progress after each file
@@ -832,7 +834,7 @@ class SimpleGraphBuilder:
832
834
  phase_complete=True,
833
835
  )
834
836
 
835
- def _process_single_file(self, filepath: Path, language: str) -> None:
837
+ async def _process_single_file(self, filepath: Path, language: str) -> None:
836
838
  """Process a single file."""
837
839
  relative_path = filepath.relative_to(self.repo_path)
838
840
  relative_path_str = str(relative_path).replace(os.sep, "/")
@@ -873,8 +875,8 @@ class SimpleGraphBuilder:
873
875
 
874
876
  # Parse file
875
877
  try:
876
- with open(filepath, "rb") as f:
877
- content = f.read()
878
+ async with aiofiles.open(filepath, "rb") as f:
879
+ content = await f.read()
878
880
 
879
881
  parser = self.parsers[language]
880
882
  tree = parser.parse(content)
@@ -1636,7 +1638,7 @@ class CodebaseIngestor:
1636
1638
  )
1637
1639
  if self.project_name:
1638
1640
  builder.project_name = self.project_name
1639
- builder.run()
1641
+ asyncio.run(builder.run())
1640
1642
 
1641
1643
  logger.info(f"Graph successfully created at: {self.db_path}")
1642
1644
 
@@ -769,7 +769,7 @@ class CodebaseGraphManager:
769
769
 
770
770
  lang_config = get_language_config(full_path.suffix)
771
771
  if lang_config and lang_config.name in parsers:
772
- builder._process_single_file(full_path, lang_config.name)
772
+ await builder._process_single_file(full_path, lang_config.name)
773
773
  stats["nodes_modified"] += 1 # Approximate
774
774
 
775
775
  # Process additions
@@ -784,7 +784,7 @@ class CodebaseGraphManager:
784
784
 
785
785
  lang_config = get_language_config(full_path.suffix)
786
786
  if lang_config and lang_config.name in parsers:
787
- builder._process_single_file(full_path, lang_config.name)
787
+ await builder._process_single_file(full_path, lang_config.name)
788
788
  stats["nodes_added"] += 1 # Approximate
789
789
 
790
790
  # Flush all pending operations
@@ -1751,7 +1751,7 @@ class CodebaseGraphManager:
1751
1751
  )
1752
1752
 
1753
1753
  # Build the graph
1754
- builder.run()
1754
+ asyncio.run(builder.run())
1755
1755
 
1756
1756
  # Run build in thread pool
1757
1757
  await anyio.to_thread.run_sync(_build_graph)
@@ -34,7 +34,7 @@ async def llm_cypher_prompt(
34
34
  Returns:
35
35
  CypherGenerationResponse with cypher_query, can_generate flag, and reason if not
36
36
  """
37
- model_config = get_provider_model()
37
+ model_config = await get_provider_model()
38
38
 
39
39
  # Create an agent with structured output for Cypher generation
40
40
  cypher_agent = Agent(
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]
@@ -50,10 +50,12 @@ def setup_sentry_observability() -> bool:
50
50
 
51
51
  # Set user context with anonymous shotgun instance ID from config
52
52
  try:
53
+ import asyncio
54
+
53
55
  from shotgun.agents.config import get_config_manager
54
56
 
55
57
  config_manager = get_config_manager()
56
- shotgun_instance_id = config_manager.get_shotgun_instance_id()
58
+ shotgun_instance_id = asyncio.run(config_manager.get_shotgun_instance_id())
57
59
  sentry_sdk.set_user({"id": shotgun_instance_id})
58
60
  logger.debug("Sentry user context set with anonymous ID")
59
61
  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
@@ -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
112
117
 
113
- if not self.check_local_shotgun_directory_exists():
114
- if isinstance(self.screen, DirectorySetupScreen):
118
+ self.push_screen(
119
+ DirectorySetupScreen(),
120
+ callback=lambda _arg: self.refresh_startup_screen(),
121
+ )
122
+ return
123
+
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
163
+
164
+ self.push_screen(chat_screen)
155
165
 
156
- self.push_screen(chat_screen)
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()