shotgun-sh 0.2.3.dev2__py3-none-any.whl → 0.2.11.dev1__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 +524 -58
- shotgun/agents/common.py +62 -62
- shotgun/agents/config/constants.py +0 -6
- shotgun/agents/config/manager.py +14 -3
- shotgun/agents/config/models.py +16 -0
- shotgun/agents/config/provider.py +68 -13
- shotgun/agents/context_analyzer/__init__.py +28 -0
- shotgun/agents/context_analyzer/analyzer.py +493 -0
- shotgun/agents/context_analyzer/constants.py +9 -0
- shotgun/agents/context_analyzer/formatter.py +115 -0
- shotgun/agents/context_analyzer/models.py +212 -0
- shotgun/agents/conversation_history.py +125 -2
- shotgun/agents/conversation_manager.py +24 -2
- shotgun/agents/export.py +4 -5
- shotgun/agents/history/compaction.py +9 -4
- shotgun/agents/history/context_extraction.py +93 -6
- shotgun/agents/history/history_processors.py +14 -2
- shotgun/agents/history/token_counting/anthropic.py +32 -10
- shotgun/agents/models.py +50 -2
- shotgun/agents/plan.py +4 -5
- shotgun/agents/research.py +4 -5
- shotgun/agents/specify.py +4 -5
- shotgun/agents/tasks.py +4 -5
- shotgun/agents/tools/__init__.py +0 -2
- shotgun/agents/tools/codebase/codebase_shell.py +6 -0
- shotgun/agents/tools/codebase/directory_lister.py +6 -0
- shotgun/agents/tools/codebase/file_read.py +6 -0
- shotgun/agents/tools/codebase/query_graph.py +6 -0
- shotgun/agents/tools/codebase/retrieve_code.py +6 -0
- shotgun/agents/tools/file_management.py +71 -9
- shotgun/agents/tools/registry.py +217 -0
- shotgun/agents/tools/web_search/__init__.py +24 -12
- shotgun/agents/tools/web_search/anthropic.py +24 -3
- shotgun/agents/tools/web_search/gemini.py +22 -10
- shotgun/agents/tools/web_search/openai.py +21 -12
- shotgun/api_endpoints.py +7 -3
- shotgun/build_constants.py +1 -1
- shotgun/cli/clear.py +52 -0
- shotgun/cli/compact.py +186 -0
- shotgun/cli/context.py +111 -0
- shotgun/cli/models.py +1 -0
- shotgun/cli/update.py +16 -2
- shotgun/codebase/core/manager.py +10 -1
- shotgun/llm_proxy/__init__.py +5 -2
- shotgun/llm_proxy/clients.py +12 -7
- shotgun/logging_config.py +8 -10
- shotgun/main.py +70 -10
- shotgun/posthog_telemetry.py +9 -3
- shotgun/prompts/agents/export.j2 +18 -1
- shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +5 -1
- shotgun/prompts/agents/partials/interactive_mode.j2 +24 -7
- shotgun/prompts/agents/plan.j2 +1 -1
- shotgun/prompts/agents/research.j2 +1 -1
- shotgun/prompts/agents/specify.j2 +270 -3
- shotgun/prompts/agents/state/system_state.j2 +4 -0
- shotgun/prompts/agents/tasks.j2 +1 -1
- shotgun/prompts/loader.py +2 -2
- shotgun/prompts/tools/web_search.j2 +14 -0
- shotgun/sentry_telemetry.py +4 -15
- shotgun/settings.py +238 -0
- shotgun/telemetry.py +15 -32
- shotgun/tui/app.py +203 -9
- shotgun/tui/commands/__init__.py +1 -1
- shotgun/tui/components/context_indicator.py +136 -0
- shotgun/tui/components/mode_indicator.py +70 -0
- shotgun/tui/components/status_bar.py +48 -0
- shotgun/tui/containers.py +93 -0
- shotgun/tui/dependencies.py +39 -0
- shotgun/tui/protocols.py +45 -0
- shotgun/tui/screens/chat/__init__.py +5 -0
- shotgun/tui/screens/chat/chat.tcss +54 -0
- shotgun/tui/screens/chat/chat_screen.py +1110 -0
- shotgun/tui/screens/chat/codebase_index_prompt_screen.py +64 -0
- shotgun/tui/screens/chat/codebase_index_selection.py +12 -0
- shotgun/tui/screens/chat/help_text.py +39 -0
- shotgun/tui/screens/chat/prompt_history.py +48 -0
- shotgun/tui/screens/chat.tcss +11 -0
- shotgun/tui/screens/chat_screen/command_providers.py +68 -2
- shotgun/tui/screens/chat_screen/history/__init__.py +22 -0
- shotgun/tui/screens/chat_screen/history/agent_response.py +66 -0
- shotgun/tui/screens/chat_screen/history/chat_history.py +116 -0
- shotgun/tui/screens/chat_screen/history/formatters.py +115 -0
- shotgun/tui/screens/chat_screen/history/partial_response.py +43 -0
- shotgun/tui/screens/chat_screen/history/user_question.py +42 -0
- shotgun/tui/screens/confirmation_dialog.py +151 -0
- shotgun/tui/screens/model_picker.py +30 -6
- shotgun/tui/screens/pipx_migration.py +153 -0
- shotgun/tui/screens/welcome.py +24 -5
- shotgun/tui/services/__init__.py +5 -0
- shotgun/tui/services/conversation_service.py +182 -0
- shotgun/tui/state/__init__.py +7 -0
- shotgun/tui/state/processing_state.py +185 -0
- shotgun/tui/widgets/__init__.py +5 -0
- shotgun/tui/widgets/widget_coordinator.py +247 -0
- shotgun/utils/datetime_utils.py +77 -0
- shotgun/utils/file_system_utils.py +3 -2
- shotgun/utils/update_checker.py +69 -14
- shotgun_sh-0.2.11.dev1.dist-info/METADATA +129 -0
- shotgun_sh-0.2.11.dev1.dist-info/RECORD +190 -0
- {shotgun_sh-0.2.3.dev2.dist-info → shotgun_sh-0.2.11.dev1.dist-info}/entry_points.txt +1 -0
- {shotgun_sh-0.2.3.dev2.dist-info → shotgun_sh-0.2.11.dev1.dist-info}/licenses/LICENSE +1 -1
- shotgun/agents/tools/user_interaction.py +0 -37
- shotgun/tui/screens/chat.py +0 -804
- shotgun/tui/screens/chat_screen/history.py +0 -352
- shotgun_sh-0.2.3.dev2.dist-info/METADATA +0 -467
- shotgun_sh-0.2.3.dev2.dist-info/RECORD +0 -154
- {shotgun_sh-0.2.3.dev2.dist-info → shotgun_sh-0.2.11.dev1.dist-info}/WHEEL +0 -0
shotgun/telemetry.py
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
"""Observability setup for Logfire."""
|
|
2
2
|
|
|
3
|
-
import os
|
|
4
|
-
|
|
5
3
|
from shotgun.logging_config import get_early_logger
|
|
6
|
-
from shotgun.
|
|
4
|
+
from shotgun.settings import settings
|
|
7
5
|
|
|
8
6
|
# Use early logger to prevent automatic StreamHandler creation
|
|
9
7
|
logger = get_early_logger(__name__)
|
|
@@ -15,36 +13,13 @@ def setup_logfire_observability() -> bool:
|
|
|
15
13
|
Returns:
|
|
16
14
|
True if Logfire was successfully set up, False otherwise
|
|
17
15
|
"""
|
|
18
|
-
#
|
|
19
|
-
logfire_enabled =
|
|
20
|
-
logfire_token =
|
|
21
|
-
|
|
22
|
-
try:
|
|
23
|
-
from shotgun.build_constants import LOGFIRE_ENABLED, LOGFIRE_TOKEN
|
|
24
|
-
|
|
25
|
-
# Use build constants if they're not empty
|
|
26
|
-
if LOGFIRE_ENABLED:
|
|
27
|
-
logfire_enabled = LOGFIRE_ENABLED
|
|
28
|
-
if LOGFIRE_TOKEN:
|
|
29
|
-
logfire_token = LOGFIRE_TOKEN
|
|
30
|
-
except ImportError:
|
|
31
|
-
# No build constants available
|
|
32
|
-
pass
|
|
33
|
-
|
|
34
|
-
# Fall back to environment variables if not set from build constants
|
|
35
|
-
if not logfire_enabled:
|
|
36
|
-
logfire_enabled = os.getenv("LOGFIRE_ENABLED", "false")
|
|
37
|
-
if not logfire_token:
|
|
38
|
-
logfire_token = os.getenv("LOGFIRE_TOKEN")
|
|
39
|
-
|
|
40
|
-
# Allow environment variable to override and disable Logfire
|
|
41
|
-
env_override = os.getenv("LOGFIRE_ENABLED")
|
|
42
|
-
if env_override and is_falsy(env_override):
|
|
43
|
-
logfire_enabled = env_override
|
|
16
|
+
# Get Logfire configuration from settings (handles build constants + env vars)
|
|
17
|
+
logfire_enabled = settings.telemetry.logfire_enabled
|
|
18
|
+
logfire_token = settings.telemetry.logfire_token
|
|
44
19
|
|
|
45
20
|
# Check if Logfire observability is enabled
|
|
46
|
-
if not
|
|
47
|
-
logger.debug("Logfire observability disabled
|
|
21
|
+
if not logfire_enabled:
|
|
22
|
+
logger.debug("Logfire observability disabled")
|
|
48
23
|
return False
|
|
49
24
|
|
|
50
25
|
try:
|
|
@@ -52,7 +27,7 @@ def setup_logfire_observability() -> bool:
|
|
|
52
27
|
|
|
53
28
|
# Check for Logfire token
|
|
54
29
|
if not logfire_token:
|
|
55
|
-
logger.warning("
|
|
30
|
+
logger.warning("Logfire token not set, Logfire observability disabled")
|
|
56
31
|
return False
|
|
57
32
|
|
|
58
33
|
# Configure Logfire
|
|
@@ -65,6 +40,14 @@ def setup_logfire_observability() -> bool:
|
|
|
65
40
|
# Instrument Pydantic AI for better observability
|
|
66
41
|
logfire.instrument_pydantic_ai()
|
|
67
42
|
|
|
43
|
+
# Add LogfireLoggingHandler to root logger so logfire logs also go to file
|
|
44
|
+
import logging
|
|
45
|
+
|
|
46
|
+
root_logger = logging.getLogger()
|
|
47
|
+
logfire_handler = logfire.LogfireLoggingHandler()
|
|
48
|
+
root_logger.addHandler(logfire_handler)
|
|
49
|
+
logger.debug("Added LogfireLoggingHandler to root logger for file integration")
|
|
50
|
+
|
|
68
51
|
# Set user context using baggage for all logs and spans
|
|
69
52
|
try:
|
|
70
53
|
from opentelemetry import baggage, context
|
shotgun/tui/app.py
CHANGED
|
@@ -6,15 +6,21 @@ from textual.binding import Binding
|
|
|
6
6
|
from textual.screen import Screen
|
|
7
7
|
|
|
8
8
|
from shotgun.agents.config import ConfigManager, get_config_manager
|
|
9
|
+
from shotgun.agents.models import AgentType
|
|
9
10
|
from shotgun.logging_config import get_logger
|
|
11
|
+
from shotgun.tui.containers import TUIContainer
|
|
10
12
|
from shotgun.tui.screens.splash import SplashScreen
|
|
11
13
|
from shotgun.utils.file_system_utils import get_shotgun_base_path
|
|
12
|
-
from shotgun.utils.update_checker import
|
|
14
|
+
from shotgun.utils.update_checker import (
|
|
15
|
+
detect_installation_method,
|
|
16
|
+
perform_auto_update_async,
|
|
17
|
+
)
|
|
13
18
|
|
|
14
19
|
from .screens.chat import ChatScreen
|
|
15
20
|
from .screens.directory_setup import DirectorySetupScreen
|
|
16
21
|
from .screens.feedback import FeedbackScreen
|
|
17
22
|
from .screens.model_picker import ModelPickerScreen
|
|
23
|
+
from .screens.pipx_migration import PipxMigrationScreen
|
|
18
24
|
from .screens.provider_config import ProviderConfigScreen
|
|
19
25
|
from .screens.welcome import WelcomeScreen
|
|
20
26
|
|
|
@@ -22,8 +28,9 @@ logger = get_logger(__name__)
|
|
|
22
28
|
|
|
23
29
|
|
|
24
30
|
class ShotgunApp(App[None]):
|
|
31
|
+
# ChatScreen removed from SCREENS dict since it requires dependency injection
|
|
32
|
+
# and is instantiated manually in refresh_startup_screen()
|
|
25
33
|
SCREENS = {
|
|
26
|
-
"chat": ChatScreen,
|
|
27
34
|
"provider_config": ProviderConfigScreen,
|
|
28
35
|
"model_picker": ModelPickerScreen,
|
|
29
36
|
"directory_setup": DirectorySetupScreen,
|
|
@@ -36,12 +43,19 @@ class ShotgunApp(App[None]):
|
|
|
36
43
|
CSS_PATH = "styles.tcss"
|
|
37
44
|
|
|
38
45
|
def __init__(
|
|
39
|
-
self,
|
|
46
|
+
self,
|
|
47
|
+
no_update_check: bool = False,
|
|
48
|
+
continue_session: bool = False,
|
|
49
|
+
force_reindex: bool = False,
|
|
40
50
|
) -> None:
|
|
41
51
|
super().__init__()
|
|
42
52
|
self.config_manager: ConfigManager = get_config_manager()
|
|
43
53
|
self.no_update_check = no_update_check
|
|
44
54
|
self.continue_session = continue_session
|
|
55
|
+
self.force_reindex = force_reindex
|
|
56
|
+
|
|
57
|
+
# Initialize dependency injection container
|
|
58
|
+
self.container = TUIContainer()
|
|
45
59
|
|
|
46
60
|
# Start async update check and install
|
|
47
61
|
if not no_update_check:
|
|
@@ -52,14 +66,35 @@ class ShotgunApp(App[None]):
|
|
|
52
66
|
# Track TUI startup
|
|
53
67
|
from shotgun.posthog_telemetry import track_event
|
|
54
68
|
|
|
55
|
-
track_event(
|
|
69
|
+
track_event(
|
|
70
|
+
"tui_started",
|
|
71
|
+
{
|
|
72
|
+
"installation_method": detect_installation_method(),
|
|
73
|
+
},
|
|
74
|
+
)
|
|
56
75
|
|
|
57
76
|
self.push_screen(
|
|
58
77
|
SplashScreen(), callback=lambda _arg: self.refresh_startup_screen()
|
|
59
78
|
)
|
|
60
79
|
|
|
61
|
-
def refresh_startup_screen(self) -> None:
|
|
80
|
+
def refresh_startup_screen(self, skip_pipx_check: bool = False) -> None:
|
|
62
81
|
"""Push the appropriate screen based on configured providers."""
|
|
82
|
+
# Check for pipx installation and show migration modal first
|
|
83
|
+
if not skip_pipx_check:
|
|
84
|
+
installation_method = detect_installation_method()
|
|
85
|
+
if installation_method == "pipx":
|
|
86
|
+
if isinstance(self.screen, PipxMigrationScreen):
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
# Show pipx migration modal as a blocking modal screen
|
|
90
|
+
self.push_screen(
|
|
91
|
+
PipxMigrationScreen(),
|
|
92
|
+
callback=lambda _arg: self.refresh_startup_screen(
|
|
93
|
+
skip_pipx_check=True
|
|
94
|
+
),
|
|
95
|
+
)
|
|
96
|
+
return
|
|
97
|
+
|
|
63
98
|
# Show welcome screen if no providers are configured OR if user hasn't seen it yet
|
|
64
99
|
config = self.config_manager.load()
|
|
65
100
|
if (
|
|
@@ -87,8 +122,38 @@ class ShotgunApp(App[None]):
|
|
|
87
122
|
|
|
88
123
|
if isinstance(self.screen, ChatScreen):
|
|
89
124
|
return
|
|
90
|
-
|
|
91
|
-
|
|
125
|
+
|
|
126
|
+
# Create ChatScreen with all dependencies injected from container
|
|
127
|
+
# Get the default agent mode (RESEARCH)
|
|
128
|
+
agent_mode = AgentType.RESEARCH
|
|
129
|
+
|
|
130
|
+
# Create AgentManager with the correct mode
|
|
131
|
+
agent_manager = self.container.agent_manager_factory(initial_type=agent_mode)
|
|
132
|
+
|
|
133
|
+
# Create ProcessingStateManager - we'll pass the screen after creation
|
|
134
|
+
# For now, create with None and the ChatScreen will set itself
|
|
135
|
+
chat_screen = ChatScreen(
|
|
136
|
+
agent_manager=agent_manager,
|
|
137
|
+
conversation_manager=self.container.conversation_manager(),
|
|
138
|
+
conversation_service=self.container.conversation_service(),
|
|
139
|
+
widget_coordinator=self.container.widget_coordinator_factory(screen=None),
|
|
140
|
+
processing_state=self.container.processing_state_factory(
|
|
141
|
+
screen=None, # Will be set after ChatScreen is created
|
|
142
|
+
telemetry_context={"agent_mode": agent_mode.value},
|
|
143
|
+
),
|
|
144
|
+
command_handler=self.container.command_handler(),
|
|
145
|
+
placeholder_hints=self.container.placeholder_hints(),
|
|
146
|
+
codebase_sdk=self.container.codebase_sdk(),
|
|
147
|
+
deps=self.container.agent_deps(),
|
|
148
|
+
continue_session=self.continue_session,
|
|
149
|
+
force_reindex=self.force_reindex,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# Update the ProcessingStateManager and WidgetCoordinator with the actual ChatScreen instance
|
|
153
|
+
chat_screen.processing_state.screen = chat_screen
|
|
154
|
+
chat_screen.widget_coordinator.screen = chat_screen
|
|
155
|
+
|
|
156
|
+
self.push_screen(chat_screen)
|
|
92
157
|
|
|
93
158
|
def check_local_shotgun_directory_exists(self) -> bool:
|
|
94
159
|
shotgun_dir = get_shotgun_base_path()
|
|
@@ -121,12 +186,17 @@ class ShotgunApp(App[None]):
|
|
|
121
186
|
self.push_screen(FeedbackScreen(), callback=handle_feedback)
|
|
122
187
|
|
|
123
188
|
|
|
124
|
-
def run(
|
|
189
|
+
def run(
|
|
190
|
+
no_update_check: bool = False,
|
|
191
|
+
continue_session: bool = False,
|
|
192
|
+
force_reindex: bool = False,
|
|
193
|
+
) -> None:
|
|
125
194
|
"""Run the TUI application.
|
|
126
195
|
|
|
127
196
|
Args:
|
|
128
197
|
no_update_check: If True, disable automatic update checks.
|
|
129
198
|
continue_session: If True, continue from previous conversation.
|
|
199
|
+
force_reindex: If True, force re-indexing of codebase (ignores existing index).
|
|
130
200
|
"""
|
|
131
201
|
# Clean up any corrupted databases BEFORE starting the TUI
|
|
132
202
|
# This prevents crashes from corrupted databases during initialization
|
|
@@ -148,9 +218,133 @@ def run(no_update_check: bool = False, continue_session: bool = False) -> None:
|
|
|
148
218
|
logger.error(f"Failed to cleanup corrupted databases: {e}")
|
|
149
219
|
# Continue anyway - the TUI can still function
|
|
150
220
|
|
|
151
|
-
app = ShotgunApp(
|
|
221
|
+
app = ShotgunApp(
|
|
222
|
+
no_update_check=no_update_check,
|
|
223
|
+
continue_session=continue_session,
|
|
224
|
+
force_reindex=force_reindex,
|
|
225
|
+
)
|
|
152
226
|
app.run(inline_no_clear=True)
|
|
153
227
|
|
|
154
228
|
|
|
229
|
+
def serve(
|
|
230
|
+
host: str = "localhost",
|
|
231
|
+
port: int = 8000,
|
|
232
|
+
public_url: str | None = None,
|
|
233
|
+
no_update_check: bool = False,
|
|
234
|
+
continue_session: bool = False,
|
|
235
|
+
force_reindex: bool = False,
|
|
236
|
+
) -> None:
|
|
237
|
+
"""Serve the TUI application as a web application.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
host: Host address for the web server.
|
|
241
|
+
port: Port number for the web server.
|
|
242
|
+
public_url: Public URL if behind a proxy.
|
|
243
|
+
no_update_check: If True, disable automatic update checks.
|
|
244
|
+
continue_session: If True, continue from previous conversation.
|
|
245
|
+
force_reindex: If True, force re-indexing of codebase (ignores existing index).
|
|
246
|
+
"""
|
|
247
|
+
# Clean up any corrupted databases BEFORE starting the TUI
|
|
248
|
+
# This prevents crashes from corrupted databases during initialization
|
|
249
|
+
import asyncio
|
|
250
|
+
|
|
251
|
+
from textual_serve.server import Server
|
|
252
|
+
|
|
253
|
+
from shotgun.codebase.core.manager import CodebaseGraphManager
|
|
254
|
+
from shotgun.utils import get_shotgun_home
|
|
255
|
+
|
|
256
|
+
storage_dir = get_shotgun_home() / "codebases"
|
|
257
|
+
manager = CodebaseGraphManager(storage_dir)
|
|
258
|
+
|
|
259
|
+
try:
|
|
260
|
+
removed = asyncio.run(manager.cleanup_corrupted_databases())
|
|
261
|
+
if removed:
|
|
262
|
+
logger.info(
|
|
263
|
+
f"Cleaned up {len(removed)} corrupted database(s) before TUI startup"
|
|
264
|
+
)
|
|
265
|
+
except Exception as e:
|
|
266
|
+
logger.error(f"Failed to cleanup corrupted databases: {e}")
|
|
267
|
+
# Continue anyway - the TUI can still function
|
|
268
|
+
|
|
269
|
+
# Create a new event loop after asyncio.run() closes the previous one
|
|
270
|
+
# This is needed for the Server.serve() method
|
|
271
|
+
loop = asyncio.new_event_loop()
|
|
272
|
+
asyncio.set_event_loop(loop)
|
|
273
|
+
|
|
274
|
+
# Build the command string based on flags
|
|
275
|
+
command = "shotgun"
|
|
276
|
+
if no_update_check:
|
|
277
|
+
command += " --no-update-check"
|
|
278
|
+
if continue_session:
|
|
279
|
+
command += " --continue"
|
|
280
|
+
if force_reindex:
|
|
281
|
+
command += " --force-reindex"
|
|
282
|
+
|
|
283
|
+
# Create and start the server with hardcoded title and debug=False
|
|
284
|
+
server = Server(
|
|
285
|
+
command=command,
|
|
286
|
+
host=host,
|
|
287
|
+
port=port,
|
|
288
|
+
title="The Shotgun",
|
|
289
|
+
public_url=public_url,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
# Set up graceful shutdown on SIGTERM/SIGINT
|
|
293
|
+
import signal
|
|
294
|
+
import sys
|
|
295
|
+
|
|
296
|
+
def signal_handler(_signum: int, _frame: Any) -> None:
|
|
297
|
+
"""Handle shutdown signals gracefully."""
|
|
298
|
+
from shotgun.posthog_telemetry import shutdown
|
|
299
|
+
|
|
300
|
+
logger.info("Received shutdown signal, cleaning up...")
|
|
301
|
+
# Restore stdout/stderr before shutting down
|
|
302
|
+
sys.stdout = original_stdout
|
|
303
|
+
sys.stderr = original_stderr
|
|
304
|
+
shutdown()
|
|
305
|
+
sys.exit(0)
|
|
306
|
+
|
|
307
|
+
signal.signal(signal.SIGTERM, signal_handler)
|
|
308
|
+
signal.signal(signal.SIGINT, signal_handler)
|
|
309
|
+
|
|
310
|
+
# Suppress the textual-serve banner by redirecting stdout/stderr
|
|
311
|
+
import io
|
|
312
|
+
|
|
313
|
+
# Capture and suppress the banner, but show the actual serving URL
|
|
314
|
+
original_stdout = sys.stdout
|
|
315
|
+
original_stderr = sys.stderr
|
|
316
|
+
|
|
317
|
+
captured_output = io.StringIO()
|
|
318
|
+
sys.stdout = captured_output
|
|
319
|
+
sys.stderr = captured_output
|
|
320
|
+
|
|
321
|
+
try:
|
|
322
|
+
# This will print the banner to our captured output
|
|
323
|
+
import logging
|
|
324
|
+
|
|
325
|
+
# Temporarily set logging to ERROR level to suppress INFO messages
|
|
326
|
+
textual_serve_logger = logging.getLogger("textual_serve")
|
|
327
|
+
original_level = textual_serve_logger.level
|
|
328
|
+
textual_serve_logger.setLevel(logging.ERROR)
|
|
329
|
+
|
|
330
|
+
# Print our own message to the original stdout
|
|
331
|
+
sys.stdout = original_stdout
|
|
332
|
+
sys.stderr = original_stderr
|
|
333
|
+
print(f"Serving Shotgun TUI at http://{host}:{port}")
|
|
334
|
+
print("Press Ctrl+C to quit")
|
|
335
|
+
|
|
336
|
+
# Now suppress output again for the serve call
|
|
337
|
+
sys.stdout = captured_output
|
|
338
|
+
sys.stderr = captured_output
|
|
339
|
+
|
|
340
|
+
server.serve(debug=False)
|
|
341
|
+
finally:
|
|
342
|
+
# Restore original stdout/stderr
|
|
343
|
+
sys.stdout = original_stdout
|
|
344
|
+
sys.stderr = original_stderr
|
|
345
|
+
if "textual_serve_logger" in locals():
|
|
346
|
+
textual_serve_logger.setLevel(original_level)
|
|
347
|
+
|
|
348
|
+
|
|
155
349
|
if __name__ == "__main__":
|
|
156
350
|
run()
|
shotgun/tui/commands/__init__.py
CHANGED
|
@@ -57,7 +57,7 @@ class CommandHandler:
|
|
|
57
57
|
**Keyboard Shortcuts:**
|
|
58
58
|
|
|
59
59
|
* `Enter` - Send message
|
|
60
|
-
* `Ctrl+P` - Open command palette
|
|
60
|
+
* `Ctrl+P` - Open command palette (for usage, context, and other commands)
|
|
61
61
|
* `Shift+Tab` - Cycle agent modes
|
|
62
62
|
* `Ctrl+C` - Quit application
|
|
63
63
|
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Context window indicator component for showing model usage."""
|
|
2
|
+
|
|
3
|
+
from textual.reactive import reactive
|
|
4
|
+
from textual.widgets import Static
|
|
5
|
+
|
|
6
|
+
from shotgun.agents.config.models import MODEL_SPECS, ModelName
|
|
7
|
+
from shotgun.agents.context_analyzer.models import ContextAnalysis
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ContextIndicator(Static):
|
|
11
|
+
"""Display context window usage and current model name."""
|
|
12
|
+
|
|
13
|
+
DEFAULT_CSS = """
|
|
14
|
+
ContextIndicator {
|
|
15
|
+
width: auto;
|
|
16
|
+
height: 1;
|
|
17
|
+
text-align: right;
|
|
18
|
+
}
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
context_analysis: reactive[ContextAnalysis | None] = reactive(None)
|
|
22
|
+
model_name: reactive[ModelName | None] = reactive(None)
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
*,
|
|
27
|
+
name: str | None = None,
|
|
28
|
+
id: str | None = None,
|
|
29
|
+
classes: str | None = None,
|
|
30
|
+
) -> None:
|
|
31
|
+
super().__init__(name=name, id=id, classes=classes)
|
|
32
|
+
|
|
33
|
+
def update_context(
|
|
34
|
+
self, analysis: ContextAnalysis | None, model: ModelName | None
|
|
35
|
+
) -> None:
|
|
36
|
+
"""Update the context indicator with new analysis and model data.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
analysis: Context analysis with token usage data
|
|
40
|
+
model: Current model name
|
|
41
|
+
"""
|
|
42
|
+
self.context_analysis = analysis
|
|
43
|
+
self.model_name = model
|
|
44
|
+
self._refresh_display()
|
|
45
|
+
|
|
46
|
+
def _get_percentage_color(self, percentage: float) -> str:
|
|
47
|
+
"""Get color for percentage based on threshold.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
percentage: Usage percentage (0-100)
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Color name for Textual markup
|
|
54
|
+
"""
|
|
55
|
+
if percentage < 60:
|
|
56
|
+
return "#00ff00" # Green
|
|
57
|
+
elif percentage < 85:
|
|
58
|
+
return "#ffff00" # Yellow
|
|
59
|
+
else:
|
|
60
|
+
return "#ff0000" # Red
|
|
61
|
+
|
|
62
|
+
def _format_token_count(self, tokens: int) -> str:
|
|
63
|
+
"""Format token count for display (e.g., 115000 -> "115K").
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
tokens: Token count
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Formatted string
|
|
70
|
+
"""
|
|
71
|
+
if tokens >= 1_000_000:
|
|
72
|
+
return f"{tokens / 1_000_000:.1f}M"
|
|
73
|
+
elif tokens >= 1_000:
|
|
74
|
+
return f"{tokens / 1_000:.0f}K"
|
|
75
|
+
else:
|
|
76
|
+
return str(tokens)
|
|
77
|
+
|
|
78
|
+
def _refresh_display(self) -> None:
|
|
79
|
+
"""Refresh the display with current context data."""
|
|
80
|
+
# If no analysis yet, show placeholder with model name or empty
|
|
81
|
+
if self.context_analysis is None:
|
|
82
|
+
if self.model_name:
|
|
83
|
+
model_spec = MODEL_SPECS.get(self.model_name)
|
|
84
|
+
model_display = (
|
|
85
|
+
model_spec.short_name if model_spec else str(self.model_name)
|
|
86
|
+
)
|
|
87
|
+
self.update(f"[bold]{model_display}[/bold]")
|
|
88
|
+
else:
|
|
89
|
+
self.update("")
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
analysis = self.context_analysis
|
|
93
|
+
|
|
94
|
+
# Calculate percentage
|
|
95
|
+
if analysis.max_usable_tokens > 0:
|
|
96
|
+
percentage = round(
|
|
97
|
+
(analysis.agent_context_tokens / analysis.max_usable_tokens) * 100, 1
|
|
98
|
+
)
|
|
99
|
+
else:
|
|
100
|
+
percentage = 0.0
|
|
101
|
+
|
|
102
|
+
# Format token counts
|
|
103
|
+
current_tokens = self._format_token_count(analysis.agent_context_tokens)
|
|
104
|
+
max_tokens = self._format_token_count(analysis.max_usable_tokens)
|
|
105
|
+
|
|
106
|
+
# Get color based on percentage
|
|
107
|
+
color = self._get_percentage_color(percentage)
|
|
108
|
+
|
|
109
|
+
# Build the display string - always show full context info
|
|
110
|
+
parts = [
|
|
111
|
+
"[$foreground-muted]Context window:[/]",
|
|
112
|
+
f"[{color}]{percentage}% ({current_tokens}/{max_tokens})[/]",
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
# Add model name if available
|
|
116
|
+
if self.model_name:
|
|
117
|
+
model_spec = MODEL_SPECS.get(self.model_name)
|
|
118
|
+
model_display = (
|
|
119
|
+
model_spec.short_name if model_spec else str(self.model_name)
|
|
120
|
+
)
|
|
121
|
+
parts.extend(
|
|
122
|
+
[
|
|
123
|
+
"[$foreground-muted]|[/]",
|
|
124
|
+
f"[bold]{model_display}[/bold]",
|
|
125
|
+
]
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
self.update(" ".join(parts))
|
|
129
|
+
|
|
130
|
+
def watch_context_analysis(self, analysis: ContextAnalysis | None) -> None:
|
|
131
|
+
"""React to context analysis changes."""
|
|
132
|
+
self._refresh_display()
|
|
133
|
+
|
|
134
|
+
def watch_model_name(self, model: ModelName | None) -> None:
|
|
135
|
+
"""React to model name changes."""
|
|
136
|
+
self._refresh_display()
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Widget to display the current agent mode."""
|
|
2
|
+
|
|
3
|
+
from textual.widget import Widget
|
|
4
|
+
|
|
5
|
+
from shotgun.agents.models import AgentType
|
|
6
|
+
from shotgun.tui.protocols import QAStateProvider
|
|
7
|
+
from shotgun.tui.utils.mode_progress import PlaceholderHints
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ModeIndicator(Widget):
|
|
11
|
+
"""Widget to display the current agent mode."""
|
|
12
|
+
|
|
13
|
+
DEFAULT_CSS = """
|
|
14
|
+
ModeIndicator {
|
|
15
|
+
text-wrap: wrap;
|
|
16
|
+
padding-left: 1;
|
|
17
|
+
}
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, mode: AgentType) -> None:
|
|
21
|
+
"""Initialize the mode indicator.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
mode: The current agent type/mode.
|
|
25
|
+
"""
|
|
26
|
+
super().__init__()
|
|
27
|
+
self.mode = mode
|
|
28
|
+
self.progress_checker = PlaceholderHints().progress_checker
|
|
29
|
+
|
|
30
|
+
def render(self) -> str:
|
|
31
|
+
"""Render the mode indicator."""
|
|
32
|
+
# Check if in Q&A mode first
|
|
33
|
+
if isinstance(self.screen, QAStateProvider) and self.screen.qa_mode:
|
|
34
|
+
return (
|
|
35
|
+
"[bold $text-accent]Q&A mode[/]"
|
|
36
|
+
"[$foreground-muted] (Answer the clarifying questions or ESC to cancel)[/]"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
mode_display = {
|
|
40
|
+
AgentType.RESEARCH: "Research",
|
|
41
|
+
AgentType.PLAN: "Planning",
|
|
42
|
+
AgentType.TASKS: "Tasks",
|
|
43
|
+
AgentType.SPECIFY: "Specify",
|
|
44
|
+
AgentType.EXPORT: "Export",
|
|
45
|
+
}
|
|
46
|
+
mode_description = {
|
|
47
|
+
AgentType.RESEARCH: (
|
|
48
|
+
"Research topics with web search and synthesize findings"
|
|
49
|
+
),
|
|
50
|
+
AgentType.PLAN: "Create comprehensive, actionable plans with milestones",
|
|
51
|
+
AgentType.TASKS: (
|
|
52
|
+
"Generate specific, actionable tasks from research and plans"
|
|
53
|
+
),
|
|
54
|
+
AgentType.SPECIFY: (
|
|
55
|
+
"Create detailed specifications and requirements documents"
|
|
56
|
+
),
|
|
57
|
+
AgentType.EXPORT: "Export artifacts and findings to various formats",
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
mode_title = mode_display.get(self.mode, self.mode.value.title())
|
|
61
|
+
description = mode_description.get(self.mode, "")
|
|
62
|
+
|
|
63
|
+
# Check if mode has content
|
|
64
|
+
has_content = self.progress_checker.has_mode_content(self.mode)
|
|
65
|
+
status_icon = " ✓" if has_content else ""
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
f"[bold $text-accent]{mode_title}{status_icon} mode[/]"
|
|
69
|
+
f"[$foreground-muted] ({description})[/]"
|
|
70
|
+
)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Widget to display the status bar with contextual help text."""
|
|
2
|
+
|
|
3
|
+
from textual.widget import Widget
|
|
4
|
+
|
|
5
|
+
from shotgun.tui.protocols import QAStateProvider
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class StatusBar(Widget):
|
|
9
|
+
"""Widget to display the status bar with contextual help text."""
|
|
10
|
+
|
|
11
|
+
DEFAULT_CSS = """
|
|
12
|
+
StatusBar {
|
|
13
|
+
text-wrap: wrap;
|
|
14
|
+
padding-left: 1;
|
|
15
|
+
}
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, working: bool = False) -> None:
|
|
19
|
+
"""Initialize the status bar.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
working: Whether an agent is currently working.
|
|
23
|
+
"""
|
|
24
|
+
super().__init__()
|
|
25
|
+
self.working = working
|
|
26
|
+
|
|
27
|
+
def render(self) -> str:
|
|
28
|
+
"""Render the status bar with contextual help text."""
|
|
29
|
+
# Check if in Q&A mode first (highest priority)
|
|
30
|
+
if isinstance(self.screen, QAStateProvider) and self.screen.qa_mode:
|
|
31
|
+
return (
|
|
32
|
+
"[$foreground-muted][bold $text]esc[/] to exit Q&A mode • "
|
|
33
|
+
"[bold $text]enter[/] to send answer • [bold $text]ctrl+j[/] for newline[/]"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
if self.working:
|
|
37
|
+
return (
|
|
38
|
+
"[$foreground-muted][bold $text]esc[/] to stop • "
|
|
39
|
+
"[bold $text]enter[/] to send • [bold $text]ctrl+j[/] for newline • "
|
|
40
|
+
"[bold $text]ctrl+p[/] command palette • [bold $text]shift+tab[/] cycle modes • "
|
|
41
|
+
"/help for commands[/]"
|
|
42
|
+
)
|
|
43
|
+
else:
|
|
44
|
+
return (
|
|
45
|
+
"[$foreground-muted][bold $text]enter[/] to send • "
|
|
46
|
+
"[bold $text]ctrl+j[/] for newline • [bold $text]ctrl+p[/] command palette • "
|
|
47
|
+
"[bold $text]shift+tab[/] cycle modes • /help for commands[/]"
|
|
48
|
+
)
|