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
@@ -2,14 +2,18 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import webbrowser
5
6
  from typing import TYPE_CHECKING, cast
6
7
 
7
8
  from textual import on
8
9
  from textual.app import ComposeResult
9
10
  from textual.containers import Container, Horizontal, Vertical
11
+ from textual.events import Resize
10
12
  from textual.screen import Screen
11
13
  from textual.widgets import Button, Markdown, Static
12
14
 
15
+ from shotgun.tui.layout import TINY_HEIGHT_THRESHOLD
16
+
13
17
  if TYPE_CHECKING:
14
18
  from ..app import ShotgunApp
15
19
 
@@ -85,6 +89,69 @@ class WelcomeScreen(Screen[None]):
85
89
  margin: 1 0 0 0;
86
90
  width: 100%;
87
91
  }
92
+
93
+ #migration-warning {
94
+ width: 80%;
95
+ height: auto;
96
+ padding: 2;
97
+ margin: 1 0;
98
+ border: solid $warning;
99
+ background: $warning 20%;
100
+ }
101
+
102
+ #migration-warning-title {
103
+ text-style: bold;
104
+ color: $warning;
105
+ padding: 0 0 1 0;
106
+ }
107
+
108
+ /* Tiny screen fallback */
109
+ #tiny-welcome-container {
110
+ display: none;
111
+ width: 100%;
112
+ height: auto;
113
+ padding: 0;
114
+ align: center middle;
115
+ }
116
+
117
+ #tiny-welcome-message {
118
+ text-align: center;
119
+ padding: 0;
120
+ }
121
+
122
+ #tiny-welcome-link {
123
+ text-align: center;
124
+ padding: 0;
125
+ color: $accent;
126
+ }
127
+
128
+ #tiny-welcome-buttons {
129
+ width: auto;
130
+ height: auto;
131
+ padding: 0;
132
+ align: center middle;
133
+ }
134
+
135
+ #tiny-welcome-buttons Button {
136
+ margin: 0 1;
137
+ }
138
+
139
+ /* Tiny mode - hide full welcome, show minimal */
140
+ WelcomeScreen.tiny #titlebox {
141
+ display: none;
142
+ }
143
+
144
+ WelcomeScreen.tiny #options-container {
145
+ display: none;
146
+ }
147
+
148
+ WelcomeScreen.tiny #migration-warning {
149
+ display: none;
150
+ }
151
+
152
+ WelcomeScreen.tiny #tiny-welcome-container {
153
+ display: block;
154
+ }
88
155
  """
89
156
 
90
157
  BINDINGS = [
@@ -92,6 +159,24 @@ class WelcomeScreen(Screen[None]):
92
159
  ]
93
160
 
94
161
  def compose(self) -> ComposeResult:
162
+ # Tiny screen fallback
163
+ with Container(id="tiny-welcome-container"):
164
+ yield Static(
165
+ "Welcome to Shotgun",
166
+ id="tiny-welcome-message",
167
+ )
168
+ yield Static(
169
+ "[@click=screen.open_usage_guide]View setup instructions[/]",
170
+ id="tiny-welcome-link",
171
+ markup=True,
172
+ )
173
+ with Horizontal(id="tiny-welcome-buttons"):
174
+ yield Button(
175
+ "Shotgun Account", id="tiny-shotgun-button", variant="primary"
176
+ )
177
+ yield Button("BYOK", id="tiny-byok-button", variant="success")
178
+
179
+ # Full welcome screen
95
180
  with Vertical(id="titlebox"):
96
181
  yield Static("Welcome to Shotgun", id="welcome-title")
97
182
  yield Static(
@@ -99,6 +184,23 @@ class WelcomeScreen(Screen[None]):
99
184
  id="welcome-subtitle",
100
185
  )
101
186
 
187
+ # Show migration warning if migration failed
188
+ app = cast("ShotgunApp", self.app)
189
+ # Note: This is a synchronous call in compose, but config should already be loaded
190
+ if hasattr(app, "config_manager") and app.config_manager._config:
191
+ config = app.config_manager._config
192
+ if config.migration_failed:
193
+ with Vertical(id="migration-warning"):
194
+ yield Static(
195
+ "⚠️ Configuration Migration Failed",
196
+ id="migration-warning-title",
197
+ )
198
+ backup_msg = "Your previous configuration couldn't be migrated automatically."
199
+ if config.migration_backup_path:
200
+ backup_msg += f"\n\nYour old configuration (including API keys) has been backed up to:\n{config.migration_backup_path}"
201
+ backup_msg += "\n\nYou'll need to reconfigure Shotgun by choosing an option below."
202
+ yield Markdown(backup_msg)
203
+
102
204
  with Container(id="options-container"):
103
205
  with Horizontal(id="options"):
104
206
  # Left box - Shotgun Account
@@ -136,10 +238,29 @@ class WelcomeScreen(Screen[None]):
136
238
 
137
239
  def on_mount(self) -> None:
138
240
  """Focus the first button on mount."""
241
+ self._apply_layout_for_height(self.app.size.height)
139
242
  self.query_one("#shotgun-button", Button).focus()
140
243
  # Update BYOK button text asynchronously
141
244
  self.run_worker(self._update_byok_button_text(), exclusive=False)
142
245
 
246
+ @on(Resize)
247
+ def handle_resize(self, event: Resize) -> None:
248
+ """Adjust layout based on terminal height."""
249
+ self._apply_layout_for_height(event.size.height)
250
+
251
+ def _apply_layout_for_height(self, height: int) -> None:
252
+ """Apply appropriate layout based on terminal height."""
253
+ if height < TINY_HEIGHT_THRESHOLD:
254
+ self.add_class("tiny")
255
+ else:
256
+ self.remove_class("tiny")
257
+
258
+ def action_open_usage_guide(self) -> None:
259
+ """Open the usage guide in browser."""
260
+ webbrowser.open(
261
+ "https://github.com/shotgun-sh/shotgun?tab=readme-ov-file#-usage"
262
+ )
263
+
143
264
  async def _update_byok_button_text(self) -> None:
144
265
  """Update BYOK button text based on whether user has existing providers."""
145
266
  byok_button = self.query_one("#byok-button", Button)
@@ -148,11 +269,13 @@ class WelcomeScreen(Screen[None]):
148
269
  byok_button.label = "I'll stick with my BYOK setup"
149
270
 
150
271
  @on(Button.Pressed, "#shotgun-button")
272
+ @on(Button.Pressed, "#tiny-shotgun-button")
151
273
  def _on_shotgun_pressed(self) -> None:
152
274
  """Handle Shotgun Account button press."""
153
275
  self.run_worker(self._start_shotgun_auth(), exclusive=True)
154
276
 
155
277
  @on(Button.Pressed, "#byok-button")
278
+ @on(Button.Pressed, "#tiny-byok-button")
156
279
  def _on_byok_pressed(self) -> None:
157
280
  """Handle BYOK button press."""
158
281
  self.run_worker(self._start_byok_config(), exclusive=True)
@@ -10,8 +10,11 @@ from typing import TYPE_CHECKING
10
10
 
11
11
  import aiofiles.os
12
12
 
13
- from shotgun.agents.conversation_history import ConversationHistory, ConversationState
14
- from shotgun.agents.conversation_manager import ConversationManager
13
+ from shotgun.agents.conversation import (
14
+ ConversationHistory,
15
+ ConversationManager,
16
+ ConversationState,
17
+ )
15
18
  from shotgun.agents.models import AgentType
16
19
 
17
20
  if TYPE_CHECKING:
@@ -113,93 +113,24 @@ class ModeProgressChecker:
113
113
 
114
114
 
115
115
  class PlaceholderHints:
116
- """Manages dynamic placeholder hints for each mode based on progress."""
116
+ """Manages dynamic placeholder hints for the Router agent."""
117
117
 
118
- # Placeholder variations for each mode and state
118
+ # Placeholder variations for Router mode
119
119
  HINTS = {
120
- # Research mode
121
- AgentType.RESEARCH: {
120
+ AgentType.ROUTER: {
122
121
  False: [
123
- "Research a product or idea (SHIFT+TAB to cycle modes)",
124
- "What would you like to explore? Start your research journey here (SHIFT+TAB to switch modes)",
125
- "Dive into discovery mode - research anything that sparks curiosity (SHIFT+TAB for mode menu)",
126
- "Ready to investigate? Feed me your burning questions (SHIFT+TAB to explore other modes)",
127
- " 🔍 The research rabbit hole awaits! What shall we uncover? (SHIFT+TAB for mode carousel)",
122
+ "What would you like to work on? (SHIFT+TAB to toggle Planning/Drafting)",
123
+ "Ask me to research, plan, or implement anything (SHIFT+TAB toggles mode)",
124
+ "Describe your goal and I'll help break it down (SHIFT+TAB for mode toggle)",
125
+ "Ready to help with research, specs, plans, or tasks (SHIFT+TAB toggles mode)",
126
+ "Tell me what you need - I'll coordinate the work (SHIFT+TAB for Planning/Drafting)",
128
127
  ],
129
128
  True: [
130
- "Research complete! SHIFT+TAB to move to Specify mode",
131
- "Great research! Time to specify (SHIFT+TAB to Specify mode)",
132
- "Research done! Ready to create specifications (SHIFT+TAB to Specify)",
133
- "Findings gathered! Move to specifications (SHIFT+TAB for Specify mode)",
134
- " 🎯 Research complete! Advance to Specify mode (SHIFT+TAB)",
135
- ],
136
- },
137
- # Specify mode
138
- AgentType.SPECIFY: {
139
- False: [
140
- "Create detailed specifications and requirements (SHIFT+TAB to switch modes)",
141
- "Define your project specifications here (SHIFT+TAB to navigate modes)",
142
- "Time to get specific - write comprehensive specs (SHIFT+TAB for mode options)",
143
- "Specification station: Document requirements and designs (SHIFT+TAB to change modes)",
144
- " 📋 Spec-tacular time! Let's architect your ideas (SHIFT+TAB for mode magic)",
145
- ],
146
- True: [
147
- "Specifications complete! SHIFT+TAB to create a Plan",
148
- "Specs ready! Time to plan (SHIFT+TAB to Plan mode)",
149
- "Requirements defined! Move to planning (SHIFT+TAB to Plan)",
150
- "Specifications done! Create your roadmap (SHIFT+TAB for Plan mode)",
151
- " 🚀 Specs complete! Advance to Plan mode (SHIFT+TAB)",
152
- ],
153
- },
154
- # Tasks mode
155
- AgentType.TASKS: {
156
- False: [
157
- "Break down your project into actionable tasks (SHIFT+TAB for modes)",
158
- "Task creation time! Define your implementation steps (SHIFT+TAB to switch)",
159
- "Ready to get tactical? Create your task list (SHIFT+TAB for mode options)",
160
- "Task command center: Organize your work items (SHIFT+TAB to navigate)",
161
- " ✅ Task mode activated! Break it down into bite-sized pieces (SHIFT+TAB)",
162
- ],
163
- True: [
164
- "Tasks defined! Ready to export or cycle back (SHIFT+TAB)",
165
- "Task list complete! Export your work (SHIFT+TAB to Export)",
166
- "All tasks created! Time to export (SHIFT+TAB for Export mode)",
167
- "Implementation plan ready! Export everything (SHIFT+TAB to Export)",
168
- " 🎊 Tasks complete! Export your masterpiece (SHIFT+TAB)",
169
- ],
170
- },
171
- # Export mode
172
- AgentType.EXPORT: {
173
- False: [
174
- "Export your complete project documentation (SHIFT+TAB for modes)",
175
- "Ready to package everything? Export time! (SHIFT+TAB to switch)",
176
- "Export station: Generate deliverables (SHIFT+TAB for mode menu)",
177
- "Time to share your work! Export documents (SHIFT+TAB to navigate)",
178
- " 📦 Export mode! Package and share your creation (SHIFT+TAB)",
179
- ],
180
- True: [
181
- "Exported! Start new research or continue refining (SHIFT+TAB)",
182
- "Export complete! New cycle begins (SHIFT+TAB to Research)",
183
- "All exported! Ready for another round (SHIFT+TAB for Research)",
184
- "Documents exported! Start fresh (SHIFT+TAB to Research mode)",
185
- " 🎉 Export complete! Begin a new adventure (SHIFT+TAB)",
186
- ],
187
- },
188
- # Plan mode
189
- AgentType.PLAN: {
190
- False: [
191
- "Create a strategic plan for your project (SHIFT+TAB for modes)",
192
- "Planning phase: Map out your roadmap (SHIFT+TAB to switch)",
193
- "Time to strategize! Create your project plan (SHIFT+TAB for options)",
194
- "Plan your approach and milestones (SHIFT+TAB to navigate)",
195
- " 🗺️ Plan mode! Chart your course to success (SHIFT+TAB)",
196
- ],
197
- True: [
198
- "Plan complete! Move to Tasks mode (SHIFT+TAB)",
199
- "Strategy ready! Time for tasks (SHIFT+TAB to Tasks mode)",
200
- "Roadmap done! Create task list (SHIFT+TAB for Tasks)",
201
- "Planning complete! Break into tasks (SHIFT+TAB to Tasks)",
202
- " ⚡ Plan ready! Advance to Tasks mode (SHIFT+TAB)",
129
+ "Continue working or start something new (SHIFT+TAB toggles mode)",
130
+ "What's next? (SHIFT+TAB to toggle Planning/Drafting)",
131
+ "Ready for the next task (SHIFT+TAB toggles Planning/Drafting)",
132
+ "Let's keep going! (SHIFT+TAB to toggle mode)",
133
+ "What else can I help with? (SHIFT+TAB for mode toggle)",
203
134
  ],
204
135
  },
205
136
  }
@@ -224,19 +155,22 @@ class PlaceholderHints:
224
155
  Returns:
225
156
  A contextual hint string for the placeholder.
226
157
  """
158
+ # Always use Router hints since Router is the only user-facing agent
159
+ mode_key = AgentType.ROUTER
160
+
227
161
  # Default hint if mode not configured
228
- if current_mode not in self.HINTS:
229
- return f"Enter your {current_mode.value} mode prompt (SHIFT+TAB to switch modes)"
162
+ if mode_key not in self.HINTS:
163
+ return "Enter your prompt (SHIFT+TAB to toggle Planning/Drafting mode)"
230
164
 
231
165
  # For placeholder text, we default to "no content" state (initial hints)
232
166
  # This avoids async file system checks in the UI rendering path
233
167
  has_content = False
234
168
 
235
169
  # Get hint variations for this mode and state
236
- hints_list = self.HINTS[current_mode][has_content]
170
+ hints_list = self.HINTS[mode_key][has_content]
237
171
 
238
172
  # Cache key for this mode and state
239
- cache_key = (current_mode, has_content)
173
+ cache_key = (mode_key, has_content)
240
174
 
241
175
  # Force refresh or first time
242
176
  if force_refresh or cache_key not in self._cached_hints:
@@ -1,5 +1,6 @@
1
1
  """Widget utilities and coordinators for TUI."""
2
2
 
3
+ from shotgun.tui.widgets.plan_panel import PlanPanelWidget
3
4
  from shotgun.tui.widgets.widget_coordinator import WidgetCoordinator
4
5
 
5
- __all__ = ["WidgetCoordinator"]
6
+ __all__ = ["PlanPanelWidget", "WidgetCoordinator"]
@@ -0,0 +1,152 @@
1
+ """Plan approval widget for Planning mode.
2
+
3
+ This widget displays when a multi-step plan is created in Planning mode,
4
+ allowing the user to approve or reject the plan before execution begins.
5
+ """
6
+
7
+ from textual import events, on
8
+ from textual.app import ComposeResult
9
+ from textual.containers import Horizontal, VerticalScroll
10
+ from textual.css.query import NoMatches
11
+ from textual.widget import Widget
12
+ from textual.widgets import Button, Static
13
+
14
+ from shotgun.agents.router.models import ExecutionPlan
15
+ from shotgun.tui.screens.chat_screen.messages import (
16
+ PlanApproved,
17
+ PlanRejected,
18
+ )
19
+
20
+
21
+ class PlanApprovalWidget(Widget):
22
+ """Widget for plan approval in Planning mode.
23
+
24
+ Displays the execution plan summary with goal and steps,
25
+ and provides action buttons for the user to approve or reject.
26
+
27
+ Attributes:
28
+ plan: The execution plan that needs user approval.
29
+ """
30
+
31
+ DEFAULT_CSS = """
32
+ PlanApprovalWidget {
33
+ background: $secondary-background-darken-1;
34
+ height: auto;
35
+ max-height: 20;
36
+ margin: 0 1;
37
+ padding: 1;
38
+ }
39
+
40
+ PlanApprovalWidget .approval-header {
41
+ height: auto;
42
+ }
43
+
44
+ PlanApprovalWidget .plan-content {
45
+ height: auto;
46
+ max-height: 12;
47
+ margin: 1 0;
48
+ }
49
+
50
+ PlanApprovalWidget .plan-goal {
51
+ color: $text;
52
+ margin-bottom: 1;
53
+ }
54
+
55
+ PlanApprovalWidget .plan-steps-label {
56
+ color: $text-muted;
57
+ }
58
+
59
+ PlanApprovalWidget .step-item {
60
+ color: $text;
61
+ margin-left: 2;
62
+ }
63
+
64
+ PlanApprovalWidget .approval-buttons {
65
+ height: auto;
66
+ width: 100%;
67
+ dock: bottom;
68
+ }
69
+
70
+ PlanApprovalWidget Button {
71
+ margin-right: 1;
72
+ min-width: 18;
73
+ }
74
+
75
+ PlanApprovalWidget #btn-approve {
76
+ background: $success;
77
+ }
78
+
79
+ PlanApprovalWidget #btn-reject {
80
+ background: $error;
81
+ }
82
+ """
83
+
84
+ def __init__(self, plan: ExecutionPlan) -> None:
85
+ """Initialize the approval widget.
86
+
87
+ Args:
88
+ plan: The execution plan that needs user approval.
89
+ """
90
+ super().__init__()
91
+ self.plan = plan
92
+
93
+ def compose(self) -> ComposeResult:
94
+ """Compose the approval widget layout."""
95
+ # Header with step count
96
+ yield Static(
97
+ f"[bold]📋 Plan created with {len(self.plan.steps)} steps[/]",
98
+ classes="approval-header",
99
+ )
100
+
101
+ # Scrollable content area for goal and steps
102
+ with VerticalScroll(classes="plan-content"):
103
+ # Goal
104
+ yield Static(
105
+ f"[dim]Goal:[/] {self.plan.goal}",
106
+ classes="plan-goal",
107
+ )
108
+
109
+ # Steps list
110
+ yield Static("[dim]Steps:[/]", classes="plan-steps-label")
111
+ for i, step in enumerate(self.plan.steps, 1):
112
+ yield Static(
113
+ f"{i}. {step.title}",
114
+ classes="step-item",
115
+ )
116
+
117
+ # Action buttons (always visible at bottom)
118
+ with Horizontal(classes="approval-buttons"):
119
+ yield Button("✓ Go Ahead", id="btn-approve")
120
+ yield Button("✗ No, Let Me Clarify", id="btn-reject")
121
+
122
+ def on_mount(self) -> None:
123
+ """Auto-focus the Go Ahead button on mount."""
124
+ try:
125
+ approve_btn = self.query_one("#btn-approve", Button)
126
+ approve_btn.focus()
127
+ except NoMatches:
128
+ pass
129
+
130
+ @on(Button.Pressed, "#btn-approve")
131
+ def handle_approve(self) -> None:
132
+ """Handle Go Ahead button press."""
133
+ self.post_message(PlanApproved())
134
+
135
+ @on(Button.Pressed, "#btn-reject")
136
+ def handle_reject(self) -> None:
137
+ """Handle No, Let Me Clarify button press."""
138
+ self.post_message(PlanRejected())
139
+
140
+ def on_key(self, event: events.Key) -> None:
141
+ """Handle keyboard shortcuts for approval actions.
142
+
143
+ Shortcuts:
144
+ Enter/Y: Approve plan (Go Ahead)
145
+ Escape/N: Reject plan (No, Let Me Clarify)
146
+ """
147
+ if event.key in ("enter", "y", "Y"):
148
+ self.post_message(PlanApproved())
149
+ event.stop()
150
+ elif event.key in ("escape", "n", "N"):
151
+ self.post_message(PlanRejected())
152
+ event.stop()