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.
Files changed (150) hide show
  1. shotgun/agents/agent_manager.py +219 -37
  2. shotgun/agents/common.py +79 -78
  3. shotgun/agents/config/README.md +89 -0
  4. shotgun/agents/config/__init__.py +10 -1
  5. shotgun/agents/config/manager.py +364 -53
  6. shotgun/agents/config/models.py +101 -21
  7. shotgun/agents/config/provider.py +51 -13
  8. shotgun/agents/config/streaming_test.py +119 -0
  9. shotgun/agents/context_analyzer/analyzer.py +6 -2
  10. shotgun/agents/conversation/__init__.py +18 -0
  11. shotgun/agents/conversation/filters.py +164 -0
  12. shotgun/agents/conversation/history/chunking.py +278 -0
  13. shotgun/agents/{history → conversation/history}/compaction.py +27 -1
  14. shotgun/agents/{history → conversation/history}/constants.py +5 -0
  15. shotgun/agents/conversation/history/file_content_deduplication.py +239 -0
  16. shotgun/agents/{history → conversation/history}/history_processors.py +267 -3
  17. shotgun/agents/{history → conversation/history}/token_counting/anthropic.py +8 -0
  18. shotgun/agents/{conversation_manager.py → conversation/manager.py} +1 -1
  19. shotgun/agents/{conversation_history.py → conversation/models.py} +8 -94
  20. shotgun/agents/error/__init__.py +11 -0
  21. shotgun/agents/error/models.py +19 -0
  22. shotgun/agents/export.py +12 -13
  23. shotgun/agents/models.py +66 -1
  24. shotgun/agents/plan.py +12 -13
  25. shotgun/agents/research.py +13 -10
  26. shotgun/agents/router/__init__.py +47 -0
  27. shotgun/agents/router/models.py +376 -0
  28. shotgun/agents/router/router.py +185 -0
  29. shotgun/agents/router/tools/__init__.py +18 -0
  30. shotgun/agents/router/tools/delegation_tools.py +503 -0
  31. shotgun/agents/router/tools/plan_tools.py +322 -0
  32. shotgun/agents/runner.py +230 -0
  33. shotgun/agents/specify.py +12 -13
  34. shotgun/agents/tasks.py +12 -13
  35. shotgun/agents/tools/file_management.py +49 -1
  36. shotgun/agents/tools/registry.py +2 -0
  37. shotgun/agents/tools/web_search/__init__.py +1 -2
  38. shotgun/agents/tools/web_search/gemini.py +1 -3
  39. shotgun/agents/tools/web_search/openai.py +1 -1
  40. shotgun/build_constants.py +2 -2
  41. shotgun/cli/clear.py +1 -1
  42. shotgun/cli/compact.py +5 -3
  43. shotgun/cli/context.py +44 -1
  44. shotgun/cli/error_handler.py +24 -0
  45. shotgun/cli/export.py +34 -34
  46. shotgun/cli/plan.py +34 -34
  47. shotgun/cli/research.py +17 -9
  48. shotgun/cli/spec/__init__.py +5 -0
  49. shotgun/cli/spec/backup.py +81 -0
  50. shotgun/cli/spec/commands.py +132 -0
  51. shotgun/cli/spec/models.py +48 -0
  52. shotgun/cli/spec/pull_service.py +219 -0
  53. shotgun/cli/specify.py +20 -19
  54. shotgun/cli/tasks.py +34 -34
  55. shotgun/codebase/core/change_detector.py +1 -1
  56. shotgun/codebase/core/ingestor.py +154 -8
  57. shotgun/codebase/core/manager.py +1 -1
  58. shotgun/codebase/models.py +2 -0
  59. shotgun/exceptions.py +325 -0
  60. shotgun/llm_proxy/__init__.py +17 -0
  61. shotgun/llm_proxy/client.py +215 -0
  62. shotgun/llm_proxy/models.py +137 -0
  63. shotgun/logging_config.py +42 -0
  64. shotgun/main.py +4 -0
  65. shotgun/posthog_telemetry.py +1 -1
  66. shotgun/prompts/agents/export.j2 +2 -0
  67. shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +23 -3
  68. shotgun/prompts/agents/partials/interactive_mode.j2 +3 -3
  69. shotgun/prompts/agents/partials/router_delegation_mode.j2 +36 -0
  70. shotgun/prompts/agents/plan.j2 +29 -1
  71. shotgun/prompts/agents/research.j2 +75 -23
  72. shotgun/prompts/agents/router.j2 +440 -0
  73. shotgun/prompts/agents/specify.j2 +80 -4
  74. shotgun/prompts/agents/state/system_state.j2 +15 -8
  75. shotgun/prompts/agents/tasks.j2 +63 -23
  76. shotgun/prompts/history/chunk_summarization.j2 +34 -0
  77. shotgun/prompts/history/combine_summaries.j2 +53 -0
  78. shotgun/sdk/codebase.py +14 -3
  79. shotgun/settings.py +5 -0
  80. shotgun/shotgun_web/__init__.py +67 -1
  81. shotgun/shotgun_web/client.py +42 -1
  82. shotgun/shotgun_web/constants.py +46 -0
  83. shotgun/shotgun_web/exceptions.py +29 -0
  84. shotgun/shotgun_web/models.py +390 -0
  85. shotgun/shotgun_web/shared_specs/__init__.py +32 -0
  86. shotgun/shotgun_web/shared_specs/file_scanner.py +175 -0
  87. shotgun/shotgun_web/shared_specs/hasher.py +83 -0
  88. shotgun/shotgun_web/shared_specs/models.py +71 -0
  89. shotgun/shotgun_web/shared_specs/upload_pipeline.py +329 -0
  90. shotgun/shotgun_web/shared_specs/utils.py +34 -0
  91. shotgun/shotgun_web/specs_client.py +703 -0
  92. shotgun/shotgun_web/supabase_client.py +31 -0
  93. shotgun/tui/app.py +78 -15
  94. shotgun/tui/components/mode_indicator.py +120 -25
  95. shotgun/tui/components/status_bar.py +2 -2
  96. shotgun/tui/containers.py +1 -1
  97. shotgun/tui/dependencies.py +64 -9
  98. shotgun/tui/layout.py +5 -0
  99. shotgun/tui/protocols.py +37 -0
  100. shotgun/tui/screens/chat/chat.tcss +9 -1
  101. shotgun/tui/screens/chat/chat_screen.py +1015 -106
  102. shotgun/tui/screens/chat/codebase_index_prompt_screen.py +196 -17
  103. shotgun/tui/screens/chat_screen/command_providers.py +13 -89
  104. shotgun/tui/screens/chat_screen/hint_message.py +76 -1
  105. shotgun/tui/screens/chat_screen/history/agent_response.py +7 -3
  106. shotgun/tui/screens/chat_screen/history/chat_history.py +12 -0
  107. shotgun/tui/screens/chat_screen/history/formatters.py +53 -15
  108. shotgun/tui/screens/chat_screen/history/partial_response.py +11 -1
  109. shotgun/tui/screens/chat_screen/messages.py +219 -0
  110. shotgun/tui/screens/confirmation_dialog.py +40 -0
  111. shotgun/tui/screens/directory_setup.py +45 -41
  112. shotgun/tui/screens/feedback.py +10 -3
  113. shotgun/tui/screens/github_issue.py +11 -2
  114. shotgun/tui/screens/model_picker.py +28 -8
  115. shotgun/tui/screens/onboarding.py +179 -26
  116. shotgun/tui/screens/pipx_migration.py +58 -6
  117. shotgun/tui/screens/provider_config.py +66 -8
  118. shotgun/tui/screens/shared_specs/__init__.py +21 -0
  119. shotgun/tui/screens/shared_specs/create_spec_dialog.py +273 -0
  120. shotgun/tui/screens/shared_specs/models.py +56 -0
  121. shotgun/tui/screens/shared_specs/share_specs_dialog.py +390 -0
  122. shotgun/tui/screens/shared_specs/upload_progress_screen.py +452 -0
  123. shotgun/tui/screens/shotgun_auth.py +110 -16
  124. shotgun/tui/screens/spec_pull.py +288 -0
  125. shotgun/tui/screens/welcome.py +123 -0
  126. shotgun/tui/services/conversation_service.py +5 -2
  127. shotgun/tui/utils/mode_progress.py +20 -86
  128. shotgun/tui/widgets/__init__.py +2 -1
  129. shotgun/tui/widgets/approval_widget.py +152 -0
  130. shotgun/tui/widgets/cascade_confirmation_widget.py +203 -0
  131. shotgun/tui/widgets/plan_panel.py +129 -0
  132. shotgun/tui/widgets/step_checkpoint_widget.py +180 -0
  133. shotgun/tui/widgets/widget_coordinator.py +1 -1
  134. {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.4.0.dev1.dist-info}/METADATA +11 -4
  135. shotgun_sh-0.4.0.dev1.dist-info/RECORD +242 -0
  136. {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.4.0.dev1.dist-info}/WHEEL +1 -1
  137. shotgun_sh-0.2.17.dist-info/RECORD +0 -194
  138. /shotgun/agents/{history → conversation/history}/__init__.py +0 -0
  139. /shotgun/agents/{history → conversation/history}/context_extraction.py +0 -0
  140. /shotgun/agents/{history → conversation/history}/history_building.py +0 -0
  141. /shotgun/agents/{history → conversation/history}/message_utils.py +0 -0
  142. /shotgun/agents/{history → conversation/history}/token_counting/__init__.py +0 -0
  143. /shotgun/agents/{history → conversation/history}/token_counting/base.py +0 -0
  144. /shotgun/agents/{history → conversation/history}/token_counting/openai.py +0 -0
  145. /shotgun/agents/{history → conversation/history}/token_counting/sentencepiece_counter.py +0 -0
  146. /shotgun/agents/{history → conversation/history}/token_counting/tokenizer_cache.py +0 -0
  147. /shotgun/agents/{history → conversation/history}/token_counting/utils.py +0 -0
  148. /shotgun/agents/{history → conversation/history}/token_estimation.py +0 -0
  149. {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.4.0.dev1.dist-info}/entry_points.txt +0 -0
  150. {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 ConfigManager, get_config_manager
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 get_shotgun_base_path
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
- if isinstance(self.screen, DirectorySetupScreen):
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
- self.push_screen(
119
- DirectorySetupScreen(),
120
- callback=lambda _arg: self.refresh_startup_screen(),
121
- )
156
+ if isinstance(self.screen, ChatScreen):
122
157
  return
123
158
 
124
- if isinstance(self.screen, ChatScreen):
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 (RESEARCH)
129
- agent_mode = AgentType.RESEARCH
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
- agent_deps = await create_default_tui_deps()
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.tui.protocols import QAStateProvider
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
- 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
- }
87
+ # Router mode display
88
+ if self.mode == AgentType.ROUTER:
89
+ return self._render_router_mode()
59
90
 
60
- mode_title = mode_display.get(self.mode, self.mode.value.title())
61
- description = mode_description.get(self.mode, "")
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[/] cycle modes • "
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[/] cycle modes • /help for commands[/]"
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.conversation_manager import ConversationManager
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
@@ -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 get_provider_model()
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
@@ -0,0 +1,5 @@
1
+ """Layout utilities for responsive terminal UI."""
2
+
3
+ # Height thresholds for responsive layouts
4
+ TINY_HEIGHT_THRESHOLD = 25 # Below this: minimal UI, hide most content
5
+ COMPACT_HEIGHT_THRESHOLD = 35 # Below this: reduced padding, hide verbose text
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: 14;
31
+ max-height: 24;
24
32
  }
25
33
 
26
34
  #window {