shotgun-sh 0.2.8.dev2__py3-none-any.whl → 0.3.3.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.
- shotgun/agents/agent_manager.py +382 -60
- shotgun/agents/common.py +15 -9
- shotgun/agents/config/README.md +89 -0
- shotgun/agents/config/__init__.py +10 -1
- shotgun/agents/config/constants.py +0 -6
- shotgun/agents/config/manager.py +383 -82
- shotgun/agents/config/models.py +122 -18
- shotgun/agents/config/provider.py +81 -15
- shotgun/agents/config/streaming_test.py +119 -0
- shotgun/agents/context_analyzer/__init__.py +28 -0
- shotgun/agents/context_analyzer/analyzer.py +475 -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/__init__.py +18 -0
- shotgun/agents/conversation/filters.py +164 -0
- shotgun/agents/conversation/history/chunking.py +278 -0
- shotgun/agents/{history → conversation/history}/compaction.py +36 -5
- shotgun/agents/{history → conversation/history}/constants.py +5 -0
- shotgun/agents/conversation/history/file_content_deduplication.py +216 -0
- shotgun/agents/{history → conversation/history}/history_processors.py +380 -8
- shotgun/agents/{history → conversation/history}/token_counting/anthropic.py +25 -1
- shotgun/agents/{history → conversation/history}/token_counting/base.py +14 -3
- shotgun/agents/{history → conversation/history}/token_counting/openai.py +11 -1
- shotgun/agents/{history → conversation/history}/token_counting/sentencepiece_counter.py +8 -0
- shotgun/agents/{history → conversation/history}/token_counting/tokenizer_cache.py +3 -1
- shotgun/agents/{history → conversation/history}/token_counting/utils.py +0 -3
- shotgun/agents/{conversation_manager.py → conversation/manager.py} +36 -20
- shotgun/agents/{conversation_history.py → conversation/models.py} +8 -92
- shotgun/agents/error/__init__.py +11 -0
- shotgun/agents/error/models.py +19 -0
- shotgun/agents/export.py +2 -2
- shotgun/agents/plan.py +2 -2
- shotgun/agents/research.py +3 -3
- shotgun/agents/runner.py +230 -0
- shotgun/agents/specify.py +2 -2
- shotgun/agents/tasks.py +2 -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 +11 -2
- shotgun/agents/tools/codebase/query_graph.py +6 -0
- shotgun/agents/tools/codebase/retrieve_code.py +6 -0
- shotgun/agents/tools/file_management.py +27 -7
- shotgun/agents/tools/registry.py +217 -0
- shotgun/agents/tools/web_search/__init__.py +8 -8
- shotgun/agents/tools/web_search/anthropic.py +8 -2
- shotgun/agents/tools/web_search/gemini.py +7 -1
- shotgun/agents/tools/web_search/openai.py +8 -2
- shotgun/agents/tools/web_search/utils.py +2 -2
- shotgun/agents/usage_manager.py +16 -11
- shotgun/api_endpoints.py +7 -3
- shotgun/build_constants.py +2 -2
- shotgun/cli/clear.py +53 -0
- shotgun/cli/compact.py +188 -0
- shotgun/cli/config.py +8 -5
- shotgun/cli/context.py +154 -0
- shotgun/cli/error_handler.py +24 -0
- shotgun/cli/export.py +34 -34
- shotgun/cli/feedback.py +4 -2
- shotgun/cli/models.py +1 -0
- shotgun/cli/plan.py +34 -34
- shotgun/cli/research.py +18 -10
- shotgun/cli/spec/__init__.py +5 -0
- shotgun/cli/spec/backup.py +81 -0
- shotgun/cli/spec/commands.py +132 -0
- shotgun/cli/spec/models.py +48 -0
- shotgun/cli/spec/pull_service.py +219 -0
- shotgun/cli/specify.py +20 -19
- shotgun/cli/tasks.py +34 -34
- shotgun/cli/update.py +16 -2
- shotgun/codebase/core/change_detector.py +5 -3
- shotgun/codebase/core/code_retrieval.py +4 -2
- shotgun/codebase/core/ingestor.py +163 -15
- shotgun/codebase/core/manager.py +13 -4
- shotgun/codebase/core/nl_query.py +1 -1
- shotgun/codebase/models.py +2 -0
- shotgun/exceptions.py +357 -0
- shotgun/llm_proxy/__init__.py +17 -0
- shotgun/llm_proxy/client.py +215 -0
- shotgun/llm_proxy/models.py +137 -0
- shotgun/logging_config.py +60 -27
- shotgun/main.py +77 -11
- shotgun/posthog_telemetry.py +38 -29
- shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +28 -2
- shotgun/prompts/agents/partials/interactive_mode.j2 +3 -3
- shotgun/prompts/agents/plan.j2 +16 -0
- shotgun/prompts/agents/research.j2 +16 -3
- shotgun/prompts/agents/specify.j2 +54 -1
- shotgun/prompts/agents/state/system_state.j2 +0 -2
- shotgun/prompts/agents/tasks.j2 +16 -0
- shotgun/prompts/history/chunk_summarization.j2 +34 -0
- shotgun/prompts/history/combine_summaries.j2 +53 -0
- shotgun/sdk/codebase.py +14 -3
- shotgun/sentry_telemetry.py +163 -16
- shotgun/settings.py +243 -0
- shotgun/shotgun_web/__init__.py +67 -1
- shotgun/shotgun_web/client.py +42 -1
- shotgun/shotgun_web/constants.py +46 -0
- shotgun/shotgun_web/exceptions.py +29 -0
- shotgun/shotgun_web/models.py +390 -0
- shotgun/shotgun_web/shared_specs/__init__.py +32 -0
- shotgun/shotgun_web/shared_specs/file_scanner.py +175 -0
- shotgun/shotgun_web/shared_specs/hasher.py +83 -0
- shotgun/shotgun_web/shared_specs/models.py +71 -0
- shotgun/shotgun_web/shared_specs/upload_pipeline.py +329 -0
- shotgun/shotgun_web/shared_specs/utils.py +34 -0
- shotgun/shotgun_web/specs_client.py +703 -0
- shotgun/shotgun_web/supabase_client.py +31 -0
- shotgun/telemetry.py +10 -33
- shotgun/tui/app.py +310 -46
- shotgun/tui/commands/__init__.py +1 -1
- shotgun/tui/components/context_indicator.py +179 -0
- shotgun/tui/components/mode_indicator.py +70 -0
- shotgun/tui/components/status_bar.py +48 -0
- shotgun/tui/containers.py +91 -0
- shotgun/tui/dependencies.py +39 -0
- shotgun/tui/layout.py +5 -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 +1531 -0
- shotgun/tui/screens/chat/codebase_index_prompt_screen.py +243 -0
- shotgun/tui/screens/chat/codebase_index_selection.py +12 -0
- shotgun/tui/screens/chat/help_text.py +40 -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 +91 -4
- shotgun/tui/screens/chat_screen/hint_message.py +76 -1
- 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 +115 -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 +191 -0
- shotgun/tui/screens/directory_setup.py +45 -41
- shotgun/tui/screens/feedback.py +14 -7
- shotgun/tui/screens/github_issue.py +111 -0
- shotgun/tui/screens/model_picker.py +77 -32
- shotgun/tui/screens/onboarding.py +580 -0
- shotgun/tui/screens/pipx_migration.py +205 -0
- shotgun/tui/screens/provider_config.py +116 -35
- shotgun/tui/screens/shared_specs/__init__.py +21 -0
- shotgun/tui/screens/shared_specs/create_spec_dialog.py +273 -0
- shotgun/tui/screens/shared_specs/models.py +56 -0
- shotgun/tui/screens/shared_specs/share_specs_dialog.py +390 -0
- shotgun/tui/screens/shared_specs/upload_progress_screen.py +452 -0
- shotgun/tui/screens/shotgun_auth.py +112 -18
- shotgun/tui/screens/spec_pull.py +288 -0
- shotgun/tui/screens/welcome.py +137 -11
- shotgun/tui/services/__init__.py +5 -0
- shotgun/tui/services/conversation_service.py +187 -0
- shotgun/tui/state/__init__.py +7 -0
- shotgun/tui/state/processing_state.py +185 -0
- shotgun/tui/utils/mode_progress.py +14 -7
- shotgun/tui/widgets/__init__.py +5 -0
- shotgun/tui/widgets/widget_coordinator.py +263 -0
- shotgun/utils/file_system_utils.py +22 -2
- shotgun/utils/marketing.py +110 -0
- shotgun/utils/update_checker.py +69 -14
- shotgun_sh-0.3.3.dev1.dist-info/METADATA +472 -0
- shotgun_sh-0.3.3.dev1.dist-info/RECORD +229 -0
- {shotgun_sh-0.2.8.dev2.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/WHEEL +1 -1
- {shotgun_sh-0.2.8.dev2.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/entry_points.txt +1 -0
- {shotgun_sh-0.2.8.dev2.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/licenses/LICENSE +1 -1
- shotgun/tui/screens/chat.py +0 -996
- shotgun/tui/screens/chat_screen/history.py +0 -335
- shotgun_sh-0.2.8.dev2.dist-info/METADATA +0 -126
- shotgun_sh-0.2.8.dev2.dist-info/RECORD +0 -155
- /shotgun/agents/{history → conversation/history}/__init__.py +0 -0
- /shotgun/agents/{history → conversation/history}/context_extraction.py +0 -0
- /shotgun/agents/{history → conversation/history}/history_building.py +0 -0
- /shotgun/agents/{history → conversation/history}/message_utils.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_counting/__init__.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_estimation.py +0 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Supabase Storage download utilities."""
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
|
|
5
|
+
from shotgun.logging_config import get_logger
|
|
6
|
+
|
|
7
|
+
logger = get_logger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
async def download_file_from_url(download_url: str) -> bytes:
|
|
11
|
+
"""Download a file from a presigned Supabase Storage URL.
|
|
12
|
+
|
|
13
|
+
The API returns presigned URLs with embedded tokens that don't require
|
|
14
|
+
any authentication headers.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
download_url: Presigned Supabase Storage URL
|
|
18
|
+
(e.g., "https://...supabase.co/storage/v1/object/sign/...?token=...")
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
File contents as bytes
|
|
22
|
+
|
|
23
|
+
Raises:
|
|
24
|
+
httpx.HTTPStatusError: If download fails
|
|
25
|
+
"""
|
|
26
|
+
logger.debug("Downloading file from: %s", download_url)
|
|
27
|
+
|
|
28
|
+
async with httpx.AsyncClient(timeout=60.0) as client:
|
|
29
|
+
response = await client.get(download_url)
|
|
30
|
+
response.raise_for_status()
|
|
31
|
+
return response.content
|
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
|
|
@@ -75,12 +50,14 @@ def setup_logfire_observability() -> bool:
|
|
|
75
50
|
|
|
76
51
|
# Set user context using baggage for all logs and spans
|
|
77
52
|
try:
|
|
53
|
+
import asyncio
|
|
54
|
+
|
|
78
55
|
from opentelemetry import baggage, context
|
|
79
56
|
|
|
80
57
|
from shotgun.agents.config import get_config_manager
|
|
81
58
|
|
|
82
59
|
config_manager = get_config_manager()
|
|
83
|
-
shotgun_instance_id = config_manager.get_shotgun_instance_id()
|
|
60
|
+
shotgun_instance_id = asyncio.run(config_manager.get_shotgun_instance_id())
|
|
84
61
|
|
|
85
62
|
# Set shotgun_instance_id as baggage in global context - this will be included in all logs/spans
|
|
86
63
|
ctx = baggage.set_baggage("shotgun_instance_id", shotgun_instance_id)
|
shotgun/tui/app.py
CHANGED
|
@@ -5,16 +5,29 @@ from textual.app import App, SystemCommand
|
|
|
5
5
|
from textual.binding import Binding
|
|
6
6
|
from textual.screen import Screen
|
|
7
7
|
|
|
8
|
-
from shotgun.agents.
|
|
8
|
+
from shotgun.agents.agent_manager import AgentManager
|
|
9
|
+
from shotgun.agents.config import (
|
|
10
|
+
ConfigManager,
|
|
11
|
+
get_config_manager,
|
|
12
|
+
)
|
|
13
|
+
from shotgun.agents.models import AgentType
|
|
9
14
|
from shotgun.logging_config import get_logger
|
|
15
|
+
from shotgun.tui.containers import TUIContainer
|
|
10
16
|
from shotgun.tui.screens.splash import SplashScreen
|
|
11
|
-
from shotgun.utils.file_system_utils import
|
|
12
|
-
|
|
17
|
+
from shotgun.utils.file_system_utils import (
|
|
18
|
+
ensure_shotgun_directory_exists,
|
|
19
|
+
get_shotgun_base_path,
|
|
20
|
+
)
|
|
21
|
+
from shotgun.utils.update_checker import (
|
|
22
|
+
detect_installation_method,
|
|
23
|
+
perform_auto_update_async,
|
|
24
|
+
)
|
|
13
25
|
|
|
14
26
|
from .screens.chat import ChatScreen
|
|
15
27
|
from .screens.directory_setup import DirectorySetupScreen
|
|
16
|
-
from .screens.
|
|
28
|
+
from .screens.github_issue import GitHubIssueScreen
|
|
17
29
|
from .screens.model_picker import ModelPickerScreen
|
|
30
|
+
from .screens.pipx_migration import PipxMigrationScreen
|
|
18
31
|
from .screens.provider_config import ProviderConfigScreen
|
|
19
32
|
from .screens.welcome import WelcomeScreen
|
|
20
33
|
|
|
@@ -22,12 +35,13 @@ logger = get_logger(__name__)
|
|
|
22
35
|
|
|
23
36
|
|
|
24
37
|
class ShotgunApp(App[None]):
|
|
38
|
+
# ChatScreen removed from SCREENS dict since it requires dependency injection
|
|
39
|
+
# and is instantiated manually in refresh_startup_screen()
|
|
40
|
+
# DirectorySetupScreen also removed since it requires error_message parameter
|
|
25
41
|
SCREENS = {
|
|
26
|
-
"chat": ChatScreen,
|
|
27
42
|
"provider_config": ProviderConfigScreen,
|
|
28
43
|
"model_picker": ModelPickerScreen,
|
|
29
|
-
"
|
|
30
|
-
"feedback": FeedbackScreen,
|
|
44
|
+
"github_issue": GitHubIssueScreen,
|
|
31
45
|
}
|
|
32
46
|
BINDINGS = [
|
|
33
47
|
Binding("ctrl+c", "quit", "Quit the app"),
|
|
@@ -36,12 +50,23 @@ class ShotgunApp(App[None]):
|
|
|
36
50
|
CSS_PATH = "styles.tcss"
|
|
37
51
|
|
|
38
52
|
def __init__(
|
|
39
|
-
self,
|
|
53
|
+
self,
|
|
54
|
+
no_update_check: bool = False,
|
|
55
|
+
continue_session: bool = False,
|
|
56
|
+
force_reindex: bool = False,
|
|
57
|
+
show_pull_hint: bool = False,
|
|
58
|
+
pull_version_id: str | None = None,
|
|
40
59
|
) -> None:
|
|
41
60
|
super().__init__()
|
|
42
61
|
self.config_manager: ConfigManager = get_config_manager()
|
|
43
62
|
self.no_update_check = no_update_check
|
|
44
63
|
self.continue_session = continue_session
|
|
64
|
+
self.force_reindex = force_reindex
|
|
65
|
+
self.show_pull_hint = show_pull_hint
|
|
66
|
+
self.pull_version_id = pull_version_id
|
|
67
|
+
|
|
68
|
+
# Initialize dependency injection container
|
|
69
|
+
self.container = TUIContainer()
|
|
45
70
|
|
|
46
71
|
# Start async update check and install
|
|
47
72
|
if not no_update_check:
|
|
@@ -52,48 +77,157 @@ class ShotgunApp(App[None]):
|
|
|
52
77
|
# Track TUI startup
|
|
53
78
|
from shotgun.posthog_telemetry import track_event
|
|
54
79
|
|
|
55
|
-
track_event(
|
|
80
|
+
track_event(
|
|
81
|
+
"tui_started",
|
|
82
|
+
{
|
|
83
|
+
"installation_method": detect_installation_method(),
|
|
84
|
+
"terminal_width": self.size.width,
|
|
85
|
+
"terminal_height": self.size.height,
|
|
86
|
+
},
|
|
87
|
+
)
|
|
56
88
|
|
|
57
89
|
self.push_screen(
|
|
58
90
|
SplashScreen(), callback=lambda _arg: self.refresh_startup_screen()
|
|
59
91
|
)
|
|
60
92
|
|
|
61
|
-
def refresh_startup_screen(self) -> None:
|
|
93
|
+
def refresh_startup_screen(self, skip_pipx_check: bool = False) -> None:
|
|
62
94
|
"""Push the appropriate screen based on configured providers."""
|
|
63
|
-
#
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
95
|
+
# Check for pipx installation and show migration modal first
|
|
96
|
+
if not skip_pipx_check:
|
|
97
|
+
installation_method = detect_installation_method()
|
|
98
|
+
if installation_method == "pipx":
|
|
99
|
+
if isinstance(self.screen, PipxMigrationScreen):
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
# Show pipx migration modal as a blocking modal screen
|
|
103
|
+
self.push_screen(
|
|
104
|
+
PipxMigrationScreen(),
|
|
105
|
+
callback=lambda _arg: self.refresh_startup_screen(
|
|
106
|
+
skip_pipx_check=True
|
|
107
|
+
),
|
|
108
|
+
)
|
|
70
109
|
return
|
|
71
110
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
111
|
+
# Run async config loading in worker
|
|
112
|
+
async def _check_config() -> None:
|
|
113
|
+
# Show welcome screen if no providers are configured OR if user hasn't seen it yet
|
|
114
|
+
# Note: If config migration fails, ConfigManager will auto-create fresh config
|
|
115
|
+
# and set migration_failed flag, which WelcomeScreen will display
|
|
116
|
+
config = await self.config_manager.load()
|
|
117
|
+
|
|
118
|
+
has_any_key = await self.config_manager.has_any_provider_key()
|
|
119
|
+
if not has_any_key or not config.shown_welcome_screen:
|
|
120
|
+
if isinstance(self.screen, WelcomeScreen):
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
self.push_screen(
|
|
124
|
+
WelcomeScreen(),
|
|
125
|
+
callback=lambda _arg: self.refresh_startup_screen(),
|
|
126
|
+
)
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
# Try to create .shotgun directory if it doesn't exist
|
|
130
|
+
if not self.check_local_shotgun_directory_exists():
|
|
131
|
+
try:
|
|
132
|
+
path = ensure_shotgun_directory_exists()
|
|
133
|
+
# Verify directory was created successfully
|
|
134
|
+
if not path.is_dir():
|
|
135
|
+
# Show error screen if creation failed
|
|
136
|
+
if isinstance(self.screen, DirectorySetupScreen):
|
|
137
|
+
return
|
|
138
|
+
self.push_screen(
|
|
139
|
+
DirectorySetupScreen(
|
|
140
|
+
error_message="Unable to create .shotgun directory due to filesystem conflict."
|
|
141
|
+
),
|
|
142
|
+
callback=lambda _arg: self.refresh_startup_screen(),
|
|
143
|
+
)
|
|
144
|
+
return
|
|
145
|
+
except Exception as exc:
|
|
146
|
+
# Show error screen if creation failed with exception
|
|
147
|
+
if isinstance(self.screen, DirectorySetupScreen):
|
|
148
|
+
return
|
|
149
|
+
self.push_screen(
|
|
150
|
+
DirectorySetupScreen(error_message=str(exc)),
|
|
151
|
+
callback=lambda _arg: self.refresh_startup_screen(),
|
|
152
|
+
)
|
|
153
|
+
return
|
|
154
|
+
|
|
155
|
+
if isinstance(self.screen, ChatScreen):
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
# If we have a version to pull, show pull screen first
|
|
159
|
+
if self.pull_version_id:
|
|
160
|
+
from .screens.spec_pull import SpecPullScreen
|
|
77
161
|
|
|
78
|
-
|
|
79
|
-
|
|
162
|
+
self.push_screen(
|
|
163
|
+
SpecPullScreen(self.pull_version_id),
|
|
164
|
+
callback=self._handle_pull_complete,
|
|
165
|
+
)
|
|
80
166
|
return
|
|
81
167
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
168
|
+
# Create ChatScreen with all dependencies injected from container
|
|
169
|
+
# Get the default agent mode (RESEARCH)
|
|
170
|
+
agent_mode = AgentType.RESEARCH
|
|
171
|
+
|
|
172
|
+
# Create AgentDeps asynchronously (get_provider_model is now async)
|
|
173
|
+
from shotgun.tui.dependencies import create_default_tui_deps
|
|
174
|
+
|
|
175
|
+
agent_deps = await create_default_tui_deps()
|
|
176
|
+
|
|
177
|
+
# Create AgentManager with async initialization
|
|
178
|
+
agent_manager = AgentManager(deps=agent_deps, initial_type=agent_mode)
|
|
179
|
+
|
|
180
|
+
# Create ProcessingStateManager - we'll pass the screen after creation
|
|
181
|
+
# For now, create with None and the ChatScreen will set itself
|
|
182
|
+
chat_screen = ChatScreen(
|
|
183
|
+
agent_manager=agent_manager,
|
|
184
|
+
conversation_manager=self.container.conversation_manager(),
|
|
185
|
+
conversation_service=self.container.conversation_service(),
|
|
186
|
+
widget_coordinator=self.container.widget_coordinator_factory(
|
|
187
|
+
screen=None
|
|
188
|
+
),
|
|
189
|
+
processing_state=self.container.processing_state_factory(
|
|
190
|
+
screen=None, # Will be set after ChatScreen is created
|
|
191
|
+
telemetry_context={"agent_mode": agent_mode.value},
|
|
192
|
+
),
|
|
193
|
+
command_handler=self.container.command_handler(),
|
|
194
|
+
placeholder_hints=self.container.placeholder_hints(),
|
|
195
|
+
codebase_sdk=self.container.codebase_sdk(),
|
|
196
|
+
deps=agent_deps,
|
|
197
|
+
continue_session=self.continue_session,
|
|
198
|
+
force_reindex=self.force_reindex,
|
|
199
|
+
show_pull_hint=self.show_pull_hint,
|
|
85
200
|
)
|
|
86
|
-
return
|
|
87
201
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
202
|
+
# Update the ProcessingStateManager and WidgetCoordinator with the actual ChatScreen instance
|
|
203
|
+
chat_screen.processing_state.screen = chat_screen
|
|
204
|
+
chat_screen.widget_coordinator.screen = chat_screen
|
|
205
|
+
|
|
206
|
+
self.push_screen(chat_screen)
|
|
207
|
+
|
|
208
|
+
# Run the async config check in a worker
|
|
209
|
+
self.run_worker(_check_config(), exclusive=False)
|
|
92
210
|
|
|
93
211
|
def check_local_shotgun_directory_exists(self) -> bool:
|
|
94
212
|
shotgun_dir = get_shotgun_base_path()
|
|
95
213
|
return shotgun_dir.exists() and shotgun_dir.is_dir()
|
|
96
214
|
|
|
215
|
+
def _handle_pull_complete(self, success: bool | None) -> None:
|
|
216
|
+
"""Handle completion of spec pull screen.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
success: Whether the pull was successful, or None if dismissed.
|
|
220
|
+
"""
|
|
221
|
+
# Clear version_id so we don't pull again on next refresh
|
|
222
|
+
self.pull_version_id = None
|
|
223
|
+
|
|
224
|
+
if success:
|
|
225
|
+
# Enable hint for ChatScreen
|
|
226
|
+
self.show_pull_hint = True
|
|
227
|
+
|
|
228
|
+
# Continue to ChatScreen
|
|
229
|
+
self.refresh_startup_screen()
|
|
230
|
+
|
|
97
231
|
async def action_quit(self) -> None:
|
|
98
232
|
"""Quit the application."""
|
|
99
233
|
# Shut down PostHog client to prevent threading errors
|
|
@@ -105,28 +239,32 @@ class ShotgunApp(App[None]):
|
|
|
105
239
|
def get_system_commands(self, screen: Screen[Any]) -> Iterable[SystemCommand]:
|
|
106
240
|
return [
|
|
107
241
|
SystemCommand(
|
|
108
|
-
"
|
|
242
|
+
"New Issue",
|
|
243
|
+
"Report a bug or request a feature on GitHub",
|
|
244
|
+
self.action_new_issue,
|
|
109
245
|
)
|
|
110
|
-
]
|
|
246
|
+
]
|
|
111
247
|
|
|
112
|
-
def
|
|
113
|
-
"""Open
|
|
114
|
-
|
|
248
|
+
def action_new_issue(self) -> None:
|
|
249
|
+
"""Open GitHub issue screen to guide users to create an issue."""
|
|
250
|
+
self.push_screen(GitHubIssueScreen())
|
|
115
251
|
|
|
116
|
-
def handle_feedback(feedback: Feedback | None) -> None:
|
|
117
|
-
if feedback is not None:
|
|
118
|
-
submit_feedback_survey(feedback)
|
|
119
|
-
self.notify("Feedback sent. Thank you!")
|
|
120
252
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
253
|
+
def run(
|
|
254
|
+
no_update_check: bool = False,
|
|
255
|
+
continue_session: bool = False,
|
|
256
|
+
force_reindex: bool = False,
|
|
257
|
+
show_pull_hint: bool = False,
|
|
258
|
+
pull_version_id: str | None = None,
|
|
259
|
+
) -> None:
|
|
125
260
|
"""Run the TUI application.
|
|
126
261
|
|
|
127
262
|
Args:
|
|
128
263
|
no_update_check: If True, disable automatic update checks.
|
|
129
264
|
continue_session: If True, continue from previous conversation.
|
|
265
|
+
force_reindex: If True, force re-indexing of codebase (ignores existing index).
|
|
266
|
+
show_pull_hint: If True, show hint about recently pulled spec.
|
|
267
|
+
pull_version_id: If provided, pull this spec version before showing ChatScreen.
|
|
130
268
|
"""
|
|
131
269
|
# Clean up any corrupted databases BEFORE starting the TUI
|
|
132
270
|
# This prevents crashes from corrupted databases during initialization
|
|
@@ -148,9 +286,135 @@ def run(no_update_check: bool = False, continue_session: bool = False) -> None:
|
|
|
148
286
|
logger.error(f"Failed to cleanup corrupted databases: {e}")
|
|
149
287
|
# Continue anyway - the TUI can still function
|
|
150
288
|
|
|
151
|
-
app = ShotgunApp(
|
|
289
|
+
app = ShotgunApp(
|
|
290
|
+
no_update_check=no_update_check,
|
|
291
|
+
continue_session=continue_session,
|
|
292
|
+
force_reindex=force_reindex,
|
|
293
|
+
show_pull_hint=show_pull_hint,
|
|
294
|
+
pull_version_id=pull_version_id,
|
|
295
|
+
)
|
|
152
296
|
app.run(inline_no_clear=True)
|
|
153
297
|
|
|
154
298
|
|
|
299
|
+
def serve(
|
|
300
|
+
host: str = "localhost",
|
|
301
|
+
port: int = 8000,
|
|
302
|
+
public_url: str | None = None,
|
|
303
|
+
no_update_check: bool = False,
|
|
304
|
+
continue_session: bool = False,
|
|
305
|
+
force_reindex: bool = False,
|
|
306
|
+
) -> None:
|
|
307
|
+
"""Serve the TUI application as a web application.
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
host: Host address for the web server.
|
|
311
|
+
port: Port number for the web server.
|
|
312
|
+
public_url: Public URL if behind a proxy.
|
|
313
|
+
no_update_check: If True, disable automatic update checks.
|
|
314
|
+
continue_session: If True, continue from previous conversation.
|
|
315
|
+
force_reindex: If True, force re-indexing of codebase (ignores existing index).
|
|
316
|
+
"""
|
|
317
|
+
# Clean up any corrupted databases BEFORE starting the TUI
|
|
318
|
+
# This prevents crashes from corrupted databases during initialization
|
|
319
|
+
import asyncio
|
|
320
|
+
|
|
321
|
+
from textual_serve.server import Server
|
|
322
|
+
|
|
323
|
+
from shotgun.codebase.core.manager import CodebaseGraphManager
|
|
324
|
+
from shotgun.utils import get_shotgun_home
|
|
325
|
+
|
|
326
|
+
storage_dir = get_shotgun_home() / "codebases"
|
|
327
|
+
manager = CodebaseGraphManager(storage_dir)
|
|
328
|
+
|
|
329
|
+
try:
|
|
330
|
+
removed = asyncio.run(manager.cleanup_corrupted_databases())
|
|
331
|
+
if removed:
|
|
332
|
+
logger.info(
|
|
333
|
+
f"Cleaned up {len(removed)} corrupted database(s) before TUI startup"
|
|
334
|
+
)
|
|
335
|
+
except Exception as e:
|
|
336
|
+
logger.error(f"Failed to cleanup corrupted databases: {e}")
|
|
337
|
+
# Continue anyway - the TUI can still function
|
|
338
|
+
|
|
339
|
+
# Create a new event loop after asyncio.run() closes the previous one
|
|
340
|
+
# This is needed for the Server.serve() method
|
|
341
|
+
loop = asyncio.new_event_loop()
|
|
342
|
+
asyncio.set_event_loop(loop)
|
|
343
|
+
|
|
344
|
+
# Build the command string based on flags
|
|
345
|
+
command = "shotgun"
|
|
346
|
+
if no_update_check:
|
|
347
|
+
command += " --no-update-check"
|
|
348
|
+
if continue_session:
|
|
349
|
+
command += " --continue"
|
|
350
|
+
if force_reindex:
|
|
351
|
+
command += " --force-reindex"
|
|
352
|
+
|
|
353
|
+
# Create and start the server with hardcoded title and debug=False
|
|
354
|
+
server = Server(
|
|
355
|
+
command=command,
|
|
356
|
+
host=host,
|
|
357
|
+
port=port,
|
|
358
|
+
title="The Shotgun",
|
|
359
|
+
public_url=public_url,
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
# Set up graceful shutdown on SIGTERM/SIGINT
|
|
363
|
+
import signal
|
|
364
|
+
import sys
|
|
365
|
+
|
|
366
|
+
def signal_handler(_signum: int, _frame: Any) -> None:
|
|
367
|
+
"""Handle shutdown signals gracefully."""
|
|
368
|
+
from shotgun.posthog_telemetry import shutdown
|
|
369
|
+
|
|
370
|
+
logger.info("Received shutdown signal, cleaning up...")
|
|
371
|
+
# Restore stdout/stderr before shutting down
|
|
372
|
+
sys.stdout = original_stdout
|
|
373
|
+
sys.stderr = original_stderr
|
|
374
|
+
shutdown()
|
|
375
|
+
sys.exit(0)
|
|
376
|
+
|
|
377
|
+
signal.signal(signal.SIGTERM, signal_handler)
|
|
378
|
+
signal.signal(signal.SIGINT, signal_handler)
|
|
379
|
+
|
|
380
|
+
# Suppress the textual-serve banner by redirecting stdout/stderr
|
|
381
|
+
import io
|
|
382
|
+
|
|
383
|
+
# Capture and suppress the banner, but show the actual serving URL
|
|
384
|
+
original_stdout = sys.stdout
|
|
385
|
+
original_stderr = sys.stderr
|
|
386
|
+
|
|
387
|
+
captured_output = io.StringIO()
|
|
388
|
+
sys.stdout = captured_output
|
|
389
|
+
sys.stderr = captured_output
|
|
390
|
+
|
|
391
|
+
try:
|
|
392
|
+
# This will print the banner to our captured output
|
|
393
|
+
import logging
|
|
394
|
+
|
|
395
|
+
# Temporarily set logging to ERROR level to suppress INFO messages
|
|
396
|
+
textual_serve_logger = logging.getLogger("textual_serve")
|
|
397
|
+
original_level = textual_serve_logger.level
|
|
398
|
+
textual_serve_logger.setLevel(logging.ERROR)
|
|
399
|
+
|
|
400
|
+
# Print our own message to the original stdout
|
|
401
|
+
sys.stdout = original_stdout
|
|
402
|
+
sys.stderr = original_stderr
|
|
403
|
+
print(f"Serving Shotgun TUI at http://{host}:{port}")
|
|
404
|
+
print("Press Ctrl+C to quit")
|
|
405
|
+
|
|
406
|
+
# Now suppress output again for the serve call
|
|
407
|
+
sys.stdout = captured_output
|
|
408
|
+
sys.stderr = captured_output
|
|
409
|
+
|
|
410
|
+
server.serve(debug=False)
|
|
411
|
+
finally:
|
|
412
|
+
# Restore original stdout/stderr
|
|
413
|
+
sys.stdout = original_stdout
|
|
414
|
+
sys.stderr = original_stderr
|
|
415
|
+
if "textual_serve_logger" in locals():
|
|
416
|
+
textual_serve_logger.setLevel(original_level)
|
|
417
|
+
|
|
418
|
+
|
|
155
419
|
if __name__ == "__main__":
|
|
156
420
|
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
|
|