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
@@ -97,7 +97,7 @@ class ProviderConfigScreen(Screen[None]):
97
97
  "Don't have an API Key? Use these links to get one: [OpenAI](https://platform.openai.com/api-keys) | [Anthropic](https://console.anthropic.com) | [Google Gemini](https://aistudio.google.com)",
98
98
  id="provider-links",
99
99
  )
100
- yield ListView(*self._build_provider_items_sync(), id="provider-list")
100
+ yield ListView(*self._build_provider_items(), id="provider-list")
101
101
  yield Input(
102
102
  placeholder=self._input_placeholder(self.selected_provider),
103
103
  password=True,
@@ -110,6 +110,8 @@ class ProviderConfigScreen(Screen[None]):
110
110
  yield Button("Done \\[ESC]", id="done")
111
111
 
112
112
  def on_mount(self) -> None:
113
+ self.refresh_provider_status()
114
+ self._update_done_button_visibility()
113
115
  list_view = self.query_one(ListView)
114
116
  if list_view.children:
115
117
  list_view.index = 0
@@ -119,20 +121,13 @@ class ProviderConfigScreen(Screen[None]):
119
121
  self.query_one("#authenticate", Button).display = False
120
122
  self.set_focus(self.query_one("#api-key", Input))
121
123
 
122
- # Refresh UI asynchronously
123
- self.run_worker(self._refresh_ui(), exclusive=False)
124
-
125
124
  def on_screenresume(self) -> None:
126
125
  """Refresh provider status when screen is resumed.
127
126
 
128
127
  This ensures the UI reflects any provider changes made elsewhere.
129
128
  """
130
- self.run_worker(self._refresh_ui(), exclusive=False)
131
-
132
- async def _refresh_ui(self) -> None:
133
- """Refresh provider status and button visibility."""
134
- await self.refresh_provider_status()
135
- await self._update_done_button_visibility()
129
+ self.refresh_provider_status()
130
+ self._update_done_button_visibility()
136
131
 
137
132
  def action_done(self) -> None:
138
133
  self.dismiss()
@@ -175,11 +170,7 @@ class ProviderConfigScreen(Screen[None]):
175
170
  if not self.is_mounted:
176
171
  return
177
172
 
178
- # Show/hide UI elements based on provider type asynchronously
179
- self.run_worker(self._update_provider_ui(provider), exclusive=False)
180
-
181
- async def _update_provider_ui(self, provider: ProviderType) -> None:
182
- """Update UI elements based on selected provider."""
173
+ # Show/hide UI elements based on provider type
183
174
  is_shotgun = provider == "shotgun"
184
175
 
185
176
  input_widget = self.query_one("#api-key", Input)
@@ -192,7 +183,7 @@ class ProviderConfigScreen(Screen[None]):
192
183
  save_button.display = False
193
184
 
194
185
  # Only show Authenticate button if shotgun is NOT already configured
195
- if await self._has_provider_key("shotgun"):
186
+ if self._has_provider_key("shotgun"):
196
187
  auth_button.display = False
197
188
  else:
198
189
  auth_button.display = True
@@ -209,29 +200,22 @@ class ProviderConfigScreen(Screen[None]):
209
200
  app = cast("ShotgunApp", self.app)
210
201
  return app.config_manager
211
202
 
212
- async def refresh_provider_status(self) -> None:
203
+ def refresh_provider_status(self) -> None:
213
204
  """Update the list view entries to reflect configured providers."""
214
205
  for provider_id in get_configurable_providers():
215
206
  label = self.query_one(f"#label-{provider_id}", Label)
216
- label.update(await self._provider_label(provider_id))
207
+ label.update(self._provider_label(provider_id))
217
208
 
218
- async def _update_done_button_visibility(self) -> None:
209
+ def _update_done_button_visibility(self) -> None:
219
210
  """Show/hide Done button based on whether any provider keys are configured."""
220
211
  done_button = self.query_one("#done", Button)
221
- has_keys = await self.config_manager.has_any_provider_key()
212
+ has_keys = self.config_manager.has_any_provider_key()
222
213
  done_button.display = has_keys
223
214
 
224
- def _build_provider_items_sync(self) -> list[ListItem]:
225
- """Build provider items synchronously for compose().
226
-
227
- Labels will be populated with status asynchronously in on_mount().
228
- """
215
+ def _build_provider_items(self) -> list[ListItem]:
229
216
  items: list[ListItem] = []
230
217
  for provider_id in get_configurable_providers():
231
- # Create labels with placeholder text - will be updated in on_mount()
232
- label = Label(
233
- self._provider_display_name(provider_id), id=f"label-{provider_id}"
234
- )
218
+ label = Label(self._provider_label(provider_id), id=f"label-{provider_id}")
235
219
  items.append(ListItem(label, id=f"provider-{provider_id}"))
236
220
  return items
237
221
 
@@ -241,10 +225,11 @@ class ProviderConfigScreen(Screen[None]):
241
225
  provider_id = item.id.removeprefix("provider-")
242
226
  return provider_id if provider_id in get_configurable_providers() else None
243
227
 
244
- async def _provider_label(self, provider_id: str) -> str:
228
+ def _provider_label(self, provider_id: str) -> str:
245
229
  display = self._provider_display_name(provider_id)
246
- has_key = await self._has_provider_key(provider_id)
247
- status = "Configured" if has_key else "Not configured"
230
+ status = (
231
+ "Configured" if self._has_provider_key(provider_id) else "Not configured"
232
+ )
248
233
  return f"{display} · {status}"
249
234
 
250
235
  def _provider_display_name(self, provider_id: str) -> str:
@@ -259,25 +244,21 @@ class ProviderConfigScreen(Screen[None]):
259
244
  def _input_placeholder(self, provider_id: str) -> str:
260
245
  return f"{self._provider_display_name(provider_id)} API key"
261
246
 
262
- async def _has_provider_key(self, provider_id: str) -> bool:
247
+ def _has_provider_key(self, provider_id: str) -> bool:
263
248
  """Check if provider has a configured API key."""
264
249
  if provider_id == "shotgun":
265
250
  # Check shotgun key directly
266
- config = await self.config_manager.load()
251
+ config = self.config_manager.load()
267
252
  return self.config_manager._provider_has_api_key(config.shotgun)
268
253
  else:
269
254
  # Check LLM provider key
270
255
  try:
271
256
  provider = ProviderType(provider_id)
272
- return await self.config_manager.has_provider_key(provider)
257
+ return self.config_manager.has_provider_key(provider)
273
258
  except ValueError:
274
259
  return False
275
260
 
276
261
  def _save_api_key(self) -> None:
277
- self.run_worker(self._do_save_api_key(), exclusive=True)
278
-
279
- async def _do_save_api_key(self) -> None:
280
- """Async implementation of API key saving."""
281
262
  input_widget = self.query_one("#api-key", Input)
282
263
  api_key = input_widget.value.strip()
283
264
 
@@ -286,7 +267,7 @@ class ProviderConfigScreen(Screen[None]):
286
267
  return
287
268
 
288
269
  try:
289
- await self.config_manager.update_provider(
270
+ self.config_manager.update_provider(
290
271
  self.selected_provider,
291
272
  api_key=api_key,
292
273
  )
@@ -295,25 +276,21 @@ class ProviderConfigScreen(Screen[None]):
295
276
  return
296
277
 
297
278
  input_widget.value = ""
298
- await self.refresh_provider_status()
299
- await self._update_done_button_visibility()
279
+ self.refresh_provider_status()
280
+ self._update_done_button_visibility()
300
281
  self.notify(
301
282
  f"Saved API key for {self._provider_display_name(self.selected_provider)}."
302
283
  )
303
284
 
304
285
  def _clear_api_key(self) -> None:
305
- self.run_worker(self._do_clear_api_key(), exclusive=True)
306
-
307
- async def _do_clear_api_key(self) -> None:
308
- """Async implementation of API key clearing."""
309
286
  try:
310
- await self.config_manager.clear_provider_key(self.selected_provider)
287
+ self.config_manager.clear_provider_key(self.selected_provider)
311
288
  except Exception as exc: # pragma: no cover - defensive; textual path
312
289
  self.notify(f"Failed to clear key: {exc}", severity="error")
313
290
  return
314
291
 
315
- await self.refresh_provider_status()
316
- await self._update_done_button_visibility()
292
+ self.refresh_provider_status()
293
+ self._update_done_button_visibility()
317
294
  self.query_one("#api-key", Input).value = ""
318
295
 
319
296
  # If we just cleared shotgun, show the Authenticate button
@@ -334,5 +311,5 @@ class ProviderConfigScreen(Screen[None]):
334
311
 
335
312
  # Refresh provider status after auth completes
336
313
  if result:
337
- await self.refresh_provider_status()
314
+ self.refresh_provider_status()
338
315
  # Notify handled by auth screen
@@ -135,7 +135,7 @@ class ShotgunAuthScreen(Screen[bool]):
135
135
  """Start the authentication flow."""
136
136
  try:
137
137
  # Get shotgun instance ID from config
138
- shotgun_instance_id = await self.config_manager.get_shotgun_instance_id()
138
+ shotgun_instance_id = self.config_manager.get_shotgun_instance_id()
139
139
  logger.info("Starting auth flow with instance ID: %s", shotgun_instance_id)
140
140
 
141
141
  # Update status
@@ -215,7 +215,7 @@ class ShotgunAuthScreen(Screen[bool]):
215
215
  logger.info("Authentication completed successfully")
216
216
 
217
217
  if status_response.litellm_key and status_response.supabase_key:
218
- await self.config_manager.update_shotgun_account(
218
+ self.config_manager.update_shotgun_account(
219
219
  api_key=status_response.litellm_key,
220
220
  supabase_jwt=status_response.supabase_key,
221
221
  )
@@ -136,17 +136,14 @@ class WelcomeScreen(Screen[None]):
136
136
 
137
137
  def on_mount(self) -> None:
138
138
  """Focus the first button on mount."""
139
- self.query_one("#shotgun-button", Button).focus()
140
- # Update BYOK button text asynchronously
141
- self.run_worker(self._update_byok_button_text(), exclusive=False)
142
-
143
- async def _update_byok_button_text(self) -> None:
144
- """Update BYOK button text based on whether user has existing providers."""
139
+ # Update BYOK button text based on whether user has existing providers
145
140
  byok_button = self.query_one("#byok-button", Button)
146
141
  app = cast("ShotgunApp", self.app)
147
- if await app.config_manager.has_any_provider_key():
142
+ if app.config_manager.has_any_provider_key():
148
143
  byok_button.label = "I'll stick with my BYOK setup"
149
144
 
145
+ self.query_one("#shotgun-button", Button).focus()
146
+
150
147
  @on(Button.Pressed, "#shotgun-button")
151
148
  def _on_shotgun_pressed(self) -> None:
152
149
  """Handle Shotgun Account button press."""
@@ -159,12 +156,12 @@ class WelcomeScreen(Screen[None]):
159
156
 
160
157
  async def _start_byok_config(self) -> None:
161
158
  """Launch BYOK provider configuration flow."""
162
- await self._mark_welcome_shown()
159
+ self._mark_welcome_shown()
163
160
 
164
161
  app = cast("ShotgunApp", self.app)
165
162
 
166
163
  # If user already has providers, just dismiss and continue to chat
167
- if await app.config_manager.has_any_provider_key():
164
+ if app.config_manager.has_any_provider_key():
168
165
  self.dismiss()
169
166
  return
170
167
 
@@ -174,7 +171,7 @@ class WelcomeScreen(Screen[None]):
174
171
  await self.app.push_screen_wait(ProviderConfigScreen())
175
172
 
176
173
  # Dismiss welcome screen after config if providers are now configured
177
- if await app.config_manager.has_any_provider_key():
174
+ if app.config_manager.has_any_provider_key():
178
175
  self.dismiss()
179
176
 
180
177
  async def _start_shotgun_auth(self) -> None:
@@ -182,7 +179,7 @@ class WelcomeScreen(Screen[None]):
182
179
  from .shotgun_auth import ShotgunAuthScreen
183
180
 
184
181
  # Mark welcome screen as shown before auth
185
- await self._mark_welcome_shown()
182
+ self._mark_welcome_shown()
186
183
 
187
184
  # Push the auth screen and wait for result
188
185
  await self.app.push_screen_wait(ShotgunAuthScreen())
@@ -190,9 +187,9 @@ class WelcomeScreen(Screen[None]):
190
187
  # Dismiss welcome screen after auth
191
188
  self.dismiss()
192
189
 
193
- async def _mark_welcome_shown(self) -> None:
190
+ def _mark_welcome_shown(self) -> None:
194
191
  """Mark the welcome screen as shown in config."""
195
192
  app = cast("ShotgunApp", self.app)
196
- config = await app.config_manager.load()
193
+ config = app.config_manager.load()
197
194
  config.shown_welcome_screen = True
198
- await app.config_manager.save(config)
195
+ app.config_manager.save(config)
@@ -8,8 +8,6 @@ import logging
8
8
  from pathlib import Path
9
9
  from typing import TYPE_CHECKING
10
10
 
11
- import aiofiles.os
12
-
13
11
  from shotgun.agents.conversation_history import ConversationHistory, ConversationState
14
12
  from shotgun.agents.conversation_manager import ConversationManager
15
13
  from shotgun.agents.models import AgentType
@@ -50,7 +48,7 @@ class ConversationService:
50
48
  else:
51
49
  self.conversation_manager = ConversationManager()
52
50
 
53
- async def save_conversation(self, agent_manager: "AgentManager") -> bool:
51
+ def save_conversation(self, agent_manager: "AgentManager") -> bool:
54
52
  """Save the current conversation to persistent storage.
55
53
 
56
54
  Args:
@@ -70,15 +68,15 @@ class ConversationService:
70
68
  conversation.set_agent_messages(state.agent_messages)
71
69
  conversation.set_ui_messages(state.ui_messages)
72
70
 
73
- # Save to file (now async)
74
- await self.conversation_manager.save(conversation)
71
+ # Save to file
72
+ self.conversation_manager.save(conversation)
75
73
  logger.debug("Conversation saved successfully")
76
74
  return True
77
75
  except Exception as e:
78
76
  logger.exception(f"Failed to save conversation: {e}")
79
77
  return False
80
78
 
81
- async def load_conversation(self) -> ConversationHistory | None:
79
+ def load_conversation(self) -> ConversationHistory | None:
82
80
  """Load conversation from persistent storage.
83
81
 
84
82
  Returns:
@@ -86,7 +84,7 @@ class ConversationService:
86
84
  or if loading failed.
87
85
  """
88
86
  try:
89
- conversation = await self.conversation_manager.load()
87
+ conversation = self.conversation_manager.load()
90
88
  if conversation is None:
91
89
  logger.debug("No conversation file found")
92
90
  return None
@@ -97,7 +95,7 @@ class ConversationService:
97
95
  logger.exception(f"Failed to load conversation: {e}")
98
96
  return None
99
97
 
100
- async def check_for_corrupted_conversation(self) -> bool:
98
+ def check_for_corrupted_conversation(self) -> bool:
101
99
  """Check if a conversation backup exists (indicating corruption).
102
100
 
103
101
  Returns:
@@ -106,9 +104,9 @@ class ConversationService:
106
104
  backup_path = self.conversation_manager.conversation_path.with_suffix(
107
105
  ".json.backup"
108
106
  )
109
- return await aiofiles.os.path.exists(str(backup_path))
107
+ return backup_path.exists()
110
108
 
111
- async def restore_conversation(
109
+ def restore_conversation(
112
110
  self,
113
111
  agent_manager: "AgentManager",
114
112
  usage_manager: "SessionUsageManager | None" = None,
@@ -125,11 +123,11 @@ class ConversationService:
125
123
  - error_message: Error message if restoration failed, None otherwise
126
124
  - restored_agent_type: The agent type from restored conversation
127
125
  """
128
- conversation = await self.load_conversation()
126
+ conversation = self.load_conversation()
129
127
 
130
128
  if conversation is None:
131
129
  # Check for corruption
132
- if await self.check_for_corrupted_conversation():
130
+ if self.check_for_corrupted_conversation():
133
131
  return (
134
132
  False,
135
133
  "⚠️ Previous session was corrupted and has been backed up. Starting fresh conversation.",
@@ -153,7 +151,7 @@ class ConversationService:
153
151
 
154
152
  # Restore usage state if manager provided
155
153
  if usage_manager:
156
- await usage_manager.restore_usage_state()
154
+ usage_manager.restore_usage_state()
157
155
 
158
156
  restored_type = AgentType(conversation.last_agent_model)
159
157
  logger.info(f"Conversation restored successfully (mode: {restored_type})")
@@ -167,7 +165,7 @@ class ConversationService:
167
165
  None,
168
166
  )
169
167
 
170
- async def clear_conversation(self) -> bool:
168
+ def clear_conversation(self) -> bool:
171
169
  """Clear the saved conversation file.
172
170
 
173
171
  Returns:
@@ -175,8 +173,8 @@ class ConversationService:
175
173
  """
176
174
  try:
177
175
  conversation_path = self.conversation_manager.conversation_path
178
- if await aiofiles.os.path.exists(str(conversation_path)):
179
- await aiofiles.os.unlink(str(conversation_path))
176
+ if conversation_path.exists():
177
+ conversation_path.unlink()
180
178
  logger.info("Conversation file cleared")
181
179
  return True
182
180
  except Exception as e:
@@ -3,8 +3,6 @@
3
3
  import random
4
4
  from pathlib import Path
5
5
 
6
- import aiofiles
7
-
8
6
  from shotgun.agents.models import AgentType
9
7
  from shotgun.utils.file_system_utils import get_shotgun_base_path
10
8
 
@@ -32,7 +30,7 @@ class ModeProgressChecker:
32
30
  """
33
31
  self.base_path = base_path or get_shotgun_base_path()
34
32
 
35
- async def has_mode_content(self, mode: AgentType) -> bool:
33
+ def has_mode_content(self, mode: AgentType) -> bool:
36
34
  """Check if a mode has meaningful content.
37
35
 
38
36
  Args:
@@ -54,8 +52,7 @@ class ModeProgressChecker:
54
52
  for item in export_path.glob("*"):
55
53
  if item.is_file() and not item.name.startswith("."):
56
54
  try:
57
- async with aiofiles.open(item, encoding="utf-8") as f:
58
- content = await f.read()
55
+ content = item.read_text(encoding="utf-8")
59
56
  if len(content.strip()) > self.MIN_CONTENT_SIZE:
60
57
  return True
61
58
  except (OSError, UnicodeDecodeError):
@@ -68,16 +65,13 @@ class ModeProgressChecker:
68
65
  return False
69
66
 
70
67
  try:
71
- async with aiofiles.open(file_path, encoding="utf-8") as f:
72
- content = await f.read()
68
+ content = file_path.read_text(encoding="utf-8")
73
69
  # Check if file has meaningful content
74
70
  return len(content.strip()) > self.MIN_CONTENT_SIZE
75
71
  except (OSError, UnicodeDecodeError):
76
72
  return False
77
73
 
78
- async def get_next_suggested_mode(
79
- self, current_mode: AgentType
80
- ) -> AgentType | None:
74
+ def get_next_suggested_mode(self, current_mode: AgentType) -> AgentType | None:
81
75
  """Get the next suggested mode based on current progress.
82
76
 
83
77
  Args:
@@ -100,7 +94,7 @@ class ModeProgressChecker:
100
94
  return None
101
95
 
102
96
  # Check if current mode has content
103
- if not await self.has_mode_content(current_mode):
97
+ if not self.has_mode_content(current_mode):
104
98
  # Current mode is empty, no suggestion for next mode
105
99
  return None
106
100
 
@@ -228,9 +222,8 @@ class PlaceholderHints:
228
222
  if current_mode not in self.HINTS:
229
223
  return f"Enter your {current_mode.value} mode prompt (SHIFT+TAB to switch modes)"
230
224
 
231
- # For placeholder text, we default to "no content" state (initial hints)
232
- # This avoids async file system checks in the UI rendering path
233
- has_content = False
225
+ # Determine if mode has content
226
+ has_content = self.progress_checker.has_mode_content(current_mode)
234
227
 
235
228
  # Get hint variations for this mode and state
236
229
  hints_list = self.HINTS[current_mode][has_content]
@@ -245,18 +245,3 @@ class WidgetCoordinator:
245
245
  spinner.text = text
246
246
  except Exception as e:
247
247
  logger.exception(f"Failed to update spinner text: {e}")
248
-
249
- def set_context_streaming(self, streaming: bool) -> None:
250
- """Enable or disable context indicator streaming animation.
251
-
252
- Args:
253
- streaming: Whether to show streaming animation.
254
- """
255
- if not self.screen.is_mounted:
256
- return
257
-
258
- try:
259
- context_indicator = self.screen.query_one(ContextIndicator)
260
- context_indicator.set_streaming(streaming)
261
- except Exception as e:
262
- logger.exception(f"Failed to set context streaming: {e}")
@@ -2,8 +2,6 @@
2
2
 
3
3
  from pathlib import Path
4
4
 
5
- import aiofiles
6
-
7
5
  from shotgun.settings import settings
8
6
 
9
7
 
@@ -37,20 +35,3 @@ def ensure_shotgun_directory_exists() -> Path:
37
35
  shotgun_dir.mkdir(exist_ok=True)
38
36
  # Note: Removed logger to avoid circular dependency with logging_config
39
37
  return shotgun_dir
40
-
41
-
42
- async def async_copy_file(src: Path, dst: Path) -> None:
43
- """Asynchronously copy a file from src to dst.
44
-
45
- Args:
46
- src: Source file path
47
- dst: Destination file path
48
-
49
- Raises:
50
- FileNotFoundError: If source file doesn't exist
51
- OSError: If copy operation fails
52
- """
53
- async with aiofiles.open(src, "rb") as src_file:
54
- content = await src_file.read()
55
- async with aiofiles.open(dst, "wb") as dst_file:
56
- await dst_file.write(content)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shotgun-sh
3
- Version: 0.2.11
3
+ Version: 0.2.11.dev2
4
4
  Summary: AI-powered research, planning, and task management CLI tool
5
5
  Project-URL: Homepage, https://shotgun.sh/
6
6
  Project-URL: Repository, https://github.com/shotgun-sh/shotgun
@@ -21,7 +21,6 @@ Classifier: Programming Language :: Python :: 3.12
21
21
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
22
  Classifier: Topic :: Utilities
23
23
  Requires-Python: >=3.11
24
- Requires-Dist: aiofiles>=24.0.0
25
24
  Requires-Dist: anthropic>=0.39.0
26
25
  Requires-Dist: dependency-injector>=4.41.0
27
26
  Requires-Dist: genai-prices>=0.0.27