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.
- shotgun/agents/agent_manager.py +11 -3
- shotgun/agents/config/manager.py +4 -20
- shotgun/agents/config/models.py +8 -0
- shotgun/agents/config/provider.py +17 -32
- shotgun/agents/models.py +7 -0
- shotgun/agents/tools/web_search/anthropic.py +8 -10
- shotgun/agents/usage_manager.py +159 -0
- shotgun/cli/config.py +16 -4
- shotgun/cli/feedback.py +1 -1
- shotgun/tui/app.py +1 -1
- shotgun/tui/screens/chat.py +36 -29
- shotgun/tui/screens/chat_screen/command_providers.py +127 -10
- shotgun/tui/screens/chat_screen/history.py +3 -1
- shotgun/tui/screens/model_picker.py +52 -14
- shotgun/tui/screens/provider_config.py +25 -6
- shotgun/utils/env_utils.py +12 -0
- {shotgun_sh-0.2.1.dev1.dist-info → shotgun_sh-0.2.1.dev3.dist-info}/METADATA +2 -1
- {shotgun_sh-0.2.1.dev1.dist-info → shotgun_sh-0.2.1.dev3.dist-info}/RECORD +21 -20
- {shotgun_sh-0.2.1.dev1.dist-info → shotgun_sh-0.2.1.dev3.dist-info}/WHEEL +0 -0
- {shotgun_sh-0.2.1.dev1.dist-info → shotgun_sh-0.2.1.dev3.dist-info}/entry_points.txt +0 -0
- {shotgun_sh-0.2.1.dev1.dist-info → shotgun_sh-0.2.1.dev3.dist-info}/licenses/LICENSE +0 -0
shotgun/agents/agent_manager.py
CHANGED
|
@@ -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
|
|
shotgun/agents/config/manager.py
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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."""
|
shotgun/agents/config/models.py
CHANGED
|
@@ -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
|
|
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-
|
|
156
|
-
model_name = config.selected_model or ModelName.
|
|
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
|
|
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
|
|
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-
|
|
235
|
-
model_name = requested_model if requested_model else ModelName.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
297
|
-
"""Get API key from config
|
|
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
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
print("
|
|
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
|
-
|
|
166
|
+
providers_to_show = [
|
|
166
167
|
("OpenAI", config.openai),
|
|
167
168
|
("Anthropic", config.anthropic),
|
|
168
169
|
("Google", config.google),
|
|
169
|
-
|
|
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
|
-
|
|
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
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("
|
|
111
|
+
self.notify("Feedback sent. Thank you!")
|
|
112
112
|
|
|
113
113
|
self.push_screen("feedback", callback=handle_feedback)
|
|
114
114
|
|
shotgun/tui/screens/chat.py
CHANGED
|
@@ -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
|
-
|
|
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 = {
|
|
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
|
|
611
|
+
# Spinner animation frames
|
|
603
612
|
spinner_frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
|
|
604
|
-
|
|
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
|
|
611
|
-
"""Update
|
|
620
|
+
def update_progress_display() -> None:
|
|
621
|
+
"""Update progress bar on timer - runs every 100ms."""
|
|
612
622
|
# Advance spinner frame
|
|
613
|
-
frame_idx = int(
|
|
614
|
-
|
|
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
|
-
|
|
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} {
|
|
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 (
|
|
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
|
-
|
|
652
|
-
spinner_state["percentage"] = overall_pct
|
|
660
|
+
progress_state["percentage"] = overall_pct
|
|
653
661
|
|
|
654
|
-
# Start
|
|
655
|
-
|
|
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
|
|
671
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
708
|
-
|
|
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
|
-
|
|
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.
|
|
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,
|
|
107
|
-
if
|
|
108
|
-
|
|
109
|
-
|
|
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.
|
|
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
|
-
|
|
20
|
-
|
|
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
|
|
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
|
|
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
|
|
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)
|
shotgun/utils/env_utils.py
CHANGED
|
@@ -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.
|
|
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=
|
|
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=
|
|
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=
|
|
25
|
-
shotgun/agents/config/models.py,sha256=
|
|
26
|
-
shotgun/agents/config/provider.py,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
129
|
-
shotgun/tui/screens/provider_config.py,sha256
|
|
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=
|
|
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=
|
|
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=
|
|
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.
|
|
143
|
-
shotgun_sh-0.2.1.
|
|
144
|
-
shotgun_sh-0.2.1.
|
|
145
|
-
shotgun_sh-0.2.1.
|
|
146
|
-
shotgun_sh-0.2.1.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|