shotgun-sh 0.2.17__py3-none-any.whl → 0.4.0.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 +219 -37
- shotgun/agents/common.py +79 -78
- shotgun/agents/config/README.md +89 -0
- shotgun/agents/config/__init__.py +10 -1
- shotgun/agents/config/manager.py +364 -53
- shotgun/agents/config/models.py +101 -21
- shotgun/agents/config/provider.py +51 -13
- shotgun/agents/config/streaming_test.py +119 -0
- shotgun/agents/context_analyzer/analyzer.py +6 -2
- 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 +27 -1
- shotgun/agents/{history → conversation/history}/constants.py +5 -0
- shotgun/agents/conversation/history/file_content_deduplication.py +239 -0
- shotgun/agents/{history → conversation/history}/history_processors.py +267 -3
- shotgun/agents/{history → conversation/history}/token_counting/anthropic.py +8 -0
- shotgun/agents/{conversation_manager.py → conversation/manager.py} +1 -1
- shotgun/agents/{conversation_history.py → conversation/models.py} +8 -94
- shotgun/agents/error/__init__.py +11 -0
- shotgun/agents/error/models.py +19 -0
- shotgun/agents/export.py +12 -13
- shotgun/agents/models.py +66 -1
- shotgun/agents/plan.py +12 -13
- shotgun/agents/research.py +13 -10
- shotgun/agents/router/__init__.py +47 -0
- shotgun/agents/router/models.py +376 -0
- shotgun/agents/router/router.py +185 -0
- shotgun/agents/router/tools/__init__.py +18 -0
- shotgun/agents/router/tools/delegation_tools.py +503 -0
- shotgun/agents/router/tools/plan_tools.py +322 -0
- shotgun/agents/runner.py +230 -0
- shotgun/agents/specify.py +12 -13
- shotgun/agents/tasks.py +12 -13
- shotgun/agents/tools/file_management.py +49 -1
- shotgun/agents/tools/registry.py +2 -0
- shotgun/agents/tools/web_search/__init__.py +1 -2
- shotgun/agents/tools/web_search/gemini.py +1 -3
- shotgun/agents/tools/web_search/openai.py +1 -1
- shotgun/build_constants.py +2 -2
- shotgun/cli/clear.py +1 -1
- shotgun/cli/compact.py +5 -3
- shotgun/cli/context.py +44 -1
- shotgun/cli/error_handler.py +24 -0
- shotgun/cli/export.py +34 -34
- shotgun/cli/plan.py +34 -34
- shotgun/cli/research.py +17 -9
- 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/codebase/core/change_detector.py +1 -1
- shotgun/codebase/core/ingestor.py +154 -8
- shotgun/codebase/core/manager.py +1 -1
- shotgun/codebase/models.py +2 -0
- shotgun/exceptions.py +325 -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 +42 -0
- shotgun/main.py +4 -0
- shotgun/posthog_telemetry.py +1 -1
- shotgun/prompts/agents/export.j2 +2 -0
- shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +23 -3
- shotgun/prompts/agents/partials/interactive_mode.j2 +3 -3
- shotgun/prompts/agents/partials/router_delegation_mode.j2 +36 -0
- shotgun/prompts/agents/plan.j2 +29 -1
- shotgun/prompts/agents/research.j2 +75 -23
- shotgun/prompts/agents/router.j2 +440 -0
- shotgun/prompts/agents/specify.j2 +80 -4
- shotgun/prompts/agents/state/system_state.j2 +15 -8
- shotgun/prompts/agents/tasks.j2 +63 -23
- shotgun/prompts/history/chunk_summarization.j2 +34 -0
- shotgun/prompts/history/combine_summaries.j2 +53 -0
- shotgun/sdk/codebase.py +14 -3
- shotgun/settings.py +5 -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/tui/app.py +78 -15
- shotgun/tui/components/mode_indicator.py +120 -25
- shotgun/tui/components/status_bar.py +2 -2
- shotgun/tui/containers.py +1 -1
- shotgun/tui/dependencies.py +64 -9
- shotgun/tui/layout.py +5 -0
- shotgun/tui/protocols.py +37 -0
- shotgun/tui/screens/chat/chat.tcss +9 -1
- shotgun/tui/screens/chat/chat_screen.py +1015 -106
- shotgun/tui/screens/chat/codebase_index_prompt_screen.py +196 -17
- shotgun/tui/screens/chat_screen/command_providers.py +13 -89
- shotgun/tui/screens/chat_screen/hint_message.py +76 -1
- shotgun/tui/screens/chat_screen/history/agent_response.py +7 -3
- shotgun/tui/screens/chat_screen/history/chat_history.py +12 -0
- shotgun/tui/screens/chat_screen/history/formatters.py +53 -15
- shotgun/tui/screens/chat_screen/history/partial_response.py +11 -1
- shotgun/tui/screens/chat_screen/messages.py +219 -0
- shotgun/tui/screens/confirmation_dialog.py +40 -0
- shotgun/tui/screens/directory_setup.py +45 -41
- shotgun/tui/screens/feedback.py +10 -3
- shotgun/tui/screens/github_issue.py +11 -2
- shotgun/tui/screens/model_picker.py +28 -8
- shotgun/tui/screens/onboarding.py +179 -26
- shotgun/tui/screens/pipx_migration.py +58 -6
- shotgun/tui/screens/provider_config.py +66 -8
- 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 +110 -16
- shotgun/tui/screens/spec_pull.py +288 -0
- shotgun/tui/screens/welcome.py +123 -0
- shotgun/tui/services/conversation_service.py +5 -2
- shotgun/tui/utils/mode_progress.py +20 -86
- shotgun/tui/widgets/__init__.py +2 -1
- shotgun/tui/widgets/approval_widget.py +152 -0
- shotgun/tui/widgets/cascade_confirmation_widget.py +203 -0
- shotgun/tui/widgets/plan_panel.py +129 -0
- shotgun/tui/widgets/step_checkpoint_widget.py +180 -0
- shotgun/tui/widgets/widget_coordinator.py +1 -1
- {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.4.0.dev1.dist-info}/METADATA +11 -4
- shotgun_sh-0.4.0.dev1.dist-info/RECORD +242 -0
- {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.4.0.dev1.dist-info}/WHEEL +1 -1
- shotgun_sh-0.2.17.dist-info/RECORD +0 -194
- /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_counting/base.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_counting/openai.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_counting/sentencepiece_counter.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_counting/tokenizer_cache.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_counting/utils.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_estimation.py +0 -0
- {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.4.0.dev1.dist-info}/entry_points.txt +0 -0
- {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.4.0.dev1.dist-info}/licenses/LICENSE +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/tui/app.py
CHANGED
|
@@ -6,12 +6,19 @@ from textual.binding import Binding
|
|
|
6
6
|
from textual.screen import Screen
|
|
7
7
|
|
|
8
8
|
from shotgun.agents.agent_manager import AgentManager
|
|
9
|
-
from shotgun.agents.config import
|
|
9
|
+
from shotgun.agents.config import (
|
|
10
|
+
ConfigManager,
|
|
11
|
+
get_config_manager,
|
|
12
|
+
)
|
|
10
13
|
from shotgun.agents.models import AgentType
|
|
11
14
|
from shotgun.logging_config import get_logger
|
|
12
15
|
from shotgun.tui.containers import TUIContainer
|
|
16
|
+
from shotgun.tui.dependencies import create_default_router_deps
|
|
13
17
|
from shotgun.tui.screens.splash import SplashScreen
|
|
14
|
-
from shotgun.utils.file_system_utils import
|
|
18
|
+
from shotgun.utils.file_system_utils import (
|
|
19
|
+
ensure_shotgun_directory_exists,
|
|
20
|
+
get_shotgun_base_path,
|
|
21
|
+
)
|
|
15
22
|
from shotgun.utils.update_checker import (
|
|
16
23
|
detect_installation_method,
|
|
17
24
|
perform_auto_update_async,
|
|
@@ -31,10 +38,10 @@ logger = get_logger(__name__)
|
|
|
31
38
|
class ShotgunApp(App[None]):
|
|
32
39
|
# ChatScreen removed from SCREENS dict since it requires dependency injection
|
|
33
40
|
# and is instantiated manually in refresh_startup_screen()
|
|
41
|
+
# DirectorySetupScreen also removed since it requires error_message parameter
|
|
34
42
|
SCREENS = {
|
|
35
43
|
"provider_config": ProviderConfigScreen,
|
|
36
44
|
"model_picker": ModelPickerScreen,
|
|
37
|
-
"directory_setup": DirectorySetupScreen,
|
|
38
45
|
"github_issue": GitHubIssueScreen,
|
|
39
46
|
}
|
|
40
47
|
BINDINGS = [
|
|
@@ -48,12 +55,16 @@ class ShotgunApp(App[None]):
|
|
|
48
55
|
no_update_check: bool = False,
|
|
49
56
|
continue_session: bool = False,
|
|
50
57
|
force_reindex: bool = False,
|
|
58
|
+
show_pull_hint: bool = False,
|
|
59
|
+
pull_version_id: str | None = None,
|
|
51
60
|
) -> None:
|
|
52
61
|
super().__init__()
|
|
53
62
|
self.config_manager: ConfigManager = get_config_manager()
|
|
54
63
|
self.no_update_check = no_update_check
|
|
55
64
|
self.continue_session = continue_session
|
|
56
65
|
self.force_reindex = force_reindex
|
|
66
|
+
self.show_pull_hint = show_pull_hint
|
|
67
|
+
self.pull_version_id = pull_version_id
|
|
57
68
|
|
|
58
69
|
# Initialize dependency injection container
|
|
59
70
|
self.container = TUIContainer()
|
|
@@ -71,6 +82,8 @@ class ShotgunApp(App[None]):
|
|
|
71
82
|
"tui_started",
|
|
72
83
|
{
|
|
73
84
|
"installation_method": detect_installation_method(),
|
|
85
|
+
"terminal_width": self.size.width,
|
|
86
|
+
"terminal_height": self.size.height,
|
|
74
87
|
},
|
|
75
88
|
)
|
|
76
89
|
|
|
@@ -99,7 +112,10 @@ class ShotgunApp(App[None]):
|
|
|
99
112
|
# Run async config loading in worker
|
|
100
113
|
async def _check_config() -> None:
|
|
101
114
|
# Show welcome screen if no providers are configured OR if user hasn't seen it yet
|
|
115
|
+
# Note: If config migration fails, ConfigManager will auto-create fresh config
|
|
116
|
+
# and set migration_failed flag, which WelcomeScreen will display
|
|
102
117
|
config = await self.config_manager.load()
|
|
118
|
+
|
|
103
119
|
has_any_key = await self.config_manager.has_any_provider_key()
|
|
104
120
|
if not has_any_key or not config.shown_welcome_screen:
|
|
105
121
|
if isinstance(self.screen, WelcomeScreen):
|
|
@@ -111,27 +127,51 @@ class ShotgunApp(App[None]):
|
|
|
111
127
|
)
|
|
112
128
|
return
|
|
113
129
|
|
|
130
|
+
# Try to create .shotgun directory if it doesn't exist
|
|
114
131
|
if not self.check_local_shotgun_directory_exists():
|
|
115
|
-
|
|
132
|
+
try:
|
|
133
|
+
path = ensure_shotgun_directory_exists()
|
|
134
|
+
# Verify directory was created successfully
|
|
135
|
+
if not path.is_dir():
|
|
136
|
+
# Show error screen if creation failed
|
|
137
|
+
if isinstance(self.screen, DirectorySetupScreen):
|
|
138
|
+
return
|
|
139
|
+
self.push_screen(
|
|
140
|
+
DirectorySetupScreen(
|
|
141
|
+
error_message="Unable to create .shotgun directory due to filesystem conflict."
|
|
142
|
+
),
|
|
143
|
+
callback=lambda _arg: self.refresh_startup_screen(),
|
|
144
|
+
)
|
|
145
|
+
return
|
|
146
|
+
except Exception as exc:
|
|
147
|
+
# Show error screen if creation failed with exception
|
|
148
|
+
if isinstance(self.screen, DirectorySetupScreen):
|
|
149
|
+
return
|
|
150
|
+
self.push_screen(
|
|
151
|
+
DirectorySetupScreen(error_message=str(exc)),
|
|
152
|
+
callback=lambda _arg: self.refresh_startup_screen(),
|
|
153
|
+
)
|
|
116
154
|
return
|
|
117
155
|
|
|
118
|
-
|
|
119
|
-
DirectorySetupScreen(),
|
|
120
|
-
callback=lambda _arg: self.refresh_startup_screen(),
|
|
121
|
-
)
|
|
156
|
+
if isinstance(self.screen, ChatScreen):
|
|
122
157
|
return
|
|
123
158
|
|
|
124
|
-
|
|
159
|
+
# If we have a version to pull, show pull screen first
|
|
160
|
+
if self.pull_version_id:
|
|
161
|
+
from .screens.spec_pull import SpecPullScreen
|
|
162
|
+
|
|
163
|
+
self.push_screen(
|
|
164
|
+
SpecPullScreen(self.pull_version_id),
|
|
165
|
+
callback=self._handle_pull_complete,
|
|
166
|
+
)
|
|
125
167
|
return
|
|
126
168
|
|
|
127
169
|
# Create ChatScreen with all dependencies injected from container
|
|
128
|
-
# Get the default agent mode (
|
|
129
|
-
agent_mode = AgentType.
|
|
130
|
-
|
|
131
|
-
# Create AgentDeps asynchronously (get_provider_model is now async)
|
|
132
|
-
from shotgun.tui.dependencies import create_default_tui_deps
|
|
170
|
+
# Get the default agent mode (ROUTER)
|
|
171
|
+
agent_mode = AgentType.ROUTER
|
|
133
172
|
|
|
134
|
-
|
|
173
|
+
# Create RouterDeps asynchronously (get_provider_model is now async)
|
|
174
|
+
agent_deps = await create_default_router_deps()
|
|
135
175
|
|
|
136
176
|
# Create AgentManager with async initialization
|
|
137
177
|
agent_manager = AgentManager(deps=agent_deps, initial_type=agent_mode)
|
|
@@ -155,6 +195,7 @@ class ShotgunApp(App[None]):
|
|
|
155
195
|
deps=agent_deps,
|
|
156
196
|
continue_session=self.continue_session,
|
|
157
197
|
force_reindex=self.force_reindex,
|
|
198
|
+
show_pull_hint=self.show_pull_hint,
|
|
158
199
|
)
|
|
159
200
|
|
|
160
201
|
# Update the ProcessingStateManager and WidgetCoordinator with the actual ChatScreen instance
|
|
@@ -170,6 +211,22 @@ class ShotgunApp(App[None]):
|
|
|
170
211
|
shotgun_dir = get_shotgun_base_path()
|
|
171
212
|
return shotgun_dir.exists() and shotgun_dir.is_dir()
|
|
172
213
|
|
|
214
|
+
def _handle_pull_complete(self, success: bool | None) -> None:
|
|
215
|
+
"""Handle completion of spec pull screen.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
success: Whether the pull was successful, or None if dismissed.
|
|
219
|
+
"""
|
|
220
|
+
# Clear version_id so we don't pull again on next refresh
|
|
221
|
+
self.pull_version_id = None
|
|
222
|
+
|
|
223
|
+
if success:
|
|
224
|
+
# Enable hint for ChatScreen
|
|
225
|
+
self.show_pull_hint = True
|
|
226
|
+
|
|
227
|
+
# Continue to ChatScreen
|
|
228
|
+
self.refresh_startup_screen()
|
|
229
|
+
|
|
173
230
|
async def action_quit(self) -> None:
|
|
174
231
|
"""Quit the application."""
|
|
175
232
|
# Shut down PostHog client to prevent threading errors
|
|
@@ -196,6 +253,8 @@ def run(
|
|
|
196
253
|
no_update_check: bool = False,
|
|
197
254
|
continue_session: bool = False,
|
|
198
255
|
force_reindex: bool = False,
|
|
256
|
+
show_pull_hint: bool = False,
|
|
257
|
+
pull_version_id: str | None = None,
|
|
199
258
|
) -> None:
|
|
200
259
|
"""Run the TUI application.
|
|
201
260
|
|
|
@@ -203,6 +262,8 @@ def run(
|
|
|
203
262
|
no_update_check: If True, disable automatic update checks.
|
|
204
263
|
continue_session: If True, continue from previous conversation.
|
|
205
264
|
force_reindex: If True, force re-indexing of codebase (ignores existing index).
|
|
265
|
+
show_pull_hint: If True, show hint about recently pulled spec.
|
|
266
|
+
pull_version_id: If provided, pull this spec version before showing ChatScreen.
|
|
206
267
|
"""
|
|
207
268
|
# Clean up any corrupted databases BEFORE starting the TUI
|
|
208
269
|
# This prevents crashes from corrupted databases during initialization
|
|
@@ -228,6 +289,8 @@ def run(
|
|
|
228
289
|
no_update_check=no_update_check,
|
|
229
290
|
continue_session=continue_session,
|
|
230
291
|
force_reindex=force_reindex,
|
|
292
|
+
show_pull_hint=show_pull_hint,
|
|
293
|
+
pull_version_id=pull_version_id,
|
|
231
294
|
)
|
|
232
295
|
app.run(inline_no_clear=True)
|
|
233
296
|
|
|
@@ -1,20 +1,68 @@
|
|
|
1
1
|
"""Widget to display the current agent mode."""
|
|
2
2
|
|
|
3
|
+
from enum import StrEnum
|
|
4
|
+
|
|
3
5
|
from textual.widget import Widget
|
|
4
6
|
|
|
5
7
|
from shotgun.agents.models import AgentType
|
|
6
|
-
from shotgun.
|
|
8
|
+
from shotgun.agents.router.models import RouterMode
|
|
9
|
+
from shotgun.tui.protocols import (
|
|
10
|
+
ActiveSubAgentProvider,
|
|
11
|
+
QAStateProvider,
|
|
12
|
+
RouterModeProvider,
|
|
13
|
+
)
|
|
7
14
|
from shotgun.tui.utils.mode_progress import PlaceholderHints
|
|
8
15
|
|
|
9
16
|
|
|
17
|
+
class RouterModeCssClass(StrEnum):
|
|
18
|
+
"""CSS class names for router mode styling."""
|
|
19
|
+
|
|
20
|
+
PLANNING = "mode-planning"
|
|
21
|
+
DRAFTING = "mode-drafting"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Shared display name mapping for agent types
|
|
25
|
+
AGENT_DISPLAY_NAMES: dict[AgentType, str] = {
|
|
26
|
+
AgentType.RESEARCH: "Research",
|
|
27
|
+
AgentType.SPECIFY: "Specify",
|
|
28
|
+
AgentType.PLAN: "Planning",
|
|
29
|
+
AgentType.TASKS: "Tasks",
|
|
30
|
+
AgentType.EXPORT: "Export",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# Mode descriptions for legacy agent display
|
|
34
|
+
AGENT_DESCRIPTIONS: dict[AgentType, str] = {
|
|
35
|
+
AgentType.RESEARCH: "Research topics with web search and synthesize findings",
|
|
36
|
+
AgentType.PLAN: "Create comprehensive, actionable plans with milestones",
|
|
37
|
+
AgentType.TASKS: "Generate specific, actionable tasks from research and plans",
|
|
38
|
+
AgentType.SPECIFY: "Create detailed specifications and requirements documents",
|
|
39
|
+
AgentType.EXPORT: "Export artifacts and findings to various formats",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
10
43
|
class ModeIndicator(Widget):
|
|
11
|
-
"""Widget to display the current agent mode.
|
|
44
|
+
"""Widget to display the current agent mode.
|
|
45
|
+
|
|
46
|
+
For router mode, displays:
|
|
47
|
+
- Idle: "📋 Planning mode" or "✍️ Drafting mode"
|
|
48
|
+
- During execution: "📋 Planning → Research" format
|
|
49
|
+
|
|
50
|
+
For legacy agents, displays the agent name and description.
|
|
51
|
+
"""
|
|
12
52
|
|
|
13
53
|
DEFAULT_CSS = """
|
|
14
54
|
ModeIndicator {
|
|
15
55
|
text-wrap: wrap;
|
|
16
56
|
padding-left: 1;
|
|
17
57
|
}
|
|
58
|
+
|
|
59
|
+
ModeIndicator.mode-planning {
|
|
60
|
+
/* Planning mode styling - blue/cyan accent */
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
ModeIndicator.mode-drafting {
|
|
64
|
+
/* Drafting mode styling - green accent */
|
|
65
|
+
}
|
|
18
66
|
"""
|
|
19
67
|
|
|
20
68
|
def __init__(self, mode: AgentType) -> None:
|
|
@@ -29,41 +77,88 @@ class ModeIndicator(Widget):
|
|
|
29
77
|
|
|
30
78
|
def render(self) -> str:
|
|
31
79
|
"""Render the mode indicator."""
|
|
32
|
-
# Check if in Q&A mode first
|
|
80
|
+
# Check if in Q&A mode first - takes priority
|
|
33
81
|
if isinstance(self.screen, QAStateProvider) and self.screen.qa_mode:
|
|
34
82
|
return (
|
|
35
83
|
"[bold $text-accent]Q&A mode[/]"
|
|
36
84
|
"[$foreground-muted] (Answer the clarifying questions or ESC to cancel)[/]"
|
|
37
85
|
)
|
|
38
86
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
}
|
|
87
|
+
# Router mode display
|
|
88
|
+
if self.mode == AgentType.ROUTER:
|
|
89
|
+
return self._render_router_mode()
|
|
59
90
|
|
|
60
|
-
|
|
61
|
-
|
|
91
|
+
# Legacy agent mode display
|
|
92
|
+
return self._render_legacy_mode()
|
|
93
|
+
|
|
94
|
+
def _render_router_mode(self) -> str:
|
|
95
|
+
"""Render the router mode indicator.
|
|
96
|
+
|
|
97
|
+
Shows:
|
|
98
|
+
- "📋 Planning mode" or "✍️ Drafting mode" when idle
|
|
99
|
+
- "📋 Planning → Research" format when sub-agent is executing
|
|
100
|
+
"""
|
|
101
|
+
# Get router mode from screen
|
|
102
|
+
router_mode: str | None = None
|
|
103
|
+
if isinstance(self.screen, RouterModeProvider):
|
|
104
|
+
router_mode = self.screen.router_mode
|
|
105
|
+
|
|
106
|
+
# Get active sub-agent from screen
|
|
107
|
+
active_sub_agent: AgentType | None = None
|
|
108
|
+
if isinstance(self.screen, ActiveSubAgentProvider):
|
|
109
|
+
sub_agent_str = self.screen.active_sub_agent
|
|
110
|
+
if sub_agent_str:
|
|
111
|
+
# Convert string back to AgentType enum
|
|
112
|
+
try:
|
|
113
|
+
active_sub_agent = AgentType(sub_agent_str)
|
|
114
|
+
except ValueError:
|
|
115
|
+
pass
|
|
116
|
+
|
|
117
|
+
# Determine mode display using RouterMode enum
|
|
118
|
+
if router_mode == RouterMode.DRAFTING.value:
|
|
119
|
+
icon = "✍️"
|
|
120
|
+
mode_name = "Drafting"
|
|
121
|
+
description = "Auto-execute without confirmation"
|
|
122
|
+
css_class = RouterModeCssClass.DRAFTING
|
|
123
|
+
else:
|
|
124
|
+
# Default to planning mode
|
|
125
|
+
icon = "📋"
|
|
126
|
+
mode_name = "Planning"
|
|
127
|
+
description = "Review plans before execution"
|
|
128
|
+
css_class = RouterModeCssClass.PLANNING
|
|
129
|
+
|
|
130
|
+
# Update CSS class for styling
|
|
131
|
+
self.set_classes(css_class)
|
|
132
|
+
|
|
133
|
+
# Add sub-agent suffix if executing
|
|
134
|
+
if active_sub_agent:
|
|
135
|
+
# Use shared display name mapping
|
|
136
|
+
sub_agent_name = AGENT_DISPLAY_NAMES.get(
|
|
137
|
+
active_sub_agent, active_sub_agent.value.title()
|
|
138
|
+
)
|
|
139
|
+
return f"[bold $text-accent]{icon} {mode_name} → {sub_agent_name}[/]"
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
f"[bold $text-accent]{icon} {mode_name} mode[/]"
|
|
143
|
+
f"[$foreground-muted] ({description})[/]"
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
def _render_legacy_mode(self) -> str:
|
|
147
|
+
"""Render the legacy agent mode indicator.
|
|
148
|
+
|
|
149
|
+
Shows the agent name with description and content status.
|
|
150
|
+
"""
|
|
151
|
+
mode_title = AGENT_DISPLAY_NAMES.get(self.mode, self.mode.value.title())
|
|
152
|
+
description = AGENT_DESCRIPTIONS.get(self.mode, "")
|
|
62
153
|
|
|
63
154
|
# Check if mode has content
|
|
64
155
|
has_content = self.progress_checker.has_mode_content(self.mode)
|
|
65
156
|
status_icon = " ✓" if has_content else ""
|
|
66
157
|
|
|
158
|
+
# Clear any router mode CSS classes
|
|
159
|
+
self.remove_class(RouterModeCssClass.PLANNING)
|
|
160
|
+
self.remove_class(RouterModeCssClass.DRAFTING)
|
|
161
|
+
|
|
67
162
|
return (
|
|
68
163
|
f"[bold $text-accent]{mode_title}{status_icon} mode[/]"
|
|
69
164
|
f"[$foreground-muted] ({description})[/]"
|
|
@@ -37,12 +37,12 @@ class StatusBar(Widget):
|
|
|
37
37
|
return (
|
|
38
38
|
"[$foreground-muted][bold $text]esc[/] to stop • "
|
|
39
39
|
"[bold $text]enter[/] to send • [bold $text]ctrl+j[/] for newline • "
|
|
40
|
-
"[bold $text]ctrl+p[/] command palette • [bold $text]shift+tab[/]
|
|
40
|
+
"[bold $text]ctrl+p[/] command palette • [bold $text]shift+tab[/] toggle mode • "
|
|
41
41
|
"/help for commands[/]"
|
|
42
42
|
)
|
|
43
43
|
else:
|
|
44
44
|
return (
|
|
45
45
|
"[$foreground-muted][bold $text]enter[/] to send • "
|
|
46
46
|
"[bold $text]ctrl+j[/] for newline • [bold $text]ctrl+p[/] command palette • "
|
|
47
|
-
"[bold $text]shift+tab[/]
|
|
47
|
+
"[bold $text]shift+tab[/] toggle mode • /help for commands[/]"
|
|
48
48
|
)
|
shotgun/tui/containers.py
CHANGED
|
@@ -5,7 +5,7 @@ from typing import TYPE_CHECKING
|
|
|
5
5
|
from dependency_injector import containers, providers
|
|
6
6
|
from pydantic_ai import RunContext
|
|
7
7
|
|
|
8
|
-
from shotgun.agents.
|
|
8
|
+
from shotgun.agents.conversation import ConversationManager
|
|
9
9
|
from shotgun.agents.models import AgentDeps
|
|
10
10
|
from shotgun.sdk.codebase import CodebaseSDK
|
|
11
11
|
from shotgun.tui.commands import CommandHandler
|
shotgun/tui/dependencies.py
CHANGED
|
@@ -1,13 +1,44 @@
|
|
|
1
1
|
"""Dependency creation utilities for TUI components."""
|
|
2
2
|
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
3
5
|
from pydantic_ai import RunContext
|
|
4
6
|
|
|
5
|
-
from shotgun.agents.config import get_provider_model
|
|
7
|
+
from shotgun.agents.config import get_config_manager, get_provider_model
|
|
8
|
+
from shotgun.agents.config.models import ModelConfig
|
|
6
9
|
from shotgun.agents.models import AgentDeps
|
|
10
|
+
from shotgun.agents.router.models import RouterDeps, RouterMode
|
|
11
|
+
from shotgun.codebase.service import CodebaseService
|
|
7
12
|
from shotgun.tui.filtered_codebase_service import FilteredCodebaseService
|
|
8
13
|
from shotgun.utils import get_shotgun_home
|
|
9
14
|
|
|
10
15
|
|
|
16
|
+
async def _get_tui_config() -> tuple[ModelConfig, CodebaseService]:
|
|
17
|
+
"""Get common TUI configuration components.
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
Tuple of (model_config, codebase_service) for TUI deps.
|
|
21
|
+
"""
|
|
22
|
+
model_config = await get_provider_model()
|
|
23
|
+
storage_dir = get_shotgun_home() / "codebases"
|
|
24
|
+
codebase_service = FilteredCodebaseService(storage_dir)
|
|
25
|
+
return model_config, codebase_service
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _placeholder_system_prompt_fn(ctx: RunContext[Any]) -> str:
|
|
29
|
+
"""Placeholder system prompt that should never be called.
|
|
30
|
+
|
|
31
|
+
Agents provide their own system_prompt_fn via their create functions.
|
|
32
|
+
This placeholder exists only to satisfy the AgentDeps requirement.
|
|
33
|
+
|
|
34
|
+
Raises:
|
|
35
|
+
RuntimeError: Always, as this should never be invoked.
|
|
36
|
+
"""
|
|
37
|
+
raise RuntimeError(
|
|
38
|
+
"This should not be called - agents provide their own system_prompt_fn"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
11
42
|
async def create_default_tui_deps() -> AgentDeps:
|
|
12
43
|
"""Create default AgentDeps for TUI components.
|
|
13
44
|
|
|
@@ -21,14 +52,7 @@ async def create_default_tui_deps() -> AgentDeps:
|
|
|
21
52
|
Returns:
|
|
22
53
|
Configured AgentDeps instance ready for TUI use.
|
|
23
54
|
"""
|
|
24
|
-
model_config = await
|
|
25
|
-
storage_dir = get_shotgun_home() / "codebases"
|
|
26
|
-
codebase_service = FilteredCodebaseService(storage_dir)
|
|
27
|
-
|
|
28
|
-
def _placeholder_system_prompt_fn(ctx: RunContext[AgentDeps]) -> str:
|
|
29
|
-
raise RuntimeError(
|
|
30
|
-
"This should not be called - agents provide their own system_prompt_fn"
|
|
31
|
-
)
|
|
55
|
+
model_config, codebase_service = await _get_tui_config()
|
|
32
56
|
|
|
33
57
|
return AgentDeps(
|
|
34
58
|
interactive_mode=True,
|
|
@@ -37,3 +61,34 @@ async def create_default_tui_deps() -> AgentDeps:
|
|
|
37
61
|
codebase_service=codebase_service,
|
|
38
62
|
system_prompt_fn=_placeholder_system_prompt_fn,
|
|
39
63
|
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
async def create_default_router_deps() -> RouterDeps:
|
|
67
|
+
"""Create default RouterDeps for TUI components with router mode.
|
|
68
|
+
|
|
69
|
+
This creates a RouterDeps configuration suitable for interactive
|
|
70
|
+
TUI usage with:
|
|
71
|
+
- Router mode loaded from config (default: PLANNING)
|
|
72
|
+
- Interactive mode enabled
|
|
73
|
+
- TUI context flag set
|
|
74
|
+
- Filtered codebase service (restricted to CWD)
|
|
75
|
+
- Placeholder system prompt (router provides its own)
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Configured RouterDeps instance ready for TUI use.
|
|
79
|
+
"""
|
|
80
|
+
model_config, codebase_service = await _get_tui_config()
|
|
81
|
+
|
|
82
|
+
# Load router mode from config (default to PLANNING)
|
|
83
|
+
config_manager = get_config_manager()
|
|
84
|
+
config = await config_manager.load()
|
|
85
|
+
router_mode = RouterMode(config.router_mode)
|
|
86
|
+
|
|
87
|
+
return RouterDeps(
|
|
88
|
+
interactive_mode=True,
|
|
89
|
+
is_tui_context=True,
|
|
90
|
+
llm_model=model_config,
|
|
91
|
+
codebase_service=codebase_service,
|
|
92
|
+
system_prompt_fn=_placeholder_system_prompt_fn,
|
|
93
|
+
router_mode=router_mode,
|
|
94
|
+
)
|
shotgun/tui/layout.py
ADDED
shotgun/tui/protocols.py
CHANGED
|
@@ -43,3 +43,40 @@ class ProcessingStateProvider(Protocol):
|
|
|
43
43
|
True if an agent is processing, False otherwise.
|
|
44
44
|
"""
|
|
45
45
|
...
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@runtime_checkable
|
|
49
|
+
class RouterModeProvider(Protocol):
|
|
50
|
+
"""Protocol for screens that provide router mode state.
|
|
51
|
+
|
|
52
|
+
This protocol allows components to check the current router mode
|
|
53
|
+
(Planning or Drafting) without importing the concrete ChatScreen class.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def router_mode(self) -> str | None:
|
|
58
|
+
"""The current router mode.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
'planning' or 'drafting' if in router mode, None otherwise.
|
|
62
|
+
"""
|
|
63
|
+
...
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@runtime_checkable
|
|
67
|
+
class ActiveSubAgentProvider(Protocol):
|
|
68
|
+
"""Protocol for screens that provide active sub-agent state.
|
|
69
|
+
|
|
70
|
+
This protocol allows components to check which sub-agent is currently
|
|
71
|
+
executing during router delegation without importing ChatScreen.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def active_sub_agent(self) -> str | None:
|
|
76
|
+
"""The currently executing sub-agent type.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
The sub-agent type string (e.g., 'research', 'specify') if
|
|
80
|
+
a sub-agent is executing, None if idle.
|
|
81
|
+
"""
|
|
82
|
+
...
|
|
@@ -16,11 +16,19 @@ ModeIndicator {
|
|
|
16
16
|
height: auto;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
ModeIndicator.mode-planning {
|
|
20
|
+
/* Blue/cyan accent for planning mode */
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
ModeIndicator.mode-drafting {
|
|
24
|
+
/* Green accent for drafting mode */
|
|
25
|
+
}
|
|
26
|
+
|
|
19
27
|
#footer {
|
|
20
28
|
dock: bottom;
|
|
21
29
|
height: auto;
|
|
22
30
|
padding: 1 1 1 2;
|
|
23
|
-
max-height:
|
|
31
|
+
max-height: 24;
|
|
24
32
|
}
|
|
25
33
|
|
|
26
34
|
#window {
|