shotgun-sh 0.2.1.dev1__py3-none-any.whl → 0.2.1.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.

@@ -115,12 +115,12 @@ class AgentManager(Widget):
115
115
  super().__init__()
116
116
  self.display = False
117
117
 
118
+ if deps is None:
119
+ raise ValueError("AgentDeps must be provided to AgentManager")
120
+
118
121
  # Use provided deps or create default with interactive mode
119
122
  self.deps = deps
120
123
 
121
- if self.deps is None:
122
- raise ValueError("AgentDeps must be provided to AgentManager")
123
-
124
124
  # Create AgentRuntimeOptions from deps for agent creation
125
125
  agent_runtime_options = AgentRuntimeOptions(
126
126
  interactive_mode=self.deps.interactive_mode,
@@ -269,6 +269,7 @@ class AgentManager(Widget):
269
269
  Returns:
270
270
  The agent run result.
271
271
  """
272
+ logger.info(f"Running agent {self._current_agent_type.value}")
272
273
  # Use merged deps (shared state + agent-specific system prompt) if not provided
273
274
  if deps is None:
274
275
  deps = self._create_merged_deps(self._current_agent_type)
@@ -395,6 +396,10 @@ class AgentManager(Widget):
395
396
  # Apply compaction to persistent message history to prevent cascading growth
396
397
  all_messages = result.all_messages()
397
398
  self.message_history = await apply_persistent_compaction(all_messages, deps)
399
+ usage = result.usage()
400
+ deps.usage_manager.add_usage(
401
+ usage, model_name=deps.llm_model.name, provider=deps.llm_model.provider
402
+ )
398
403
 
399
404
  # Log file operations summary if any files were modified
400
405
  file_operations = deps.file_tracker.operations.copy()
@@ -641,6 +646,9 @@ class AgentManager(Widget):
641
646
  filtered_messages.append(msg)
642
647
  return filtered_messages
643
648
 
649
+ def get_usage_hint(self) -> str | None:
650
+ return self.deps.usage_manager.build_usage_hint()
651
+
644
652
  def get_conversation_state(self) -> "ConversationState":
645
653
  """Get the current conversation state.
646
654
 
@@ -1,7 +1,6 @@
1
1
  """Configuration manager for Shotgun CLI."""
2
2
 
3
3
  import json
4
- import os
5
4
  import uuid
6
5
  from pathlib import Path
7
6
  from typing import Any
@@ -12,10 +11,7 @@ from shotgun.logging_config import get_logger
12
11
  from shotgun.utils import get_shotgun_home
13
12
 
14
13
  from .constants import (
15
- ANTHROPIC_API_KEY_ENV,
16
14
  API_KEY_FIELD,
17
- GEMINI_API_KEY_ENV,
18
- OPENAI_API_KEY_ENV,
19
15
  ConfigSection,
20
16
  )
21
17
  from .models import (
@@ -113,7 +109,7 @@ class ConfigManager:
113
109
  # Find default model for this provider
114
110
  provider_models = {
115
111
  ProviderType.OPENAI: ModelName.GPT_5,
116
- ProviderType.ANTHROPIC: ModelName.CLAUDE_OPUS_4_1,
112
+ ProviderType.ANTHROPIC: ModelName.CLAUDE_SONNET_4_5,
117
113
  ProviderType.GOOGLE: ModelName.GEMINI_2_5_PRO,
118
114
  }
119
115
 
@@ -214,7 +210,7 @@ class ConfigManager:
214
210
 
215
211
  provider_models = {
216
212
  ProviderType.OPENAI: ModelName.GPT_5,
217
- ProviderType.ANTHROPIC: ModelName.CLAUDE_OPUS_4_1,
213
+ ProviderType.ANTHROPIC: ModelName.CLAUDE_SONNET_4_5,
218
214
  ProviderType.GOOGLE: ModelName.GEMINI_2_5_PRO,
219
215
  }
220
216
  if provider_enum in provider_models:
@@ -245,25 +241,13 @@ class ConfigManager:
245
241
  def has_provider_key(self, provider: ProviderType | str) -> bool:
246
242
  """Check if the given provider has a non-empty API key configured.
247
243
 
248
- This checks both the configuration file and environment variables.
244
+ This checks only the configuration file.
249
245
  """
250
246
  config = self.load()
251
247
  provider_enum = self._ensure_provider_enum(provider)
252
248
  provider_config = self._get_provider_config(config, provider_enum)
253
249
 
254
- # Check config first
255
- if self._provider_has_api_key(provider_config):
256
- return True
257
-
258
- # Check environment variable
259
- if provider_enum == ProviderType.OPENAI:
260
- return bool(os.getenv(OPENAI_API_KEY_ENV))
261
- elif provider_enum == ProviderType.ANTHROPIC:
262
- return bool(os.getenv(ANTHROPIC_API_KEY_ENV))
263
- elif provider_enum == ProviderType.GOOGLE:
264
- return bool(os.getenv(GEMINI_API_KEY_ENV))
265
-
266
- return False
250
+ return self._provider_has_api_key(provider_config)
267
251
 
268
252
  def has_any_provider_key(self) -> bool:
269
253
  """Determine whether any provider has a configured API key."""
@@ -27,6 +27,7 @@ class ModelName(StrEnum):
27
27
  GPT_5 = "gpt-5"
28
28
  GPT_5_MINI = "gpt-5-mini"
29
29
  CLAUDE_OPUS_4_1 = "claude-opus-4-1"
30
+ CLAUDE_SONNET_4_5 = "claude-sonnet-4-5"
30
31
  GEMINI_2_5_PRO = "gemini-2.5-pro"
31
32
  GEMINI_2_5_FLASH = "gemini-2.5-flash"
32
33
 
@@ -102,6 +103,13 @@ MODEL_SPECS: dict[ModelName, ModelSpec] = {
102
103
  max_output_tokens=32_000,
103
104
  litellm_proxy_model_name="anthropic/claude-opus-4-1",
104
105
  ),
106
+ ModelName.CLAUDE_SONNET_4_5: ModelSpec(
107
+ name=ModelName.CLAUDE_SONNET_4_5,
108
+ provider=ProviderType.ANTHROPIC,
109
+ max_input_tokens=200_000,
110
+ max_output_tokens=16_000,
111
+ litellm_proxy_model_name="anthropic/claude-sonnet-4-5",
112
+ ),
105
113
  ModelName.GEMINI_2_5_PRO: ModelSpec(
106
114
  name=ModelName.GEMINI_2_5_PRO,
107
115
  provider=ProviderType.GOOGLE,
@@ -1,7 +1,5 @@
1
1
  """Provider management for LLM configuration."""
2
2
 
3
- import os
4
-
5
3
  from pydantic import SecretStr
6
4
  from pydantic_ai.models import Model
7
5
  from pydantic_ai.models.anthropic import AnthropicModel, AnthropicModelSettings
@@ -15,12 +13,6 @@ from pydantic_ai.settings import ModelSettings
15
13
  from shotgun.llm_proxy import create_litellm_provider
16
14
  from shotgun.logging_config import get_logger
17
15
 
18
- from .constants import (
19
- ANTHROPIC_API_KEY_ENV,
20
- GEMINI_API_KEY_ENV,
21
- OPENAI_API_KEY_ENV,
22
- SHOTGUN_API_KEY_ENV,
23
- )
24
16
  from .manager import get_config_manager
25
17
  from .models import (
26
18
  MODEL_SPECS,
@@ -150,10 +142,10 @@ def get_provider_model(
150
142
  config = config_manager.load()
151
143
 
152
144
  # Priority 1: Check if Shotgun key exists - if so, use it for ANY model
153
- shotgun_api_key = _get_api_key(config.shotgun.api_key, SHOTGUN_API_KEY_ENV)
145
+ shotgun_api_key = _get_api_key(config.shotgun.api_key)
154
146
  if shotgun_api_key:
155
- # Use selected model or default to claude-opus-4-1
156
- model_name = config.selected_model or ModelName.CLAUDE_OPUS_4_1
147
+ # Use selected model or default to claude-sonnet-4-5
148
+ model_name = config.selected_model or ModelName.CLAUDE_SONNET_4_5
157
149
  if model_name not in MODEL_SPECS:
158
150
  raise ValueError(f"Model '{model_name.value}' not found")
159
151
  spec = MODEL_SPECS[model_name]
@@ -202,11 +194,9 @@ def get_provider_model(
202
194
  requested_model = None # Will use provider's default model
203
195
 
204
196
  if provider_enum == ProviderType.OPENAI:
205
- api_key = _get_api_key(config.openai.api_key, OPENAI_API_KEY_ENV)
197
+ api_key = _get_api_key(config.openai.api_key)
206
198
  if not api_key:
207
- raise ValueError(
208
- f"OpenAI API key not configured. Set via environment variable {OPENAI_API_KEY_ENV} or config."
209
- )
199
+ raise ValueError("OpenAI API key not configured. Set via config.")
210
200
 
211
201
  # Use requested model or default to gpt-5
212
202
  model_name = requested_model if requested_model else ModelName.GPT_5
@@ -225,14 +215,12 @@ def get_provider_model(
225
215
  )
226
216
 
227
217
  elif provider_enum == ProviderType.ANTHROPIC:
228
- api_key = _get_api_key(config.anthropic.api_key, ANTHROPIC_API_KEY_ENV)
218
+ api_key = _get_api_key(config.anthropic.api_key)
229
219
  if not api_key:
230
- raise ValueError(
231
- f"Anthropic API key not configured. Set via environment variable {ANTHROPIC_API_KEY_ENV} or config."
232
- )
220
+ raise ValueError("Anthropic API key not configured. Set via config.")
233
221
 
234
- # Use requested model or default to claude-opus-4-1
235
- model_name = requested_model if requested_model else ModelName.CLAUDE_OPUS_4_1
222
+ # Use requested model or default to claude-sonnet-4-5
223
+ model_name = requested_model if requested_model else ModelName.CLAUDE_SONNET_4_5
236
224
  if model_name not in MODEL_SPECS:
237
225
  raise ValueError(f"Model '{model_name.value}' not found")
238
226
  spec = MODEL_SPECS[model_name]
@@ -248,11 +236,9 @@ def get_provider_model(
248
236
  )
249
237
 
250
238
  elif provider_enum == ProviderType.GOOGLE:
251
- api_key = _get_api_key(config.google.api_key, GEMINI_API_KEY_ENV)
239
+ api_key = _get_api_key(config.google.api_key)
252
240
  if not api_key:
253
- raise ValueError(
254
- f"Gemini API key not configured. Set via environment variable {GEMINI_API_KEY_ENV} or config."
255
- )
241
+ raise ValueError("Gemini API key not configured. Set via config.")
256
242
 
257
243
  # Use requested model or default to gemini-2.5-pro
258
244
  model_name = requested_model if requested_model else ModelName.GEMINI_2_5_PRO
@@ -285,20 +271,19 @@ def _has_provider_key(config: "ShotgunConfig", provider: ProviderType) -> bool:
285
271
  True if provider has a configured API key
286
272
  """
287
273
  if provider == ProviderType.OPENAI:
288
- return bool(_get_api_key(config.openai.api_key, OPENAI_API_KEY_ENV))
274
+ return bool(_get_api_key(config.openai.api_key))
289
275
  elif provider == ProviderType.ANTHROPIC:
290
- return bool(_get_api_key(config.anthropic.api_key, ANTHROPIC_API_KEY_ENV))
276
+ return bool(_get_api_key(config.anthropic.api_key))
291
277
  elif provider == ProviderType.GOOGLE:
292
- return bool(_get_api_key(config.google.api_key, GEMINI_API_KEY_ENV))
278
+ return bool(_get_api_key(config.google.api_key))
293
279
  return False
294
280
 
295
281
 
296
- def _get_api_key(config_key: SecretStr | None, env_var: str) -> str | None:
297
- """Get API key from config or environment variable.
282
+ def _get_api_key(config_key: SecretStr | None) -> str | None:
283
+ """Get API key from config.
298
284
 
299
285
  Args:
300
286
  config_key: API key from configuration
301
- env_var: Environment variable name to check
302
287
 
303
288
  Returns:
304
289
  API key string or None
@@ -306,4 +291,4 @@ def _get_api_key(config_key: SecretStr | None, env_var: str) -> str | None:
306
291
  if config_key is not None:
307
292
  return config_key.get_secret_value()
308
293
 
309
- return os.getenv(env_var)
294
+ return None
shotgun/agents/models.py CHANGED
@@ -11,6 +11,8 @@ from typing import TYPE_CHECKING
11
11
  from pydantic import BaseModel, ConfigDict, Field
12
12
  from pydantic_ai import RunContext
13
13
 
14
+ from shotgun.agents.usage_manager import SessionUsageManager, get_session_usage_manager
15
+
14
16
  from .config.models import ModelConfig
15
17
 
16
18
  if TYPE_CHECKING:
@@ -108,6 +110,11 @@ class AgentRuntimeOptions(BaseModel):
108
110
  description="Tasks for storing deferred tool results",
109
111
  )
110
112
 
113
+ usage_manager: SessionUsageManager = Field(
114
+ default_factory=get_session_usage_manager,
115
+ description="Usage manager for tracking usage",
116
+ )
117
+
111
118
 
112
119
  class FileOperationType(StrEnum):
113
120
  """Types of file operations that can be tracked."""
@@ -94,7 +94,6 @@ async def anthropic_web_search_tool(query: str) -> str:
94
94
 
95
95
  async def main() -> None:
96
96
  """Main function for testing the Anthropic web search tool."""
97
- import os
98
97
  import sys
99
98
 
100
99
  from shotgun.logging_config import setup_logger
@@ -119,15 +118,14 @@ async def main() -> None:
119
118
  print("=" * 60)
120
119
 
121
120
  # Check if API key is available
122
- if not (
123
- os.getenv("ANTHROPIC_API_KEY")
124
- or (
125
- callable(get_provider_model)
126
- and get_provider_model(ProviderType.ANTHROPIC).api_key
127
- )
128
- ):
129
- print(" Error: ANTHROPIC_API_KEY environment variable not set")
130
- print(" Please set it with: export ANTHROPIC_API_KEY=your_key_here")
121
+ try:
122
+ if callable(get_provider_model):
123
+ model_config = get_provider_model(ProviderType.ANTHROPIC)
124
+ if not model_config.api_key:
125
+ raise ValueError("No API key configured")
126
+ except (ValueError, Exception):
127
+ print("❌ Error: Anthropic API key not configured")
128
+ print(" Please set it in your config file")
131
129
  sys.exit(1)
132
130
 
133
131
  try:
@@ -0,0 +1,159 @@
1
+ import json
2
+ from collections import defaultdict
3
+ from dataclasses import dataclass
4
+ from datetime import datetime
5
+ from logging import getLogger
6
+ from pathlib import Path
7
+ from typing import TypeAlias
8
+
9
+ from genai_prices import calc_price
10
+ from pydantic import BaseModel, Field
11
+ from pydantic_ai import RunUsage
12
+
13
+ from shotgun.agents.config.models import ProviderType
14
+ from shotgun.utils import get_shotgun_home
15
+
16
+ logger = getLogger(__name__)
17
+ ModelName: TypeAlias = str
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class UsageSummaryEntry:
22
+ model_name: ModelName
23
+ provider: ProviderType
24
+ usage: RunUsage
25
+
26
+
27
+ class UsageLogEntry(BaseModel):
28
+ timestamp: datetime = Field(default_factory=datetime.now)
29
+ model_name: ModelName
30
+ usage: RunUsage
31
+ provider: ProviderType
32
+
33
+
34
+ class SessionUsage(BaseModel):
35
+ usage: RunUsage
36
+ log: list[UsageLogEntry]
37
+
38
+
39
+ class UsageState(BaseModel):
40
+ usage: dict[ModelName, RunUsage] = Field(default_factory=dict)
41
+ model_providers: dict[ModelName, ProviderType] = Field(default_factory=dict)
42
+ usage_log: list[UsageLogEntry] = Field(default_factory=list)
43
+
44
+
45
+ class SessionUsageManager:
46
+ def __init__(self) -> None:
47
+ self.usage: defaultdict[ModelName, RunUsage] = defaultdict(RunUsage)
48
+ self._model_providers: dict[ModelName, ProviderType] = {}
49
+ self._usage_log: list[UsageLogEntry] = []
50
+ self._usage_path: Path = get_shotgun_home() / "usage.json"
51
+ self.restore_usage_state()
52
+
53
+ def add_usage(
54
+ self, usage: RunUsage, *, model_name: ModelName, provider: ProviderType
55
+ ) -> None:
56
+ self.usage[model_name] += usage
57
+ self._model_providers[model_name] = provider
58
+ self._usage_log.append(
59
+ UsageLogEntry(model_name=model_name, usage=usage, provider=provider)
60
+ )
61
+ self.persist_usage_state()
62
+
63
+ def get_usage_report(self) -> dict[ModelName, RunUsage]:
64
+ return self.usage.copy()
65
+
66
+ def get_usage_breakdown(self) -> list[UsageSummaryEntry]:
67
+ breakdown: list[UsageSummaryEntry] = []
68
+ for model_name, usage in self.usage.items():
69
+ provider = self._model_providers.get(model_name)
70
+ if provider is None:
71
+ continue
72
+ breakdown.append(
73
+ UsageSummaryEntry(model_name=model_name, provider=provider, usage=usage)
74
+ )
75
+ breakdown.sort(key=lambda entry: entry.model_name.lower())
76
+ return breakdown
77
+
78
+ def build_usage_hint(self) -> str | None:
79
+ return format_usage_hint(self.get_usage_breakdown())
80
+
81
+ def persist_usage_state(self) -> None:
82
+ state = UsageState(
83
+ usage=dict(self.usage.items()),
84
+ model_providers=self._model_providers.copy(),
85
+ usage_log=self._usage_log.copy(),
86
+ )
87
+
88
+ try:
89
+ self._usage_path.parent.mkdir(parents=True, exist_ok=True)
90
+ with self._usage_path.open("w", encoding="utf-8") as f:
91
+ json.dump(state.model_dump(mode="json"), f, indent=2)
92
+ logger.debug("Usage state persisted to %s", self._usage_path)
93
+ except Exception as exc:
94
+ logger.error(
95
+ "Failed to persist usage state to %s: %s", self._usage_path, exc
96
+ )
97
+
98
+ def restore_usage_state(self) -> None:
99
+ if not self._usage_path.exists():
100
+ logger.debug("No usage state file found at %s", self._usage_path)
101
+ return
102
+
103
+ try:
104
+ with self._usage_path.open(encoding="utf-8") as f:
105
+ data = json.load(f)
106
+
107
+ state = UsageState.model_validate(data)
108
+ except Exception as exc:
109
+ logger.error(
110
+ "Failed to restore usage state from %s: %s", self._usage_path, exc
111
+ )
112
+ return
113
+
114
+ self.usage = defaultdict(RunUsage)
115
+ for model_name, usage in state.usage.items():
116
+ self.usage[model_name] = usage
117
+
118
+ self._model_providers = state.model_providers.copy()
119
+ self._usage_log = state.usage_log.copy()
120
+
121
+
122
+ def format_usage_hint(breakdown: list[UsageSummaryEntry]) -> str | None:
123
+ if not breakdown:
124
+ return None
125
+
126
+ lines = ["# Token usage by model"]
127
+
128
+ for entry in breakdown:
129
+ usage = entry.usage
130
+ input_tokens = usage.input_tokens
131
+ output_tokens = usage.output_tokens
132
+ cached_tokens = usage.cache_read_tokens
133
+
134
+ cost = calc_price(usage=usage, model_ref=entry.model_name)
135
+ input_line = f"* Input: {input_tokens:,}"
136
+ if cached_tokens > 0:
137
+ input_line += f" (+ {cached_tokens:,} cached)"
138
+ input_line += " tokens"
139
+ section = f"""
140
+ ### {entry.model_name}
141
+
142
+ {input_line}
143
+ * Output: {output_tokens:,} tokens
144
+ * Total: {input_tokens + output_tokens:,} tokens
145
+ * Cost: ${cost.total_price:,.2f}
146
+ """.strip()
147
+ lines.append(section)
148
+
149
+ return "\n\n".join(lines)
150
+
151
+
152
+ _usage_manager = None
153
+
154
+
155
+ def get_session_usage_manager() -> SessionUsageManager:
156
+ global _usage_manager
157
+ if _usage_manager is None:
158
+ _usage_manager = SessionUsageManager()
159
+ return _usage_manager
shotgun/cli/config.py CHANGED
@@ -9,6 +9,7 @@ from rich.table import Table
9
9
 
10
10
  from shotgun.agents.config import ProviderType, get_config_manager
11
11
  from shotgun.logging_config import get_logger
12
+ from shotgun.utils.env_utils import is_shotgun_account_enabled
12
13
 
13
14
  logger = get_logger(__name__)
14
15
  console = Console()
@@ -162,12 +163,17 @@ def _show_full_config(config: Any) -> None:
162
163
  table.add_row("", "") # Separator
163
164
 
164
165
  # Provider configurations
165
- for provider_name, provider_config in [
166
+ providers_to_show = [
166
167
  ("OpenAI", config.openai),
167
168
  ("Anthropic", config.anthropic),
168
169
  ("Google", config.google),
169
- ("Shotgun Account", config.shotgun),
170
- ]:
170
+ ]
171
+
172
+ # Only show Shotgun Account if feature flag is enabled
173
+ if is_shotgun_account_enabled():
174
+ providers_to_show.append(("Shotgun Account", config.shotgun))
175
+
176
+ for provider_name, provider_config in providers_to_show:
171
177
  table.add_row(f"[bold]{provider_name}[/bold]", "")
172
178
 
173
179
  # API Key
@@ -207,7 +213,13 @@ def _show_provider_config(provider: ProviderType, config: Any) -> None:
207
213
 
208
214
  def _mask_secrets(data: dict[str, Any]) -> None:
209
215
  """Mask secrets in configuration data."""
210
- for provider in ["openai", "anthropic", "google", "shotgun"]:
216
+ providers = ["openai", "anthropic", "google"]
217
+
218
+ # Only mask shotgun if feature flag is enabled
219
+ if is_shotgun_account_enabled():
220
+ providers.append("shotgun")
221
+
222
+ for provider in providers:
211
223
  if provider in data and isinstance(data[provider], dict):
212
224
  if "api_key" in data[provider] and data[provider]["api_key"]:
213
225
  data[provider]["api_key"] = _mask_value(data[provider]["api_key"])
shotgun/cli/feedback.py CHANGED
@@ -43,4 +43,4 @@ def send_feedback(
43
43
 
44
44
  submit_feedback_survey(feedback)
45
45
 
46
- console.print("Feedback sent. Thank you!")
46
+ console.print("Feedback sent. Thank you!")
shotgun/tui/app.py CHANGED
@@ -108,7 +108,7 @@ class ShotgunApp(App[None]):
108
108
  def handle_feedback(feedback: Feedback | None) -> None:
109
109
  if feedback is not None:
110
110
  submit_feedback_survey(feedback)
111
- self.notify("Feedback sent. Thank you!")
111
+ self.notify("Feedback sent. Thank you!")
112
112
 
113
113
  self.push_screen("feedback", callback=handle_feedback)
114
114
 
@@ -54,10 +54,8 @@ from ..components.prompt_input import PromptInput
54
54
  from ..components.spinner import Spinner
55
55
  from ..utils.mode_progress import PlaceholderHints
56
56
  from .chat_screen.command_providers import (
57
- AgentModeProvider,
58
- CodebaseCommandProvider,
59
57
  DeleteCodebasePaletteProvider,
60
- ProviderSetupProvider,
58
+ UnifiedCommandProvider,
61
59
  )
62
60
 
63
61
  logger = logging.getLogger(__name__)
@@ -228,9 +226,12 @@ class ChatScreen(Screen[None]):
228
226
  BINDINGS = [
229
227
  ("ctrl+p", "command_palette", "Command Palette"),
230
228
  ("shift+tab", "toggle_mode", "Toggle mode"),
229
+ ("ctrl+u", "show_usage", "Show usage"),
231
230
  ]
232
231
 
233
- COMMANDS = {AgentModeProvider, ProviderSetupProvider, CodebaseCommandProvider}
232
+ COMMANDS = {
233
+ UnifiedCommandProvider,
234
+ }
234
235
 
235
236
  value = reactive("")
236
237
  mode = reactive(AgentType.RESEARCH)
@@ -401,6 +402,14 @@ class ChatScreen(Screen[None]):
401
402
  # whoops it actually changes focus. Let's be brutal for now
402
403
  self.call_later(lambda: self.query_one(PromptInput).focus())
403
404
 
405
+ def action_show_usage(self) -> None:
406
+ usage_hint = self.agent_manager.get_usage_hint()
407
+ logger.info(f"Usage hint: {usage_hint}")
408
+ if usage_hint:
409
+ self.mount_hint(usage_hint)
410
+ else:
411
+ self.notify("No usage hint available", severity="error")
412
+
404
413
  @work
405
414
  async def add_question_listener(self) -> None:
406
415
  while True:
@@ -589,7 +598,7 @@ class ChatScreen(Screen[None]):
589
598
  async def index_codebase(self, selection: CodebaseIndexSelection) -> None:
590
599
  label = self.query_one("#indexing-job-display", Static)
591
600
  label.update(
592
- f"[$foreground-muted]Indexing [bold $text-accent]{selection.name}[/]...[/]"
601
+ f"[$foreground-muted]Indexing codebase: [bold $text-accent]{selection.name}[/][/]"
593
602
  )
594
603
  label.refresh()
595
604
 
@@ -599,33 +608,33 @@ class ChatScreen(Screen[None]):
599
608
  empty = width - filled
600
609
  return "▓" * filled + "░" * empty
601
610
 
602
- # Spinner animation state (shared between timer and progress callback)
611
+ # Spinner animation frames
603
612
  spinner_frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
604
- spinner_state: dict[str, int | str | float] = {
613
+
614
+ # Progress state (shared between timer and progress callback)
615
+ progress_state: dict[str, int | float] = {
605
616
  "frame_index": 0,
606
- "phase_name": "Starting...",
607
617
  "percentage": 0.0,
608
618
  }
609
619
 
610
- def update_spinner_display() -> None:
611
- """Update spinner frame on timer - runs every 100ms."""
620
+ def update_progress_display() -> None:
621
+ """Update progress bar on timer - runs every 100ms."""
612
622
  # Advance spinner frame
613
- frame_idx = int(spinner_state["frame_index"])
614
- spinner_state["frame_index"] = (frame_idx + 1) % len(spinner_frames)
623
+ frame_idx = int(progress_state["frame_index"])
624
+ progress_state["frame_index"] = (frame_idx + 1) % len(spinner_frames)
615
625
  spinner = spinner_frames[frame_idx]
616
626
 
617
627
  # Get current state
618
- phase = str(spinner_state["phase_name"])
619
- pct = float(spinner_state["percentage"])
628
+ pct = float(progress_state["percentage"])
620
629
  bar = create_progress_bar(pct)
621
630
 
622
631
  # Update label
623
632
  label.update(
624
- f"[$foreground-muted]Indexing codebase: {spinner} {phase}... {bar} {pct:.0f}%[/]"
633
+ f"[$foreground-muted]Indexing codebase: {spinner} {bar} {pct:.0f}%[/]"
625
634
  )
626
635
 
627
636
  def progress_callback(progress_info: IndexProgress) -> None:
628
- """Update progress state (spinner animates independently on timer)."""
637
+ """Update progress state (timer renders it independently)."""
629
638
  # Calculate overall percentage (0-95%, reserve 95-100% for finalization)
630
639
  if progress_info.phase == ProgressPhase.STRUCTURE:
631
640
  # Phase 1: 0-10%, always show 5% while running, 10% when complete
@@ -648,11 +657,10 @@ class ChatScreen(Screen[None]):
648
657
  overall_pct = 0.0
649
658
 
650
659
  # Update shared state (timer will render it)
651
- spinner_state["phase_name"] = progress_info.phase_name
652
- spinner_state["percentage"] = overall_pct
660
+ progress_state["percentage"] = overall_pct
653
661
 
654
- # Start spinner animation timer (10 fps = 100ms interval)
655
- spinner_timer = self.set_interval(0.1, update_spinner_display)
662
+ # Start progress animation timer (10 fps = 100ms interval)
663
+ progress_timer = self.set_interval(0.1, update_progress_display)
656
664
 
657
665
  try:
658
666
  # Pass the current working directory as the indexed_from_cwd
@@ -667,14 +675,12 @@ class ChatScreen(Screen[None]):
667
675
  progress_callback=progress_callback,
668
676
  )
669
677
 
670
- # Stop spinner animation
671
- spinner_timer.stop()
678
+ # Stop progress animation
679
+ progress_timer.stop()
672
680
 
673
681
  # Show 100% completion after indexing finishes
674
682
  final_bar = create_progress_bar(100.0)
675
- label.update(
676
- f"[$foreground-muted]Indexing codebase: ✓ Complete {final_bar} 100%[/]"
677
- )
683
+ label.update(f"[$foreground-muted]Indexing codebase: {final_bar} 100%[/]")
678
684
  label.refresh()
679
685
 
680
686
  logger.info(
@@ -687,12 +693,12 @@ class ChatScreen(Screen[None]):
687
693
  )
688
694
 
689
695
  except CodebaseAlreadyIndexedError as exc:
690
- spinner_timer.stop()
696
+ progress_timer.stop()
691
697
  logger.warning(f"Codebase already indexed: {exc}")
692
698
  self.notify(str(exc), severity="warning")
693
699
  return
694
700
  except InvalidPathError as exc:
695
- spinner_timer.stop()
701
+ progress_timer.stop()
696
702
  logger.error(f"Invalid path error: {exc}")
697
703
  self.notify(str(exc), severity="error")
698
704
 
@@ -704,8 +710,8 @@ class ChatScreen(Screen[None]):
704
710
  )
705
711
  self.notify(f"Failed to index codebase: {exc}", severity="error")
706
712
  finally:
707
- # Always stop the spinner timer
708
- spinner_timer.stop()
713
+ # Always stop the progress timer
714
+ progress_timer.stop()
709
715
  label.update("")
710
716
  label.refresh()
711
717
 
@@ -774,6 +780,7 @@ class ChatScreen(Screen[None]):
774
780
 
775
781
  # Update the current mode
776
782
  self.mode = AgentType(conversation.last_agent_model)
783
+ self.deps.usage_manager.restore_usage_state()
777
784
 
778
785
 
779
786
  def help_text_with_codebase(already_indexed: bool = False) -> str:
@@ -96,6 +96,38 @@ class AgentModeProvider(Provider):
96
96
  yield Hit(score, matcher.highlight(title), callback, help=help_text)
97
97
 
98
98
 
99
+ class UsageProvider(Provider):
100
+ """Command provider for agent mode switching."""
101
+
102
+ @property
103
+ def chat_screen(self) -> "ChatScreen":
104
+ from shotgun.tui.screens.chat import ChatScreen
105
+
106
+ return cast(ChatScreen, self.screen)
107
+
108
+ async def discover(self) -> AsyncGenerator[DiscoveryHit, None]:
109
+ """Provide default mode switching commands when palette opens."""
110
+ yield DiscoveryHit(
111
+ "Show usage",
112
+ self.chat_screen.action_show_usage,
113
+ help="Display usage information for the current session",
114
+ )
115
+
116
+ async def search(self, query: str) -> AsyncGenerator[Hit, None]:
117
+ """Search for mode commands."""
118
+ matcher = self.matcher(query)
119
+
120
+ async for discovery_hit in self.discover():
121
+ score = matcher.match(discovery_hit.text or "")
122
+ if score > 0:
123
+ yield Hit(
124
+ score,
125
+ matcher.highlight(discovery_hit.text or ""),
126
+ discovery_hit.command,
127
+ help=discovery_hit.help,
128
+ )
129
+
130
+
99
131
  class ProviderSetupProvider(Provider):
100
132
  """Command palette entries for provider configuration."""
101
133
 
@@ -159,30 +191,30 @@ class CodebaseCommandProvider(Provider):
159
191
  return cast(ChatScreen, self.screen)
160
192
 
161
193
  async def discover(self) -> AsyncGenerator[DiscoveryHit, None]:
162
- yield DiscoveryHit(
163
- "Codebase: Index Codebase",
164
- self.chat_screen.index_codebase_command,
165
- help="Index a repository into the codebase graph",
166
- )
167
194
  yield DiscoveryHit(
168
195
  "Codebase: Delete Codebase Index",
169
196
  self.chat_screen.delete_codebase_command,
170
197
  help="Delete an existing codebase index",
171
198
  )
199
+ yield DiscoveryHit(
200
+ "Codebase: Index Codebase",
201
+ self.chat_screen.index_codebase_command,
202
+ help="Index a repository into the codebase graph",
203
+ )
172
204
 
173
205
  async def search(self, query: str) -> AsyncGenerator[Hit, None]:
174
206
  matcher = self.matcher(query)
175
207
  commands = [
176
- (
177
- "Codebase: Index Codebase",
178
- self.chat_screen.index_codebase_command,
179
- "Index a repository into the codebase graph",
180
- ),
181
208
  (
182
209
  "Codebase: Delete Codebase Index",
183
210
  self.chat_screen.delete_codebase_command,
184
211
  "Delete an existing codebase index",
185
212
  ),
213
+ (
214
+ "Codebase: Index Codebase",
215
+ self.chat_screen.index_codebase_command,
216
+ "Index a repository into the codebase graph",
217
+ ),
186
218
  ]
187
219
  for title, callback, help_text in commands:
188
220
  score = matcher.match(title)
@@ -237,3 +269,88 @@ class DeleteCodebasePaletteProvider(Provider):
237
269
  ),
238
270
  help=graph.repo_path,
239
271
  )
272
+
273
+
274
+ class UnifiedCommandProvider(Provider):
275
+ """Unified command provider with all commands in alphabetical order."""
276
+
277
+ @property
278
+ def chat_screen(self) -> "ChatScreen":
279
+ from shotgun.tui.screens.chat import ChatScreen
280
+
281
+ return cast(ChatScreen, self.screen)
282
+
283
+ def open_provider_config(self) -> None:
284
+ """Show the provider configuration screen."""
285
+ self.chat_screen.app.push_screen("provider_config")
286
+
287
+ def open_model_picker(self) -> None:
288
+ """Show the model picker screen."""
289
+ self.chat_screen.app.push_screen("model_picker")
290
+
291
+ async def discover(self) -> AsyncGenerator[DiscoveryHit, None]:
292
+ """Provide commands in alphabetical order when palette opens."""
293
+ # Alphabetically ordered commands
294
+ yield DiscoveryHit(
295
+ "Codebase: Delete Codebase Index",
296
+ self.chat_screen.delete_codebase_command,
297
+ help="Delete an existing codebase index",
298
+ )
299
+ yield DiscoveryHit(
300
+ "Codebase: Index Codebase",
301
+ self.chat_screen.index_codebase_command,
302
+ help="Index a repository into the codebase graph",
303
+ )
304
+ yield DiscoveryHit(
305
+ "Open Provider Setup",
306
+ self.open_provider_config,
307
+ help="⚙️ Manage API keys for available providers",
308
+ )
309
+ yield DiscoveryHit(
310
+ "Select AI Model",
311
+ self.open_model_picker,
312
+ help="🤖 Choose which AI model to use",
313
+ )
314
+ yield DiscoveryHit(
315
+ "Show usage",
316
+ self.chat_screen.action_show_usage,
317
+ help="Display usage information for the current session",
318
+ )
319
+
320
+ async def search(self, query: str) -> AsyncGenerator[Hit, None]:
321
+ """Search for commands in alphabetical order."""
322
+ matcher = self.matcher(query)
323
+
324
+ # Define all commands in alphabetical order
325
+ commands = [
326
+ (
327
+ "Codebase: Delete Codebase Index",
328
+ self.chat_screen.delete_codebase_command,
329
+ "Delete an existing codebase index",
330
+ ),
331
+ (
332
+ "Codebase: Index Codebase",
333
+ self.chat_screen.index_codebase_command,
334
+ "Index a repository into the codebase graph",
335
+ ),
336
+ (
337
+ "Open Provider Setup",
338
+ self.open_provider_config,
339
+ "⚙️ Manage API keys for available providers",
340
+ ),
341
+ (
342
+ "Select AI Model",
343
+ self.open_model_picker,
344
+ "🤖 Choose which AI model to use",
345
+ ),
346
+ (
347
+ "Show usage",
348
+ self.chat_screen.action_show_usage,
349
+ "Display usage information for the current session",
350
+ ),
351
+ ]
352
+
353
+ for title, callback, help_text in commands:
354
+ score = matcher.match(title)
355
+ if score > 0:
356
+ yield Hit(score, matcher.highlight(title), callback, help=help_text)
@@ -217,7 +217,9 @@ class AgentResponseWidget(Widget):
217
217
  return ""
218
218
  for idx, part in enumerate(self.item.parts):
219
219
  if isinstance(part, TextPart):
220
- acc += f"**⏺** {part.content}\n\n"
220
+ # Only show the circle prefix if there's actual content
221
+ if part.content and part.content.strip():
222
+ acc += f"**⏺** {part.content}\n\n"
221
223
  elif isinstance(part, ToolCallPart):
222
224
  parts_str = self._format_tool_call_part(part)
223
225
  acc += parts_str + "\n\n"
@@ -35,10 +35,6 @@ class ModelPickerScreen(Screen[None]):
35
35
  layout: vertical;
36
36
  }
37
37
 
38
- ModelPicker > * {
39
- height: auto;
40
- }
41
-
42
38
  #titlebox {
43
39
  height: auto;
44
40
  margin: 2 0;
@@ -60,6 +56,7 @@ class ModelPickerScreen(Screen[None]):
60
56
  #model-list {
61
57
  margin: 2 0;
62
58
  height: auto;
59
+ padding: 1;
63
60
  & > * {
64
61
  padding: 1 0;
65
62
  }
@@ -70,9 +67,6 @@ class ModelPickerScreen(Screen[None]):
70
67
  #model-actions > * {
71
68
  margin-right: 2;
72
69
  }
73
- #model-list {
74
- padding: 1;
75
- }
76
70
  """
77
71
 
78
72
  BINDINGS = [
@@ -97,16 +91,21 @@ class ModelPickerScreen(Screen[None]):
97
91
  # Load current selection
98
92
  config_manager = self.config_manager
99
93
  config = config_manager.load()
100
- current_model = config.selected_model or ModelName.CLAUDE_OPUS_4_1
94
+ current_model = config.selected_model or ModelName.CLAUDE_SONNET_4_5
101
95
  self.selected_model = current_model
102
96
 
103
- # Find and highlight current selection
97
+ # Find and highlight current selection (if it's in the filtered list)
104
98
  list_view = self.query_one(ListView)
105
99
  if list_view.children:
106
- for i, model_name in enumerate(AVAILABLE_MODELS):
107
- if model_name == current_model:
108
- list_view.index = i
109
- break
100
+ for i, child in enumerate(list_view.children):
101
+ if isinstance(child, ListItem) and child.id:
102
+ model_id = child.id.removeprefix("model-")
103
+ # Find the model name
104
+ for model_name in AVAILABLE_MODELS:
105
+ if _sanitize_model_name_for_id(model_name) == model_id:
106
+ if model_name == current_model:
107
+ list_view.index = i
108
+ break
110
109
  self.refresh_model_labels()
111
110
 
112
111
  def action_done(self) -> None:
@@ -141,9 +140,12 @@ class ModelPickerScreen(Screen[None]):
141
140
  def refresh_model_labels(self) -> None:
142
141
  """Update the list view entries to reflect current selection."""
143
142
  current_model = (
144
- self.config_manager.load().selected_model or ModelName.CLAUDE_OPUS_4_1
143
+ self.config_manager.load().selected_model or ModelName.CLAUDE_SONNET_4_5
145
144
  )
145
+ # Update labels for available models only
146
146
  for model_name in AVAILABLE_MODELS:
147
+ if not self._is_model_available(model_name):
148
+ continue
147
149
  label = self.query_one(
148
150
  f"#label-{_sanitize_model_name_for_id(model_name)}", Label
149
151
  )
@@ -155,6 +157,10 @@ class ModelPickerScreen(Screen[None]):
155
157
  items: list[ListItem] = []
156
158
  current_model = self.selected_model
157
159
  for model_name in AVAILABLE_MODELS:
160
+ # Only add models that are available
161
+ if not self._is_model_available(model_name):
162
+ continue
163
+
158
164
  label = Label(
159
165
  self._model_label(model_name, is_current=model_name == current_model),
160
166
  id=f"label-{_sanitize_model_name_for_id(model_name)}",
@@ -165,6 +171,7 @@ class ModelPickerScreen(Screen[None]):
165
171
  return items
166
172
 
167
173
  def _model_from_item(self, item: ListItem | None) -> ModelName | None:
174
+ """Get ModelName from a ListItem."""
168
175
  if item is None or item.id is None:
169
176
  return None
170
177
  sanitized_id = item.id.removeprefix("model-")
@@ -174,6 +181,32 @@ class ModelPickerScreen(Screen[None]):
174
181
  return model_name
175
182
  return None
176
183
 
184
+ def _is_model_available(self, model_name: ModelName) -> bool:
185
+ """Check if a model is available based on provider key configuration.
186
+
187
+ A model is available if:
188
+ 1. Shotgun Account key is configured (provides access to all models), OR
189
+ 2. The model's provider has an API key configured (BYOK mode)
190
+
191
+ Args:
192
+ model_name: The model to check availability for
193
+
194
+ Returns:
195
+ True if the model can be used, False otherwise
196
+ """
197
+ config = self.config_manager.load()
198
+
199
+ # If Shotgun Account is configured, all models are available
200
+ if self.config_manager._provider_has_api_key(config.shotgun):
201
+ return True
202
+
203
+ # In BYOK mode, check if the model's provider has a key
204
+ if model_name not in MODEL_SPECS:
205
+ return False
206
+
207
+ spec = MODEL_SPECS[model_name]
208
+ return self.config_manager.has_provider_key(spec.provider)
209
+
177
210
  def _model_label(self, model_name: ModelName, is_current: bool) -> str:
178
211
  """Generate label for model with specs and current indicator."""
179
212
  if model_name not in MODEL_SPECS:
@@ -188,6 +221,10 @@ class ModelPickerScreen(Screen[None]):
188
221
 
189
222
  label = f"{display_name} · {input_k}K context · {output_k}K output"
190
223
 
224
+ # Add cost indicator for expensive models
225
+ if model_name == ModelName.CLAUDE_OPUS_4_1:
226
+ label += " · Expensive"
227
+
191
228
  if is_current:
192
229
  label += " · Current"
193
230
 
@@ -198,6 +235,7 @@ class ModelPickerScreen(Screen[None]):
198
235
  names = {
199
236
  ModelName.GPT_5: "GPT-5 (OpenAI)",
200
237
  ModelName.CLAUDE_OPUS_4_1: "Claude Opus 4.1 (Anthropic)",
238
+ ModelName.CLAUDE_SONNET_4_5: "Claude Sonnet 4.5 (Anthropic)",
201
239
  ModelName.GEMINI_2_5_PRO: "Gemini 2.5 Pro (Google)",
202
240
  }
203
241
  return names.get(model_name, model_name.value)
@@ -9,15 +9,26 @@ from textual.app import ComposeResult
9
9
  from textual.containers import Horizontal, Vertical
10
10
  from textual.reactive import reactive
11
11
  from textual.screen import Screen
12
- from textual.widgets import Button, Input, Label, ListItem, ListView, Static
12
+ from textual.widgets import Button, Input, Label, ListItem, ListView, Markdown, Static
13
13
 
14
14
  from shotgun.agents.config import ConfigManager, ProviderType
15
+ from shotgun.utils.env_utils import is_shotgun_account_enabled
15
16
 
16
17
  if TYPE_CHECKING:
17
18
  from ..app import ShotgunApp
18
19
 
19
- # Provider identifiers for configuration screen (LLM providers + Shotgun Account)
20
- CONFIGURABLE_PROVIDERS = ["openai", "anthropic", "google", "shotgun"]
20
+
21
+ def get_configurable_providers() -> list[str]:
22
+ """Get list of configurable providers based on feature flags.
23
+
24
+ Returns:
25
+ List of provider identifiers that can be configured.
26
+ Includes shotgun only if SHOTGUN_ACCOUNT_ENABLED is set.
27
+ """
28
+ providers = ["openai", "anthropic", "google"]
29
+ if is_shotgun_account_enabled():
30
+ providers.append("shotgun")
31
+ return providers
21
32
 
22
33
 
23
34
  class ProviderConfigScreen(Screen[None]):
@@ -50,6 +61,10 @@ class ProviderConfigScreen(Screen[None]):
50
61
  color: $text-accent;
51
62
  }
52
63
 
64
+ #provider-links {
65
+ padding: 1 0;
66
+ }
67
+
53
68
  #provider-list {
54
69
  margin: 2 0;
55
70
  height: auto;
@@ -81,6 +96,10 @@ class ProviderConfigScreen(Screen[None]):
81
96
  "Select a provider and enter the API key needed to activate it.",
82
97
  id="provider-config-summary",
83
98
  )
99
+ yield Markdown(
100
+ "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)",
101
+ id="provider-links",
102
+ )
84
103
  yield ListView(*self._build_provider_items(), id="provider-list")
85
104
  yield Input(
86
105
  placeholder=self._input_placeholder(self.selected_provider),
@@ -147,13 +166,13 @@ class ProviderConfigScreen(Screen[None]):
147
166
 
148
167
  def refresh_provider_status(self) -> None:
149
168
  """Update the list view entries to reflect configured providers."""
150
- for provider_id in CONFIGURABLE_PROVIDERS:
169
+ for provider_id in get_configurable_providers():
151
170
  label = self.query_one(f"#label-{provider_id}", Label)
152
171
  label.update(self._provider_label(provider_id))
153
172
 
154
173
  def _build_provider_items(self) -> list[ListItem]:
155
174
  items: list[ListItem] = []
156
- for provider_id in CONFIGURABLE_PROVIDERS:
175
+ for provider_id in get_configurable_providers():
157
176
  label = Label(self._provider_label(provider_id), id=f"label-{provider_id}")
158
177
  items.append(ListItem(label, id=f"provider-{provider_id}"))
159
178
  return items
@@ -162,7 +181,7 @@ class ProviderConfigScreen(Screen[None]):
162
181
  if item is None or item.id is None:
163
182
  return None
164
183
  provider_id = item.id.removeprefix("provider-")
165
- return provider_id if provider_id in CONFIGURABLE_PROVIDERS else None
184
+ return provider_id if provider_id in get_configurable_providers() else None
166
185
 
167
186
  def _provider_label(self, provider_id: str) -> str:
168
187
  display = self._provider_display_name(provider_id)
@@ -1,5 +1,17 @@
1
1
  """Utilities for working with environment variables."""
2
2
 
3
+ import os
4
+
5
+
6
+ def is_shotgun_account_enabled() -> bool:
7
+ """Check if Shotgun Account feature is enabled via environment variable.
8
+
9
+ Returns:
10
+ True if SHOTGUN_ACCOUNT_ENABLED is set to a truthy value,
11
+ False otherwise
12
+ """
13
+ return is_truthy(os.environ.get("SHOTGUN_ACCOUNT_ENABLED"))
14
+
3
15
 
4
16
  def is_truthy(value: str | None) -> bool:
5
17
  """Check if a string value represents true.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shotgun-sh
3
- Version: 0.2.1.dev1
3
+ Version: 0.2.1.dev3
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
@@ -22,6 +22,7 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
22
  Classifier: Topic :: Utilities
23
23
  Requires-Python: >=3.11
24
24
  Requires-Dist: anthropic>=0.39.0
25
+ Requires-Dist: genai-prices>=0.0.27
25
26
  Requires-Dist: httpx>=0.27.0
26
27
  Requires-Dist: jinja2>=3.1.0
27
28
  Requires-Dist: kuzu>=0.7.0
@@ -7,23 +7,24 @@ shotgun/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
7
  shotgun/sentry_telemetry.py,sha256=L7jFMNAnDIENWVeQYSLpyul2nmIm2w3wnOp2kDP_cic,2902
8
8
  shotgun/telemetry.py,sha256=Ves6Ih3hshpKVNVAUUmwRdtW8NkTjFPg8hEqvFKZ0t0,3208
9
9
  shotgun/agents/__init__.py,sha256=8Jzv1YsDuLyNPFJyckSr_qI4ehTVeDyIMDW4omsfPGc,25
10
- shotgun/agents/agent_manager.py,sha256=wXUQCEsSs68FG8edFLryeePBzxxX1GStJZwd1sS5WsE,26245
10
+ shotgun/agents/agent_manager.py,sha256=xq8L0oAFgtFCpKVsyUoMtYJqUyz5XxjWLKNnxoe1zo4,26577
11
11
  shotgun/agents/common.py,sha256=Hr9HigsDopkI0Sr3FThGDv1f67NLemOjcYA6LV9v970,18963
12
12
  shotgun/agents/conversation_history.py,sha256=5J8_1yxdZiiWTq22aDio88DkBDZ4_Lh_p5Iy5_ENszc,3898
13
13
  shotgun/agents/conversation_manager.py,sha256=fxAvXbEl3Cl2ugJ4N9aWXaqZtkrnfj3QzwjWC4LFXwI,3514
14
14
  shotgun/agents/export.py,sha256=Zke952DbJ_lOBUmN-TPHw7qmjbfqsFu1uycBRQI_pkg,2969
15
15
  shotgun/agents/llm.py,sha256=hs8j1wwTczGtehzahL1Z_5D4qus5QUx4-h9-m5ZPzm4,2209
16
16
  shotgun/agents/messages.py,sha256=wNn0qC5AqASM8LMaSGFOerZEJPn5FsIOmaJs1bdosuU,1036
17
- shotgun/agents/models.py,sha256=n4N141QOPmcuFjJkn5uZRAvtVwF1acsAJE8QlQABCiM,7845
17
+ shotgun/agents/models.py,sha256=IvwwjbJYi5wi9S-budg8g1ezi1VaO57Q-XtegkbTrXg,8096
18
18
  shotgun/agents/plan.py,sha256=s-WfILBOW4l8kY59RUOVtX5MJSuSzFm1nGp6b17If78,3030
19
19
  shotgun/agents/research.py,sha256=lYG7Rytcitop8mXs3isMI3XvYzzI3JH9u0VZz6K9zfo,3274
20
20
  shotgun/agents/specify.py,sha256=7MoMxfIn34G27mw6wrp_F0i2O5rid476L3kHFONDCd0,3137
21
21
  shotgun/agents/tasks.py,sha256=nk8zIl24o01hfzOGyWSbeVWeke6OGseO4Ppciurh13U,2999
22
+ shotgun/agents/usage_manager.py,sha256=5d9JC4_cthXwhTSytMfMExMDAUYp8_nkPepTJZXk13w,5017
22
23
  shotgun/agents/config/__init__.py,sha256=Fl8K_81zBpm-OfOW27M_WWLSFdaHHek6lWz95iDREjQ,318
23
24
  shotgun/agents/config/constants.py,sha256=I3f0ueoQaTg5HddXGCYimCYpj-U57z3IBQYIVJxVIhg,872
24
- shotgun/agents/config/manager.py,sha256=phm_oqObIrCGJrZUvVbwSJHB--Mn7zqJLWB9ZWok5lY,15042
25
- shotgun/agents/config/models.py,sha256=h1XqPOh66moIYaXtEyoGJuPLXdD3acVCOgvgrELsS_4,4796
26
- shotgun/agents/config/provider.py,sha256=V4s7HwU3XwCWqmvMPoDiG9pVjUuuyNWidvqhImLXvxE,11547
25
+ shotgun/agents/config/manager.py,sha256=6WT4xaAfJTyYxpQ9AcMYHG1cjySNmtJDgTzVmqIAMpc,14503
26
+ shotgun/agents/config/models.py,sha256=ZojhfheNO337e1icy_cE2PpBXIl5oHkdajr4azzFF-U,5106
27
+ shotgun/agents/config/provider.py,sha256=_HkmN4WhGcZgtAuaF-RA6ZEOiWQ0oWLufZDxl-3rnn4,10935
27
28
  shotgun/agents/history/__init__.py,sha256=XFQj2a6fxDqVg0Q3juvN9RjV_RJbgvFZtQOCOjVJyp4,147
28
29
  shotgun/agents/history/compaction.py,sha256=9RMpG0aY_7L4TecbgwHSOkGtbd9W5XZTg-MbzZmNl00,3515
29
30
  shotgun/agents/history/constants.py,sha256=yWY8rrTZarLA3flCCMB_hS2NMvUDRDTwP4D4j7MIh1w,446
@@ -50,14 +51,14 @@ shotgun/agents/tools/codebase/models.py,sha256=8eR3_8DQiBNgB2twu0aC_evIJbugN9KW3
50
51
  shotgun/agents/tools/codebase/query_graph.py,sha256=vOeyN4-OZj-vpTSk3Z9W5TjraZAepJ-Qjk_zzvum3fU,2115
51
52
  shotgun/agents/tools/codebase/retrieve_code.py,sha256=2VjiqVKJMd9rPV-mGrL4C-N8fqGjYLW6ZInFGbcTxOM,2878
52
53
  shotgun/agents/tools/web_search/__init__.py,sha256=_9rgs_gv41-wfPvwfWM_Qfq-zvboyQ_srfyneGsxgM4,3182
53
- shotgun/agents/tools/web_search/anthropic.py,sha256=wN3dRBXohIwcJRp0KA3QTgldGjKuDZ2NW4X36mvIn3E,4971
54
+ shotgun/agents/tools/web_search/anthropic.py,sha256=GelAhAmb-b4o87-3sgxNFfw-G2LXDEjfdZ7XfF0bQD0,4983
54
55
  shotgun/agents/tools/web_search/gemini.py,sha256=-fI_deaBT4-_61A7KlKtz8tmKXW50fVx_97WAJTUg4w,3468
55
56
  shotgun/agents/tools/web_search/openai.py,sha256=pnIcTV3vwXJQuxPs4I7gQNX18XzM7D7FqeNxnn1E7yw,3437
56
57
  shotgun/agents/tools/web_search/utils.py,sha256=GLJ5QV9bT2ubFMuFN7caMN7tK9OTJ0R3GD57B-tCMF0,532
57
58
  shotgun/cli/__init__.py,sha256=_F1uW2g87y4bGFxz8Gp8u7mq2voHp8vQIUtCmm8Tojo,40
58
- shotgun/cli/config.py,sha256=wQhuz8zE2K-fjwxeK2yZt-NkV13kEZAitBqrwPLUvgg,7466
59
+ shotgun/cli/config.py,sha256=Lrcqxm7W7I6g6iP_K5-yK7QFOgcYt5KIc8A6Wit1Ksg,7835
59
60
  shotgun/cli/export.py,sha256=3hIwK2_OM1MFYSTfzBxsGuuBGm5fo0XdxASfQ5Uqb3Y,2471
60
- shotgun/cli/feedback.py,sha256=Sh0aK93wE7rvvCI31QCbm0sU-AUEldg7s9QqoAm1oQM,1242
61
+ shotgun/cli/feedback.py,sha256=Me1dQQgkYwP4AIFwYgfHcPXxFdJ6CzFbCBttKcFd2Q0,1238
61
62
  shotgun/cli/models.py,sha256=kwZEldQWUheNsqF_ezgDzRBc6h0Y0JxFw1VMQjZlvPE,182
62
63
  shotgun/cli/plan.py,sha256=T-eu-I9z-dSoKqJ-KI8X5i5Mm0VL1BfornxRiUjTgnk,2324
63
64
  shotgun/cli/research.py,sha256=qvBBtX3Wyn6pDZlJpcEvbeK-0iTOXegi71tm8HKVYaE,2490
@@ -113,7 +114,7 @@ shotgun/sdk/exceptions.py,sha256=qBcQv0v7ZTwP7CMcxZST4GqCsfOWtOUjSzGBo0-heqo,412
113
114
  shotgun/sdk/models.py,sha256=X9nOTUHH0cdkQW1NfnMEDu-QgK9oUsEISh1Jtwr5Am4,5496
114
115
  shotgun/sdk/services.py,sha256=J4PJFSxCQ6--u7rb3Ta-9eYtlYcxcbnzrMP6ThyCnw4,705
115
116
  shotgun/tui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
116
- shotgun/tui/app.py,sha256=Rv4Pxi2uyDE_tspZGMWKJY1KrH47d8uCIBBMfM05gCQ,5093
117
+ shotgun/tui/app.py,sha256=DrULqhxPPDZzz1WQNjXDY_cls4kXJNexYlI1ISrNTKs,5089
117
118
  shotgun/tui/filtered_codebase_service.py,sha256=lJ8gTMhIveTatmvmGLP299msWWTkVYKwvY_2FhuL2s4,1687
118
119
  shotgun/tui/styles.tcss,sha256=ETyyw1bpMBOqTi5RLcAJUScdPWTvAWEqE9YcT0kVs_E,121
119
120
  shotgun/tui/commands/__init__.py,sha256=8D5lvtpqMW5-fF7Bg3oJtUzU75cKOv6aUaHYYszydU8,2518
@@ -121,26 +122,26 @@ shotgun/tui/components/prompt_input.py,sha256=Ss-htqraHZAPaehGE4x86ij0veMjc4Ugad
121
122
  shotgun/tui/components/spinner.py,sha256=ovTDeaJ6FD6chZx_Aepia6R3UkPOVJ77EKHfRmn39MY,2427
122
123
  shotgun/tui/components/splash.py,sha256=vppy9vEIEvywuUKRXn2y11HwXSRkQZHLYoVjhDVdJeU,1267
123
124
  shotgun/tui/components/vertical_tail.py,sha256=kROwTaRjUwVB7H35dtmNcUVPQqNYvvfq7K2tXBKEb6c,638
124
- shotgun/tui/screens/chat.py,sha256=HIDnqt8og2KMmtdOok1lL44130M8V7IP6Un_8HcRwsA,30178
125
+ shotgun/tui/screens/chat.py,sha256=CqAv_x6R4zl-MGbtg8KgZWt8OhpBJYpx5gGBQ3oxqgw,30313
125
126
  shotgun/tui/screens/chat.tcss,sha256=2Yq3E23jxsySYsgZf4G1AYrYVcpX0UDW6kNNI0tDmtM,437
126
127
  shotgun/tui/screens/directory_setup.py,sha256=lIZ1J4A6g5Q2ZBX8epW7BhR96Dmdcg22CyiM5S-I5WU,3237
127
128
  shotgun/tui/screens/feedback.py,sha256=cYtmuM3qqKwevstu8gJ9mmk7lkIKZvfAyDEBUOLh-yI,5660
128
- shotgun/tui/screens/model_picker.py,sha256=2l0zkhJ5CPH_4Fn0MM3bp9RN7fEowbljpBsmTjzRQT8,6886
129
- shotgun/tui/screens/provider_config.py,sha256=-en99fDxTim0DSjpJTLSRba_yrON8wDhcuwU6rNZwHM,7765
129
+ shotgun/tui/screens/model_picker.py,sha256=ZutEz4w_BJkGGwwdJSj7-kPdIPzmK6ZDHYkEA0q596A,8625
130
+ shotgun/tui/screens/provider_config.py,sha256=pDJKjvl_els-XrbRTwBBDDL0jo_uRa7DMTAZJAJ9NXA,8461
130
131
  shotgun/tui/screens/splash.py,sha256=E2MsJihi3c9NY1L28o_MstDxGwrCnnV7zdq00MrGAsw,706
131
132
  shotgun/tui/screens/chat_screen/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
132
- shotgun/tui/screens/chat_screen/command_providers.py,sha256=B4uUzzGq23vfS1mp4U6zsOEqyv6Ni9AMu5fqp5xMz5s,8437
133
+ shotgun/tui/screens/chat_screen/command_providers.py,sha256=qAY09_pKxcTyxJHy9Rlwhs1tAW5QMWXxDwnpparnQmI,12542
133
134
  shotgun/tui/screens/chat_screen/hint_message.py,sha256=WOpbk8q7qt7eOHTyyHvh_IQIaublVDeJGaLpsxEk9FA,933
134
- shotgun/tui/screens/chat_screen/history.py,sha256=NVLA3_tERTyB4vkH71w8ef_M5CszfkwbQOuMb100Fzc,12272
135
+ shotgun/tui/screens/chat_screen/history.py,sha256=Go859iEjw0s5aELKpF42MjLXy7UFQ52XnJMTIkV3aLo,12406
135
136
  shotgun/tui/utils/__init__.py,sha256=cFjDfoXTRBq29wgP7TGRWUu1eFfiIG-LLOzjIGfadgI,150
136
137
  shotgun/tui/utils/mode_progress.py,sha256=lseRRo7kMWLkBzI3cU5vqJmS2ZcCjyRYf9Zwtvc-v58,10931
137
138
  shotgun/utils/__init__.py,sha256=WinIEp9oL2iMrWaDkXz2QX4nYVPAm8C9aBSKTeEwLtE,198
138
- shotgun/utils/env_utils.py,sha256=8QK5aw_f_V2AVTleQQlcL0RnD4sPJWXlDG46fsHu0d8,1057
139
+ shotgun/utils/env_utils.py,sha256=5spVCdeqVKtlWoKocPhz_5j_iRN30neqcGUzUuiWmfc,1365
139
140
  shotgun/utils/file_system_utils.py,sha256=l-0p1bEHF34OU19MahnRFdClHufThfGAjQ431teAIp0,1004
140
141
  shotgun/utils/source_detection.py,sha256=Co6Q03R3fT771TF3RzB-70stfjNP2S4F_ArZKibwzm8,454
141
142
  shotgun/utils/update_checker.py,sha256=IgzPHRhS1ETH7PnJR_dIx6lxgr1qHpCkMTgzUxvGjhI,7586
142
- shotgun_sh-0.2.1.dev1.dist-info/METADATA,sha256=KnWsl_ASqmh5tc4yblK9TGGltQxztoW_XYK7fNPCLqE,11190
143
- shotgun_sh-0.2.1.dev1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
144
- shotgun_sh-0.2.1.dev1.dist-info/entry_points.txt,sha256=asZxLU4QILneq0MWW10saVCZc4VWhZfb0wFZvERnzfA,45
145
- shotgun_sh-0.2.1.dev1.dist-info/licenses/LICENSE,sha256=YebsZl590zCHrF_acCU5pmNt0pnAfD2DmAnevJPB1tY,1065
146
- shotgun_sh-0.2.1.dev1.dist-info/RECORD,,
143
+ shotgun_sh-0.2.1.dev3.dist-info/METADATA,sha256=f_1fWjFpf7SbFyTIidsUTAgsu4wQSTn2QrH_3fE9ibA,11226
144
+ shotgun_sh-0.2.1.dev3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
145
+ shotgun_sh-0.2.1.dev3.dist-info/entry_points.txt,sha256=asZxLU4QILneq0MWW10saVCZc4VWhZfb0wFZvERnzfA,45
146
+ shotgun_sh-0.2.1.dev3.dist-info/licenses/LICENSE,sha256=YebsZl590zCHrF_acCU5pmNt0pnAfD2DmAnevJPB1tY,1065
147
+ shotgun_sh-0.2.1.dev3.dist-info/RECORD,,