shotgun-sh 0.1.16.dev2__py3-none-any.whl → 0.2.1__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/common.py +4 -5
- shotgun/agents/config/constants.py +23 -6
- shotgun/agents/config/manager.py +239 -76
- shotgun/agents/config/models.py +74 -84
- shotgun/agents/config/provider.py +174 -85
- shotgun/agents/history/compaction.py +1 -1
- shotgun/agents/history/history_processors.py +18 -9
- shotgun/agents/history/token_counting/__init__.py +31 -0
- shotgun/agents/history/token_counting/anthropic.py +89 -0
- shotgun/agents/history/token_counting/base.py +67 -0
- shotgun/agents/history/token_counting/openai.py +80 -0
- shotgun/agents/history/token_counting/sentencepiece_counter.py +119 -0
- shotgun/agents/history/token_counting/tokenizer_cache.py +90 -0
- shotgun/agents/history/token_counting/utils.py +147 -0
- shotgun/agents/history/token_estimation.py +12 -12
- shotgun/agents/llm.py +62 -0
- shotgun/agents/models.py +2 -2
- shotgun/agents/tools/web_search/__init__.py +42 -15
- shotgun/agents/tools/web_search/anthropic.py +54 -50
- shotgun/agents/tools/web_search/gemini.py +31 -20
- shotgun/agents/tools/web_search/openai.py +4 -4
- shotgun/build_constants.py +2 -2
- shotgun/cli/config.py +34 -63
- shotgun/cli/feedback.py +4 -2
- shotgun/cli/models.py +2 -2
- shotgun/codebase/core/ingestor.py +47 -8
- shotgun/codebase/core/manager.py +7 -3
- shotgun/codebase/models.py +4 -4
- shotgun/llm_proxy/__init__.py +16 -0
- shotgun/llm_proxy/clients.py +39 -0
- shotgun/llm_proxy/constants.py +8 -0
- shotgun/main.py +6 -0
- shotgun/posthog_telemetry.py +15 -11
- shotgun/sentry_telemetry.py +3 -3
- shotgun/shotgun_web/__init__.py +19 -0
- shotgun/shotgun_web/client.py +138 -0
- shotgun/shotgun_web/constants.py +17 -0
- shotgun/shotgun_web/models.py +47 -0
- shotgun/telemetry.py +7 -4
- shotgun/tui/app.py +26 -8
- shotgun/tui/screens/chat.py +2 -8
- shotgun/tui/screens/chat_screen/command_providers.py +118 -11
- shotgun/tui/screens/chat_screen/history.py +3 -1
- shotgun/tui/screens/feedback.py +2 -2
- shotgun/tui/screens/model_picker.py +327 -0
- shotgun/tui/screens/provider_config.py +118 -28
- shotgun/tui/screens/shotgun_auth.py +295 -0
- shotgun/tui/screens/welcome.py +176 -0
- shotgun/utils/env_utils.py +12 -0
- {shotgun_sh-0.1.16.dev2.dist-info → shotgun_sh-0.2.1.dist-info}/METADATA +2 -2
- {shotgun_sh-0.1.16.dev2.dist-info → shotgun_sh-0.2.1.dist-info}/RECORD +54 -37
- shotgun/agents/history/token_counting.py +0 -429
- {shotgun_sh-0.1.16.dev2.dist-info → shotgun_sh-0.2.1.dist-info}/WHEEL +0 -0
- {shotgun_sh-0.1.16.dev2.dist-info → shotgun_sh-0.2.1.dist-info}/entry_points.txt +0 -0
- {shotgun_sh-0.1.16.dev2.dist-info → shotgun_sh-0.2.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""HTTP client for Shotgun Web API."""
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
|
|
5
|
+
from shotgun.logging_config import get_logger
|
|
6
|
+
|
|
7
|
+
from .constants import (
|
|
8
|
+
SHOTGUN_WEB_BASE_URL,
|
|
9
|
+
UNIFICATION_TOKEN_CREATE_PATH,
|
|
10
|
+
UNIFICATION_TOKEN_STATUS_PATH,
|
|
11
|
+
)
|
|
12
|
+
from .models import (
|
|
13
|
+
TokenCreateRequest,
|
|
14
|
+
TokenCreateResponse,
|
|
15
|
+
TokenStatusResponse,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
logger = get_logger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ShotgunWebClient:
|
|
22
|
+
"""HTTP client for Shotgun Web API."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, base_url: str | None = None, timeout: float = 10.0):
|
|
25
|
+
"""Initialize Shotgun Web client.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
base_url: Base URL for Shotgun Web API. If None, uses SHOTGUN_WEB_BASE_URL
|
|
29
|
+
timeout: Request timeout in seconds
|
|
30
|
+
"""
|
|
31
|
+
self.base_url = base_url or SHOTGUN_WEB_BASE_URL
|
|
32
|
+
self.timeout = timeout
|
|
33
|
+
|
|
34
|
+
def create_unification_token(self, shotgun_instance_id: str) -> TokenCreateResponse:
|
|
35
|
+
"""Create a unification token for CLI authentication.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
shotgun_instance_id: UUID for this shotgun instance
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Token creation response with token and auth URL
|
|
42
|
+
|
|
43
|
+
Raises:
|
|
44
|
+
httpx.HTTPError: If request fails
|
|
45
|
+
"""
|
|
46
|
+
url = f"{self.base_url}{UNIFICATION_TOKEN_CREATE_PATH}"
|
|
47
|
+
request_data = TokenCreateRequest(shotgun_instance_id=shotgun_instance_id)
|
|
48
|
+
|
|
49
|
+
logger.debug("Creating unification token for instance %s", shotgun_instance_id)
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
response = httpx.post(
|
|
53
|
+
url,
|
|
54
|
+
json=request_data.model_dump(),
|
|
55
|
+
timeout=self.timeout,
|
|
56
|
+
)
|
|
57
|
+
response.raise_for_status()
|
|
58
|
+
|
|
59
|
+
data = response.json()
|
|
60
|
+
result = TokenCreateResponse.model_validate(data)
|
|
61
|
+
|
|
62
|
+
logger.info(
|
|
63
|
+
"Successfully created unification token, expires in %d seconds",
|
|
64
|
+
result.expires_in_seconds,
|
|
65
|
+
)
|
|
66
|
+
return result
|
|
67
|
+
|
|
68
|
+
except httpx.HTTPError as e:
|
|
69
|
+
logger.error("Failed to create unification token: %s", e)
|
|
70
|
+
raise
|
|
71
|
+
|
|
72
|
+
def check_token_status(self, token: str) -> TokenStatusResponse:
|
|
73
|
+
"""Check token status and get keys if completed.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
token: Unification token to check
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Token status response with status and keys (if completed)
|
|
80
|
+
|
|
81
|
+
Raises:
|
|
82
|
+
httpx.HTTPStatusError: If token not found (404) or expired (410)
|
|
83
|
+
httpx.HTTPError: For other request failures
|
|
84
|
+
"""
|
|
85
|
+
url = f"{self.base_url}{UNIFICATION_TOKEN_STATUS_PATH.format(token=token)}"
|
|
86
|
+
|
|
87
|
+
logger.debug("Checking status for token %s...", token[:8])
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
response = httpx.get(url, timeout=self.timeout)
|
|
91
|
+
response.raise_for_status()
|
|
92
|
+
|
|
93
|
+
data = response.json()
|
|
94
|
+
result = TokenStatusResponse.model_validate(data)
|
|
95
|
+
|
|
96
|
+
logger.debug("Token status: %s", result.status)
|
|
97
|
+
return result
|
|
98
|
+
|
|
99
|
+
except httpx.HTTPStatusError as e:
|
|
100
|
+
if e.response.status_code == 404:
|
|
101
|
+
logger.error("Token not found: %s", token[:8])
|
|
102
|
+
elif e.response.status_code == 410:
|
|
103
|
+
logger.error("Token expired: %s", token[:8])
|
|
104
|
+
raise
|
|
105
|
+
except httpx.HTTPError as e:
|
|
106
|
+
logger.error("Failed to check token status: %s", e)
|
|
107
|
+
raise
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# Convenience functions for standalone use
|
|
111
|
+
def create_unification_token(shotgun_instance_id: str) -> TokenCreateResponse:
|
|
112
|
+
"""Create a unification token.
|
|
113
|
+
|
|
114
|
+
Convenience function that creates a client and calls create_unification_token.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
shotgun_instance_id: UUID for this shotgun instance
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
Token creation response
|
|
121
|
+
"""
|
|
122
|
+
client = ShotgunWebClient()
|
|
123
|
+
return client.create_unification_token(shotgun_instance_id)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def check_token_status(token: str) -> TokenStatusResponse:
|
|
127
|
+
"""Check token status.
|
|
128
|
+
|
|
129
|
+
Convenience function that creates a client and calls check_token_status.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
token: Unification token to check
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
Token status response
|
|
136
|
+
"""
|
|
137
|
+
client = ShotgunWebClient()
|
|
138
|
+
return client.check_token_status(token)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Constants for Shotgun Web API."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
# Shotgun Web API base URL
|
|
6
|
+
# Default to production URL, can be overridden with environment variable
|
|
7
|
+
SHOTGUN_WEB_BASE_URL = os.environ.get(
|
|
8
|
+
"SHOTGUN_WEB_BASE_URL", "https://api-701197220809.us-east1.run.app"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
# API endpoints
|
|
12
|
+
UNIFICATION_TOKEN_CREATE_PATH = "/api/unification/token/create" # noqa: S105
|
|
13
|
+
UNIFICATION_TOKEN_STATUS_PATH = "/api/unification/token/{token}/status" # noqa: S105
|
|
14
|
+
|
|
15
|
+
# Polling configuration
|
|
16
|
+
DEFAULT_POLL_INTERVAL_SECONDS = 3
|
|
17
|
+
DEFAULT_TOKEN_TIMEOUT_SECONDS = 1800 # 30 minutes
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Pydantic models for Shotgun Web API."""
|
|
2
|
+
|
|
3
|
+
from enum import StrEnum
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TokenStatus(StrEnum):
|
|
9
|
+
"""Token status enum matching API specification."""
|
|
10
|
+
|
|
11
|
+
PENDING = "pending"
|
|
12
|
+
COMPLETED = "completed"
|
|
13
|
+
AWAITING_PAYMENT = "awaiting_payment"
|
|
14
|
+
EXPIRED = "expired"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TokenCreateRequest(BaseModel):
|
|
18
|
+
"""Request model for creating a unification token."""
|
|
19
|
+
|
|
20
|
+
shotgun_instance_id: str = Field(
|
|
21
|
+
description="CLI-provided UUID for shotgun instance"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TokenCreateResponse(BaseModel):
|
|
26
|
+
"""Response model for token creation."""
|
|
27
|
+
|
|
28
|
+
token: str = Field(description="Secure authentication token")
|
|
29
|
+
auth_url: str = Field(description="Web authentication URL for user to complete")
|
|
30
|
+
expires_in_seconds: int = Field(description="Token expiration time in seconds")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class TokenStatusResponse(BaseModel):
|
|
34
|
+
"""Response model for token status check."""
|
|
35
|
+
|
|
36
|
+
status: TokenStatus = Field(description="Current token status")
|
|
37
|
+
supabase_key: str | None = Field(
|
|
38
|
+
default=None,
|
|
39
|
+
description="Supabase user JWT (only returned when status=completed)",
|
|
40
|
+
)
|
|
41
|
+
litellm_key: str | None = Field(
|
|
42
|
+
default=None,
|
|
43
|
+
description="LiteLLM virtual key (only returned when status=completed)",
|
|
44
|
+
)
|
|
45
|
+
message: str | None = Field(
|
|
46
|
+
default=None, description="Human-readable status message"
|
|
47
|
+
)
|
shotgun/telemetry.py
CHANGED
|
@@ -72,12 +72,15 @@ def setup_logfire_observability() -> bool:
|
|
|
72
72
|
from shotgun.agents.config import get_config_manager
|
|
73
73
|
|
|
74
74
|
config_manager = get_config_manager()
|
|
75
|
-
|
|
75
|
+
shotgun_instance_id = config_manager.get_shotgun_instance_id()
|
|
76
76
|
|
|
77
|
-
# Set
|
|
78
|
-
ctx = baggage.set_baggage("
|
|
77
|
+
# Set shotgun_instance_id as baggage in global context - this will be included in all logs/spans
|
|
78
|
+
ctx = baggage.set_baggage("shotgun_instance_id", shotgun_instance_id)
|
|
79
79
|
context.attach(ctx)
|
|
80
|
-
logger.debug(
|
|
80
|
+
logger.debug(
|
|
81
|
+
"Logfire user context set with shotgun_instance_id: %s",
|
|
82
|
+
shotgun_instance_id,
|
|
83
|
+
)
|
|
81
84
|
except Exception as e:
|
|
82
85
|
logger.warning("Failed to set Logfire user context: %s", e)
|
|
83
86
|
|
shotgun/tui/app.py
CHANGED
|
@@ -8,13 +8,16 @@ from textual.screen import Screen
|
|
|
8
8
|
from shotgun.agents.config import ConfigManager, get_config_manager
|
|
9
9
|
from shotgun.logging_config import get_logger
|
|
10
10
|
from shotgun.tui.screens.splash import SplashScreen
|
|
11
|
+
from shotgun.utils.env_utils import is_shotgun_account_enabled
|
|
11
12
|
from shotgun.utils.file_system_utils import get_shotgun_base_path
|
|
12
13
|
from shotgun.utils.update_checker import perform_auto_update_async
|
|
13
14
|
|
|
14
15
|
from .screens.chat import ChatScreen
|
|
15
16
|
from .screens.directory_setup import DirectorySetupScreen
|
|
16
17
|
from .screens.feedback import FeedbackScreen
|
|
18
|
+
from .screens.model_picker import ModelPickerScreen
|
|
17
19
|
from .screens.provider_config import ProviderConfigScreen
|
|
20
|
+
from .screens.welcome import WelcomeScreen
|
|
18
21
|
|
|
19
22
|
logger = get_logger(__name__)
|
|
20
23
|
|
|
@@ -23,6 +26,7 @@ class ShotgunApp(App[None]):
|
|
|
23
26
|
SCREENS = {
|
|
24
27
|
"chat": ChatScreen,
|
|
25
28
|
"provider_config": ProviderConfigScreen,
|
|
29
|
+
"model_picker": ModelPickerScreen,
|
|
26
30
|
"directory_setup": DirectorySetupScreen,
|
|
27
31
|
"feedback": FeedbackScreen,
|
|
28
32
|
}
|
|
@@ -58,20 +62,34 @@ class ShotgunApp(App[None]):
|
|
|
58
62
|
def refresh_startup_screen(self) -> None:
|
|
59
63
|
"""Push the appropriate screen based on configured providers."""
|
|
60
64
|
if not self.config_manager.has_any_provider_key():
|
|
61
|
-
|
|
65
|
+
# If Shotgun Account is enabled, show welcome screen with choice
|
|
66
|
+
# Otherwise, go directly to provider config (BYOK only)
|
|
67
|
+
if is_shotgun_account_enabled():
|
|
68
|
+
if isinstance(self.screen, WelcomeScreen):
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
self.push_screen(
|
|
72
|
+
WelcomeScreen(),
|
|
73
|
+
callback=lambda _arg: self.refresh_startup_screen(),
|
|
74
|
+
)
|
|
75
|
+
return
|
|
76
|
+
else:
|
|
77
|
+
if isinstance(self.screen, ProviderConfigScreen):
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
self.push_screen(
|
|
81
|
+
ProviderConfigScreen(),
|
|
82
|
+
callback=lambda _arg: self.refresh_startup_screen(),
|
|
83
|
+
)
|
|
62
84
|
return
|
|
63
|
-
|
|
64
|
-
self.push_screen(
|
|
65
|
-
"provider_config", callback=lambda _arg: self.refresh_startup_screen()
|
|
66
|
-
)
|
|
67
|
-
return
|
|
68
85
|
|
|
69
86
|
if not self.check_local_shotgun_directory_exists():
|
|
70
87
|
if isinstance(self.screen, DirectorySetupScreen):
|
|
71
88
|
return
|
|
72
89
|
|
|
73
90
|
self.push_screen(
|
|
74
|
-
|
|
91
|
+
DirectorySetupScreen(),
|
|
92
|
+
callback=lambda _arg: self.refresh_startup_screen(),
|
|
75
93
|
)
|
|
76
94
|
return
|
|
77
95
|
|
|
@@ -108,7 +126,7 @@ class ShotgunApp(App[None]):
|
|
|
108
126
|
submit_feedback_survey(feedback)
|
|
109
127
|
self.notify("Feedback sent. Thank you!")
|
|
110
128
|
|
|
111
|
-
self.push_screen(
|
|
129
|
+
self.push_screen(FeedbackScreen(), callback=handle_feedback)
|
|
112
130
|
|
|
113
131
|
|
|
114
132
|
def run(no_update_check: bool = False, continue_session: bool = False) -> None:
|
shotgun/tui/screens/chat.py
CHANGED
|
@@ -54,11 +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
|
-
|
|
61
|
-
UsageProvider,
|
|
58
|
+
UnifiedCommandProvider,
|
|
62
59
|
)
|
|
63
60
|
|
|
64
61
|
logger = logging.getLogger(__name__)
|
|
@@ -233,10 +230,7 @@ class ChatScreen(Screen[None]):
|
|
|
233
230
|
]
|
|
234
231
|
|
|
235
232
|
COMMANDS = {
|
|
236
|
-
|
|
237
|
-
ProviderSetupProvider,
|
|
238
|
-
CodebaseCommandProvider,
|
|
239
|
-
UsageProvider,
|
|
233
|
+
UnifiedCommandProvider,
|
|
240
234
|
}
|
|
241
235
|
|
|
242
236
|
value = reactive("")
|
|
@@ -5,6 +5,8 @@ from textual.command import DiscoveryHit, Hit, Provider
|
|
|
5
5
|
|
|
6
6
|
from shotgun.agents.models import AgentType
|
|
7
7
|
from shotgun.codebase.models import CodebaseGraph
|
|
8
|
+
from shotgun.tui.screens.model_picker import ModelPickerScreen
|
|
9
|
+
from shotgun.tui.screens.provider_config import ProviderConfigScreen
|
|
8
10
|
|
|
9
11
|
if TYPE_CHECKING:
|
|
10
12
|
from shotgun.tui.screens.chat import ChatScreen
|
|
@@ -139,7 +141,11 @@ class ProviderSetupProvider(Provider):
|
|
|
139
141
|
|
|
140
142
|
def open_provider_config(self) -> None:
|
|
141
143
|
"""Show the provider configuration screen."""
|
|
142
|
-
self.chat_screen.app.push_screen(
|
|
144
|
+
self.chat_screen.app.push_screen(ProviderConfigScreen())
|
|
145
|
+
|
|
146
|
+
def open_model_picker(self) -> None:
|
|
147
|
+
"""Show the model picker screen."""
|
|
148
|
+
self.chat_screen.app.push_screen(ModelPickerScreen())
|
|
143
149
|
|
|
144
150
|
async def discover(self) -> AsyncGenerator[DiscoveryHit, None]:
|
|
145
151
|
yield DiscoveryHit(
|
|
@@ -147,9 +153,15 @@ class ProviderSetupProvider(Provider):
|
|
|
147
153
|
self.open_provider_config,
|
|
148
154
|
help="⚙️ Manage API keys for available providers",
|
|
149
155
|
)
|
|
156
|
+
yield DiscoveryHit(
|
|
157
|
+
"Select AI Model",
|
|
158
|
+
self.open_model_picker,
|
|
159
|
+
help="🤖 Choose which AI model to use",
|
|
160
|
+
)
|
|
150
161
|
|
|
151
162
|
async def search(self, query: str) -> AsyncGenerator[Hit, None]:
|
|
152
163
|
matcher = self.matcher(query)
|
|
164
|
+
|
|
153
165
|
title = "Open Provider Setup"
|
|
154
166
|
score = matcher.match(title)
|
|
155
167
|
if score > 0:
|
|
@@ -160,6 +172,16 @@ class ProviderSetupProvider(Provider):
|
|
|
160
172
|
help="⚙️ Manage API keys for available providers",
|
|
161
173
|
)
|
|
162
174
|
|
|
175
|
+
title = "Select AI Model"
|
|
176
|
+
score = matcher.match(title)
|
|
177
|
+
if score > 0:
|
|
178
|
+
yield Hit(
|
|
179
|
+
score,
|
|
180
|
+
matcher.highlight(title),
|
|
181
|
+
self.open_model_picker,
|
|
182
|
+
help="🤖 Choose which AI model to use",
|
|
183
|
+
)
|
|
184
|
+
|
|
163
185
|
|
|
164
186
|
class CodebaseCommandProvider(Provider):
|
|
165
187
|
"""Command palette entries for codebase management."""
|
|
@@ -171,30 +193,30 @@ class CodebaseCommandProvider(Provider):
|
|
|
171
193
|
return cast(ChatScreen, self.screen)
|
|
172
194
|
|
|
173
195
|
async def discover(self) -> AsyncGenerator[DiscoveryHit, None]:
|
|
174
|
-
yield DiscoveryHit(
|
|
175
|
-
"Codebase: Index Codebase",
|
|
176
|
-
self.chat_screen.index_codebase_command,
|
|
177
|
-
help="Index a repository into the codebase graph",
|
|
178
|
-
)
|
|
179
196
|
yield DiscoveryHit(
|
|
180
197
|
"Codebase: Delete Codebase Index",
|
|
181
198
|
self.chat_screen.delete_codebase_command,
|
|
182
199
|
help="Delete an existing codebase index",
|
|
183
200
|
)
|
|
201
|
+
yield DiscoveryHit(
|
|
202
|
+
"Codebase: Index Codebase",
|
|
203
|
+
self.chat_screen.index_codebase_command,
|
|
204
|
+
help="Index a repository into the codebase graph",
|
|
205
|
+
)
|
|
184
206
|
|
|
185
207
|
async def search(self, query: str) -> AsyncGenerator[Hit, None]:
|
|
186
208
|
matcher = self.matcher(query)
|
|
187
209
|
commands = [
|
|
188
|
-
(
|
|
189
|
-
"Codebase: Index Codebase",
|
|
190
|
-
self.chat_screen.index_codebase_command,
|
|
191
|
-
"Index a repository into the codebase graph",
|
|
192
|
-
),
|
|
193
210
|
(
|
|
194
211
|
"Codebase: Delete Codebase Index",
|
|
195
212
|
self.chat_screen.delete_codebase_command,
|
|
196
213
|
"Delete an existing codebase index",
|
|
197
214
|
),
|
|
215
|
+
(
|
|
216
|
+
"Codebase: Index Codebase",
|
|
217
|
+
self.chat_screen.index_codebase_command,
|
|
218
|
+
"Index a repository into the codebase graph",
|
|
219
|
+
),
|
|
198
220
|
]
|
|
199
221
|
for title, callback, help_text in commands:
|
|
200
222
|
score = matcher.match(title)
|
|
@@ -249,3 +271,88 @@ class DeleteCodebasePaletteProvider(Provider):
|
|
|
249
271
|
),
|
|
250
272
|
help=graph.repo_path,
|
|
251
273
|
)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
class UnifiedCommandProvider(Provider):
|
|
277
|
+
"""Unified command provider with all commands in alphabetical order."""
|
|
278
|
+
|
|
279
|
+
@property
|
|
280
|
+
def chat_screen(self) -> "ChatScreen":
|
|
281
|
+
from shotgun.tui.screens.chat import ChatScreen
|
|
282
|
+
|
|
283
|
+
return cast(ChatScreen, self.screen)
|
|
284
|
+
|
|
285
|
+
def open_provider_config(self) -> None:
|
|
286
|
+
"""Show the provider configuration screen."""
|
|
287
|
+
self.chat_screen.app.push_screen(ProviderConfigScreen())
|
|
288
|
+
|
|
289
|
+
def open_model_picker(self) -> None:
|
|
290
|
+
"""Show the model picker screen."""
|
|
291
|
+
self.chat_screen.app.push_screen(ModelPickerScreen())
|
|
292
|
+
|
|
293
|
+
async def discover(self) -> AsyncGenerator[DiscoveryHit, None]:
|
|
294
|
+
"""Provide commands in alphabetical order when palette opens."""
|
|
295
|
+
# Alphabetically ordered commands
|
|
296
|
+
yield DiscoveryHit(
|
|
297
|
+
"Codebase: Delete Codebase Index",
|
|
298
|
+
self.chat_screen.delete_codebase_command,
|
|
299
|
+
help="Delete an existing codebase index",
|
|
300
|
+
)
|
|
301
|
+
yield DiscoveryHit(
|
|
302
|
+
"Codebase: Index Codebase",
|
|
303
|
+
self.chat_screen.index_codebase_command,
|
|
304
|
+
help="Index a repository into the codebase graph",
|
|
305
|
+
)
|
|
306
|
+
yield DiscoveryHit(
|
|
307
|
+
"Open Provider Setup",
|
|
308
|
+
self.open_provider_config,
|
|
309
|
+
help="⚙️ Manage API keys for available providers",
|
|
310
|
+
)
|
|
311
|
+
yield DiscoveryHit(
|
|
312
|
+
"Select AI Model",
|
|
313
|
+
self.open_model_picker,
|
|
314
|
+
help="🤖 Choose which AI model to use",
|
|
315
|
+
)
|
|
316
|
+
yield DiscoveryHit(
|
|
317
|
+
"Show usage",
|
|
318
|
+
self.chat_screen.action_show_usage,
|
|
319
|
+
help="Display usage information for the current session",
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
async def search(self, query: str) -> AsyncGenerator[Hit, None]:
|
|
323
|
+
"""Search for commands in alphabetical order."""
|
|
324
|
+
matcher = self.matcher(query)
|
|
325
|
+
|
|
326
|
+
# Define all commands in alphabetical order
|
|
327
|
+
commands = [
|
|
328
|
+
(
|
|
329
|
+
"Codebase: Delete Codebase Index",
|
|
330
|
+
self.chat_screen.delete_codebase_command,
|
|
331
|
+
"Delete an existing codebase index",
|
|
332
|
+
),
|
|
333
|
+
(
|
|
334
|
+
"Codebase: Index Codebase",
|
|
335
|
+
self.chat_screen.index_codebase_command,
|
|
336
|
+
"Index a repository into the codebase graph",
|
|
337
|
+
),
|
|
338
|
+
(
|
|
339
|
+
"Open Provider Setup",
|
|
340
|
+
self.open_provider_config,
|
|
341
|
+
"⚙️ Manage API keys for available providers",
|
|
342
|
+
),
|
|
343
|
+
(
|
|
344
|
+
"Select AI Model",
|
|
345
|
+
self.open_model_picker,
|
|
346
|
+
"🤖 Choose which AI model to use",
|
|
347
|
+
),
|
|
348
|
+
(
|
|
349
|
+
"Show usage",
|
|
350
|
+
self.chat_screen.action_show_usage,
|
|
351
|
+
"Display usage information for the current session",
|
|
352
|
+
),
|
|
353
|
+
]
|
|
354
|
+
|
|
355
|
+
for title, callback, help_text in commands:
|
|
356
|
+
score = matcher.match(title)
|
|
357
|
+
if score > 0:
|
|
358
|
+
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"
|
shotgun/tui/screens/feedback.py
CHANGED
|
@@ -182,12 +182,12 @@ class FeedbackScreen(Screen[Feedback | None]):
|
|
|
182
182
|
return
|
|
183
183
|
|
|
184
184
|
app = cast("ShotgunApp", self.app)
|
|
185
|
-
|
|
185
|
+
shotgun_instance_id = app.config_manager.get_shotgun_instance_id()
|
|
186
186
|
|
|
187
187
|
feedback = Feedback(
|
|
188
188
|
kind=self.selected_kind,
|
|
189
189
|
description=description,
|
|
190
|
-
|
|
190
|
+
shotgun_instance_id=shotgun_instance_id,
|
|
191
191
|
)
|
|
192
192
|
|
|
193
193
|
self.dismiss(feedback)
|