shotgun-sh 0.2.11.dev1__py3-none-any.whl → 0.2.11.dev5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of shotgun-sh might be problematic. Click here for more details.

Files changed (70) hide show
  1. shotgun/agents/agent_manager.py +150 -27
  2. shotgun/agents/common.py +14 -8
  3. shotgun/agents/config/manager.py +64 -33
  4. shotgun/agents/config/models.py +25 -1
  5. shotgun/agents/config/provider.py +2 -2
  6. shotgun/agents/context_analyzer/analyzer.py +2 -24
  7. shotgun/agents/conversation_manager.py +35 -19
  8. shotgun/agents/export.py +2 -2
  9. shotgun/agents/history/token_counting/anthropic.py +17 -1
  10. shotgun/agents/history/token_counting/base.py +14 -3
  11. shotgun/agents/history/token_counting/openai.py +8 -0
  12. shotgun/agents/history/token_counting/sentencepiece_counter.py +8 -0
  13. shotgun/agents/history/token_counting/tokenizer_cache.py +3 -1
  14. shotgun/agents/history/token_counting/utils.py +0 -3
  15. shotgun/agents/plan.py +2 -2
  16. shotgun/agents/research.py +3 -3
  17. shotgun/agents/specify.py +2 -2
  18. shotgun/agents/tasks.py +2 -2
  19. shotgun/agents/tools/codebase/file_read.py +5 -2
  20. shotgun/agents/tools/file_management.py +11 -7
  21. shotgun/agents/tools/web_search/__init__.py +8 -8
  22. shotgun/agents/tools/web_search/anthropic.py +2 -2
  23. shotgun/agents/tools/web_search/gemini.py +1 -1
  24. shotgun/agents/tools/web_search/openai.py +1 -1
  25. shotgun/agents/tools/web_search/utils.py +2 -2
  26. shotgun/agents/usage_manager.py +16 -11
  27. shotgun/cli/clear.py +2 -1
  28. shotgun/cli/compact.py +3 -3
  29. shotgun/cli/config.py +8 -5
  30. shotgun/cli/context.py +2 -2
  31. shotgun/cli/export.py +1 -1
  32. shotgun/cli/feedback.py +4 -2
  33. shotgun/cli/plan.py +1 -1
  34. shotgun/cli/research.py +1 -1
  35. shotgun/cli/specify.py +1 -1
  36. shotgun/cli/tasks.py +1 -1
  37. shotgun/codebase/core/change_detector.py +5 -3
  38. shotgun/codebase/core/code_retrieval.py +4 -2
  39. shotgun/codebase/core/ingestor.py +10 -8
  40. shotgun/codebase/core/manager.py +3 -3
  41. shotgun/codebase/core/nl_query.py +1 -1
  42. shotgun/logging_config.py +10 -17
  43. shotgun/main.py +3 -1
  44. shotgun/posthog_telemetry.py +14 -4
  45. shotgun/sentry_telemetry.py +3 -1
  46. shotgun/telemetry.py +3 -1
  47. shotgun/tui/app.py +71 -65
  48. shotgun/tui/components/context_indicator.py +43 -0
  49. shotgun/tui/containers.py +15 -17
  50. shotgun/tui/dependencies.py +2 -2
  51. shotgun/tui/screens/chat/chat_screen.py +110 -18
  52. shotgun/tui/screens/chat/help_text.py +16 -15
  53. shotgun/tui/screens/chat_screen/command_providers.py +10 -0
  54. shotgun/tui/screens/feedback.py +4 -4
  55. shotgun/tui/screens/github_issue.py +102 -0
  56. shotgun/tui/screens/model_picker.py +21 -20
  57. shotgun/tui/screens/onboarding.py +431 -0
  58. shotgun/tui/screens/provider_config.py +50 -27
  59. shotgun/tui/screens/shotgun_auth.py +2 -2
  60. shotgun/tui/screens/welcome.py +14 -11
  61. shotgun/tui/services/conversation_service.py +16 -14
  62. shotgun/tui/utils/mode_progress.py +14 -7
  63. shotgun/tui/widgets/widget_coordinator.py +15 -0
  64. shotgun/utils/file_system_utils.py +19 -0
  65. shotgun/utils/marketing.py +110 -0
  66. {shotgun_sh-0.2.11.dev1.dist-info → shotgun_sh-0.2.11.dev5.dist-info}/METADATA +2 -1
  67. {shotgun_sh-0.2.11.dev1.dist-info → shotgun_sh-0.2.11.dev5.dist-info}/RECORD +70 -67
  68. {shotgun_sh-0.2.11.dev1.dist-info → shotgun_sh-0.2.11.dev5.dist-info}/WHEEL +0 -0
  69. {shotgun_sh-0.2.11.dev1.dist-info → shotgun_sh-0.2.11.dev5.dist-info}/entry_points.txt +0 -0
  70. {shotgun_sh-0.2.11.dev1.dist-info → shotgun_sh-0.2.11.dev5.dist-info}/licenses/LICENSE +0 -0
@@ -125,8 +125,8 @@ class FeedbackScreen(Screen[Feedback | None]):
125
125
  self.set_focus(self.query_one("#feedback-description", TextArea))
126
126
 
127
127
  @on(Button.Pressed, "#submit")
128
- def _on_submit_pressed(self) -> None:
129
- self._submit_feedback()
128
+ async def _on_submit_pressed(self) -> None:
129
+ await self._submit_feedback()
130
130
 
131
131
  @on(Button.Pressed, "#cancel")
132
132
  def _on_cancel_pressed(self) -> None:
@@ -171,7 +171,7 @@ class FeedbackScreen(Screen[Feedback | None]):
171
171
  }
172
172
  return placeholders.get(kind, "Enter your feedback...")
173
173
 
174
- def _submit_feedback(self) -> None:
174
+ async def _submit_feedback(self) -> None:
175
175
  text_area = self.query_one("#feedback-description", TextArea)
176
176
  description = text_area.text.strip()
177
177
 
@@ -182,7 +182,7 @@ class FeedbackScreen(Screen[Feedback | None]):
182
182
  return
183
183
 
184
184
  app = cast("ShotgunApp", self.app)
185
- shotgun_instance_id = app.config_manager.get_shotgun_instance_id()
185
+ shotgun_instance_id = await app.config_manager.get_shotgun_instance_id()
186
186
 
187
187
  feedback = Feedback(
188
188
  kind=self.selected_kind,
@@ -0,0 +1,102 @@
1
+ """Screen for guiding users to create GitHub issues."""
2
+
3
+ import webbrowser
4
+
5
+ from textual import on
6
+ from textual.app import ComposeResult
7
+ from textual.containers import Container, Vertical
8
+ from textual.screen import ModalScreen
9
+ from textual.widgets import Button, Markdown, Static
10
+
11
+
12
+ class GitHubIssueScreen(ModalScreen[None]):
13
+ """Guide users to create issues on GitHub."""
14
+
15
+ CSS = """
16
+ GitHubIssueScreen {
17
+ align: center middle;
18
+ }
19
+
20
+ #issue-container {
21
+ width: 70;
22
+ max-width: 100;
23
+ height: auto;
24
+ border: thick $primary;
25
+ background: $surface;
26
+ padding: 2;
27
+ }
28
+
29
+ #issue-header {
30
+ text-style: bold;
31
+ color: $text-accent;
32
+ padding-bottom: 1;
33
+ text-align: center;
34
+ }
35
+
36
+ #issue-content {
37
+ padding: 1 0;
38
+ }
39
+
40
+ #issue-buttons {
41
+ height: auto;
42
+ padding: 2 0 0 0;
43
+ align: center middle;
44
+ }
45
+
46
+ #issue-buttons Button {
47
+ margin: 1 1;
48
+ min-width: 20;
49
+ }
50
+ """
51
+
52
+ BINDINGS = [
53
+ ("escape", "dismiss", "Close"),
54
+ ]
55
+
56
+ def compose(self) -> ComposeResult:
57
+ """Compose the GitHub issue screen."""
58
+ with Container(id="issue-container"):
59
+ yield Static("Create a GitHub Issue", id="issue-header")
60
+ with Vertical(id="issue-content"):
61
+ yield Markdown(
62
+ """
63
+ ## Report Bugs or Request Features
64
+
65
+ We track all bugs, feature requests, and improvements on GitHub Issues.
66
+
67
+ ### How to Create an Issue:
68
+
69
+ 1. Click the button below to open our GitHub Issues page
70
+ 2. Click **"New Issue"**
71
+ 3. Choose a template:
72
+ - **Bug Report** - Report a bug or unexpected behavior
73
+ - **Feature Request** - Suggest new functionality
74
+ - **Documentation** - Report docs issues or improvements
75
+ 4. Fill in the details and submit
76
+
77
+ We review all issues and will respond as soon as possible!
78
+
79
+ ### Before Creating an Issue:
80
+
81
+ - Search existing issues to avoid duplicates
82
+ - Include steps to reproduce for bugs
83
+ - Be specific about what you'd like for feature requests
84
+ """,
85
+ id="issue-markdown",
86
+ )
87
+ with Vertical(id="issue-buttons"):
88
+ yield Button(
89
+ "🐙 Open GitHub Issues", id="github-button", variant="primary"
90
+ )
91
+ yield Button("Close", id="close-button")
92
+
93
+ @on(Button.Pressed, "#github-button")
94
+ def handle_github(self) -> None:
95
+ """Open GitHub issues page in browser."""
96
+ webbrowser.open("https://github.com/shotgun-sh/shotgun/issues")
97
+ self.notify("Opening GitHub Issues in your browser...")
98
+
99
+ @on(Button.Pressed, "#close-button")
100
+ def handle_close(self) -> None:
101
+ """Handle close button press."""
102
+ self.dismiss()
@@ -98,7 +98,7 @@ class ModelPickerScreen(Screen[ModelConfigUpdated | None]):
98
98
  yield Button("Select \\[ENTER]", variant="primary", id="select")
99
99
  yield Button("Done \\[ESC]", id="done")
100
100
 
101
- def _rebuild_model_list(self) -> None:
101
+ async def _rebuild_model_list(self) -> None:
102
102
  """Rebuild the model list from current config.
103
103
 
104
104
  This method is called both on first show and when screen is resumed
@@ -108,7 +108,7 @@ class ModelPickerScreen(Screen[ModelConfigUpdated | None]):
108
108
 
109
109
  # Load current config with force_reload to get latest API keys
110
110
  config_manager = self.config_manager
111
- config = config_manager.load(force_reload=True)
111
+ config = await config_manager.load(force_reload=True)
112
112
 
113
113
  # Log provider key status
114
114
  logger.debug(
@@ -133,7 +133,7 @@ class ModelPickerScreen(Screen[ModelConfigUpdated | None]):
133
133
  logger.debug("Removed %d existing model items from list", old_count)
134
134
 
135
135
  # Add new items (labels already have correct text including current indicator)
136
- new_items = self._build_model_items(config)
136
+ new_items = await self._build_model_items(config)
137
137
  for item in new_items:
138
138
  list_view.append(item)
139
139
  logger.debug("Added %d available model items to list", len(new_items))
@@ -153,7 +153,7 @@ class ModelPickerScreen(Screen[ModelConfigUpdated | None]):
153
153
  def on_show(self) -> None:
154
154
  """Rebuild model list when screen is first shown."""
155
155
  logger.debug("ModelPickerScreen.on_show() called")
156
- self._rebuild_model_list()
156
+ self.run_worker(self._rebuild_model_list(), exclusive=False)
157
157
 
158
158
  def on_screenresume(self) -> None:
159
159
  """Rebuild model list when screen is resumed (subsequent visits).
@@ -162,7 +162,7 @@ class ModelPickerScreen(Screen[ModelConfigUpdated | None]):
162
162
  ensuring the model list reflects any config changes made while away.
163
163
  """
164
164
  logger.debug("ModelPickerScreen.on_screenresume() called")
165
- self._rebuild_model_list()
165
+ self.run_worker(self._rebuild_model_list(), exclusive=False)
166
166
 
167
167
  def action_done(self) -> None:
168
168
  self.dismiss()
@@ -193,14 +193,14 @@ class ModelPickerScreen(Screen[ModelConfigUpdated | None]):
193
193
  app = cast("ShotgunApp", self.app)
194
194
  return app.config_manager
195
195
 
196
- def refresh_model_labels(self) -> None:
196
+ async def refresh_model_labels(self) -> None:
197
197
  """Update the list view entries to reflect current selection.
198
198
 
199
199
  Note: This method only updates labels for currently displayed models.
200
200
  To rebuild the entire list after provider changes, on_show() should be used.
201
201
  """
202
202
  # Load config once with force_reload
203
- config = self.config_manager.load(force_reload=True)
203
+ config = await self.config_manager.load(force_reload=True)
204
204
  current_model = config.selected_model or get_default_model_for_provider(config)
205
205
 
206
206
  # Update labels for available models only
@@ -215,9 +215,11 @@ class ModelPickerScreen(Screen[ModelConfigUpdated | None]):
215
215
  self._model_label(model_name, is_current=model_name == current_model)
216
216
  )
217
217
 
218
- def _build_model_items(self, config: ShotgunConfig | None = None) -> list[ListItem]:
218
+ async def _build_model_items(
219
+ self, config: ShotgunConfig | None = None
220
+ ) -> list[ListItem]:
219
221
  if config is None:
220
- config = self.config_manager.load(force_reload=True)
222
+ config = await self.config_manager.load(force_reload=True)
221
223
 
222
224
  items: list[ListItem] = []
223
225
  current_model = self.selected_model
@@ -246,9 +248,7 @@ class ModelPickerScreen(Screen[ModelConfigUpdated | None]):
246
248
  return model_name
247
249
  return None
248
250
 
249
- def _is_model_available(
250
- self, model_name: ModelName, config: ShotgunConfig | None = None
251
- ) -> bool:
251
+ def _is_model_available(self, model_name: ModelName, config: ShotgunConfig) -> bool:
252
252
  """Check if a model is available based on provider key configuration.
253
253
 
254
254
  A model is available if:
@@ -257,14 +257,11 @@ class ModelPickerScreen(Screen[ModelConfigUpdated | None]):
257
257
 
258
258
  Args:
259
259
  model_name: The model to check availability for
260
- config: Optional pre-loaded config to avoid multiple reloads
260
+ config: Pre-loaded config (must be provided)
261
261
 
262
262
  Returns:
263
263
  True if the model can be used, False otherwise
264
264
  """
265
- if config is None:
266
- config = self.config_manager.load(force_reload=True)
267
-
268
265
  # If Shotgun Account is configured, all models are available
269
266
  if self.config_manager._provider_has_api_key(config.shotgun):
270
267
  logger.debug("Model %s available (Shotgun Account configured)", model_name)
@@ -325,17 +322,21 @@ class ModelPickerScreen(Screen[ModelConfigUpdated | None]):
325
322
 
326
323
  def _select_model(self) -> None:
327
324
  """Save the selected model."""
325
+ self.run_worker(self._do_select_model(), exclusive=True)
326
+
327
+ async def _do_select_model(self) -> None:
328
+ """Async implementation of model selection."""
328
329
  try:
329
330
  # Get old model before updating
330
- config = self.config_manager.load()
331
+ config = await self.config_manager.load()
331
332
  old_model = config.selected_model
332
333
 
333
334
  # Update the selected model in config
334
- self.config_manager.update_selected_model(self.selected_model)
335
- self.refresh_model_labels()
335
+ await self.config_manager.update_selected_model(self.selected_model)
336
+ await self.refresh_model_labels()
336
337
 
337
338
  # Get the full model config with provider information
338
- model_config = get_provider_model(self.selected_model)
339
+ model_config = await get_provider_model(self.selected_model)
339
340
 
340
341
  # Dismiss the screen and return the model config update to the caller
341
342
  self.dismiss(
@@ -0,0 +1,431 @@
1
+ """Onboarding popup modal for first-time users."""
2
+
3
+ import webbrowser
4
+
5
+ from textual import on
6
+ from textual.app import ComposeResult
7
+ from textual.containers import Container, Horizontal, VerticalScroll
8
+ from textual.screen import ModalScreen
9
+ from textual.widgets import Button, Markdown, Static
10
+
11
+
12
+ class OnboardingModal(ModalScreen[None]):
13
+ """Multi-page onboarding modal for new users.
14
+
15
+ This modal presents helpful resources and tips for using Shotgun across
16
+ multiple pages. Users can navigate between pages using Next/Back buttons.
17
+ """
18
+
19
+ CSS = """
20
+ OnboardingModal {
21
+ align: center middle;
22
+ }
23
+
24
+ #onboarding-container {
25
+ width: 95;
26
+ max-width: 100;
27
+ height: auto;
28
+ max-height: 90%;
29
+ border: thick $primary;
30
+ background: $surface;
31
+ padding: 2;
32
+ }
33
+
34
+ #progress-sidebar {
35
+ width: 26;
36
+ dock: left;
37
+ border-right: solid $primary;
38
+ padding: 1;
39
+ height: 100%;
40
+ }
41
+
42
+ #main-content {
43
+ width: 1fr;
44
+ height: auto;
45
+ }
46
+
47
+ #progress-header {
48
+ text-style: bold;
49
+ padding-bottom: 1;
50
+ color: $text-accent;
51
+ }
52
+
53
+ .progress-item {
54
+ padding: 1 0;
55
+ }
56
+
57
+ .progress-item-current {
58
+ color: $accent;
59
+ text-style: bold;
60
+ }
61
+
62
+ .progress-item-visited {
63
+ color: $success;
64
+ }
65
+
66
+ .progress-item-unvisited {
67
+ color: $text-muted;
68
+ }
69
+
70
+ #onboarding-header {
71
+ text-style: bold;
72
+ color: $text-accent;
73
+ padding-bottom: 1;
74
+ text-align: center;
75
+ }
76
+
77
+ #onboarding-content {
78
+ height: 1fr;
79
+ padding: 1 0;
80
+ }
81
+
82
+ #page-indicator {
83
+ text-align: center;
84
+ color: $text-muted;
85
+ padding: 1 0;
86
+ }
87
+
88
+ #buttons-container {
89
+ height: auto;
90
+ padding: 1 0 0 0;
91
+ }
92
+
93
+ #navigation-buttons {
94
+ width: 100%;
95
+ height: auto;
96
+ align: center middle;
97
+ }
98
+
99
+ .nav-button {
100
+ margin: 0 1;
101
+ min-width: 12;
102
+ }
103
+
104
+ #resource-sections {
105
+ padding: 1 0;
106
+ height: auto;
107
+ }
108
+
109
+ #resource-sections Button {
110
+ width: 100%;
111
+ margin: 0 0 2 0;
112
+ }
113
+
114
+ #video-section {
115
+ padding: 0;
116
+ margin: 0 0 1 0;
117
+ }
118
+
119
+ #docs-section {
120
+ padding: 0;
121
+ margin: 2 0 1 0;
122
+ }
123
+ """
124
+
125
+ BINDINGS = [
126
+ ("escape", "dismiss", "Close"),
127
+ ("ctrl+c", "app.quit", "Quit"),
128
+ ]
129
+
130
+ def __init__(self) -> None:
131
+ """Initialize the onboarding modal."""
132
+ super().__init__()
133
+ self.current_page = 0
134
+ self.total_pages = 4
135
+ self.page_titles = [
136
+ "Getting Started",
137
+ "Discovering the 5 Modes",
138
+ "Prompting Better",
139
+ "Context Management!",
140
+ ]
141
+ # Track which pages have been visited (in memory only)
142
+ self.visited_pages: set[int] = {0} # Start on page 0, so it's visited
143
+
144
+ def compose(self) -> ComposeResult:
145
+ """Compose the onboarding modal."""
146
+ with Container(id="onboarding-container"):
147
+ # Left sidebar for progress tracking
148
+ with Container(id="progress-sidebar"):
149
+ yield Static("Progress", id="progress-header")
150
+ for i in range(self.total_pages):
151
+ yield Static(
152
+ f"{i + 1}. {self.page_titles[i]}",
153
+ id=f"progress-item-{i}",
154
+ classes="progress-item",
155
+ )
156
+
157
+ # Main content area
158
+ with Container(id="main-content"):
159
+ yield Static("Welcome to Shotgun!", id="onboarding-header")
160
+ with VerticalScroll(id="onboarding-content"):
161
+ yield Markdown(id="page-content")
162
+ # Resource sections (only shown on page 1)
163
+ with Container(id="resource-sections"):
164
+ yield Markdown(
165
+ "### 🎥 Video Demo\nWatch our demo video to see Shotgun in action",
166
+ id="video-section",
167
+ )
168
+ yield Button(
169
+ "▶️ Watch Demo Video",
170
+ id="youtube-button",
171
+ variant="success",
172
+ )
173
+ yield Markdown(
174
+ "### 📖 Documentation\nRead the comprehensive usage guide for detailed instructions",
175
+ id="docs-section",
176
+ )
177
+ yield Button(
178
+ "📚 Read Usage Guide", id="usage-button", variant="primary"
179
+ )
180
+ yield Static(id="page-indicator")
181
+ with Container(id="buttons-container"):
182
+ with Horizontal(id="navigation-buttons"):
183
+ yield Button("Back", id="back-button", classes="nav-button")
184
+ yield Button(
185
+ "Next",
186
+ id="next-button",
187
+ classes="nav-button",
188
+ variant="primary",
189
+ )
190
+ yield Button("Close", id="close-button", classes="nav-button")
191
+
192
+ def on_mount(self) -> None:
193
+ """Set up the modal after mounting."""
194
+ self.update_page()
195
+
196
+ def update_page(self) -> None:
197
+ """Update the displayed page content and navigation buttons."""
198
+ # Mark current page as visited
199
+ self.visited_pages.add(self.current_page)
200
+
201
+ # Update page content
202
+ content_widget = self.query_one("#page-content", Markdown)
203
+ content_widget.update(self.get_page_content())
204
+
205
+ # Update page indicator
206
+ page_indicator = self.query_one("#page-indicator", Static)
207
+ page_indicator.update(f"Page {self.current_page + 1} of {self.total_pages}")
208
+
209
+ # Update progress sidebar
210
+ for i in range(self.total_pages):
211
+ progress_item = self.query_one(f"#progress-item-{i}", Static)
212
+ # Remove all progress classes first
213
+ progress_item.remove_class(
214
+ "progress-item-current",
215
+ "progress-item-visited",
216
+ "progress-item-unvisited",
217
+ )
218
+ # Add appropriate class
219
+ if i == self.current_page:
220
+ progress_item.add_class("progress-item-current")
221
+ progress_item.update(f"▶ {i + 1}. {self.page_titles[i]}")
222
+ elif i in self.visited_pages:
223
+ progress_item.add_class("progress-item-visited")
224
+ progress_item.update(f"✓ {i + 1}. {self.page_titles[i]}")
225
+ else:
226
+ progress_item.add_class("progress-item-unvisited")
227
+ progress_item.update(f" {i + 1}. {self.page_titles[i]}")
228
+
229
+ # Show/hide resource sections (only on page 1)
230
+ resource_sections = self.query_one("#resource-sections", Container)
231
+ resource_sections.display = self.current_page == 0
232
+
233
+ # Update button visibility and states
234
+ back_button = self.query_one("#back-button", Button)
235
+ next_button = self.query_one("#next-button", Button)
236
+
237
+ # Update back button label and state
238
+ if self.current_page == 0:
239
+ back_button.disabled = True
240
+ back_button.label = "Back"
241
+ else:
242
+ back_button.disabled = False
243
+ prev_title = self.page_titles[self.current_page - 1]
244
+ back_button.label = f"← {prev_title}"
245
+
246
+ # Update next button label
247
+ if self.current_page == self.total_pages - 1:
248
+ next_button.label = "Finish"
249
+ else:
250
+ next_title = self.page_titles[self.current_page + 1]
251
+ next_button.label = f"{next_title} (Next →)"
252
+
253
+ # Focus the appropriate button
254
+ if self.current_page == 0:
255
+ next_button.focus()
256
+ else:
257
+ next_button.focus()
258
+
259
+ # Scroll content to top
260
+ self.query_one("#onboarding-content", VerticalScroll).scroll_home(animate=False)
261
+
262
+ def get_page_content(self) -> str:
263
+ """Get the content for the current page."""
264
+ if self.current_page == 0:
265
+ return self._page_1_resources()
266
+ elif self.current_page == 1:
267
+ return self._page_2_modes()
268
+ elif self.current_page == 2:
269
+ return self._page_3_prompts()
270
+ else:
271
+ return self._page_4_context_management()
272
+
273
+ def _page_1_resources(self) -> str:
274
+ """Page 1: Helpful resources."""
275
+ return """
276
+ ## Getting Started Resources
277
+
278
+ Here are some helpful resources to get you up to speed with Shotgun:
279
+ """
280
+
281
+ def _page_2_modes(self) -> str:
282
+ """Page 2: Explanation of the 5 modes."""
283
+ return """
284
+ ## Understanding Shotgun's 5 Modes
285
+
286
+ Shotgun has 5 specialized modes, each designed for specific tasks. Each mode writes to its own dedicated file in `.shotgun/`:
287
+
288
+ ### 🔬 Research Mode
289
+ Research topics with web search and synthesize findings. Perfect for gathering information and exploring new concepts.
290
+
291
+ **Writes to:** `.shotgun/research.md`
292
+
293
+ ### 📝 Specify Mode
294
+ Create detailed specifications and requirements documents. Great for planning features and documenting requirements.
295
+
296
+ **Writes to:** `.shotgun/specification.md`
297
+
298
+ ### 📋 Plan Mode
299
+ Create comprehensive, actionable plans with milestones. Ideal for breaking down large projects into manageable steps.
300
+
301
+ **Writes to:** `.shotgun/plan.md`
302
+
303
+ ### ✅ Tasks Mode
304
+ Generate specific, actionable tasks from research and plans. Best for getting concrete next steps and action items.
305
+
306
+ **Writes to:** `.shotgun/tasks.md`
307
+
308
+ ### 📤 Export Mode
309
+ Export artifacts and findings to various formats. Creates documentation like Claude.md (AI instructions), Agent.md (agent specs), PRDs, and other deliverables. Can write to any file in `.shotgun/` except the mode-specific files above.
310
+
311
+ **Writes to:** `.shotgun/Claude.md`, `.shotgun/Agent.md`, `.shotgun/PRD.md`, etc.
312
+
313
+ ---
314
+
315
+ **Tip:** You can switch between modes using `Shift+Tab` or `Ctrl+P` to open the command palette!
316
+ """
317
+
318
+ def _page_3_prompts(self) -> str:
319
+ """Page 3: Tips for better prompts."""
320
+ return """
321
+ ## Writing Better Prompts
322
+
323
+ Here are some tips to get the best results from Shotgun:
324
+
325
+ ### 1. Ask for Research First
326
+ Before jumping into a task, ask Shotgun to research the codebase or topic:
327
+
328
+ > "Can you research how authentication works in this codebase?"
329
+
330
+ ### 2. Request Clarifying Questions
331
+ Let Shotgun ask you questions to better understand your needs:
332
+
333
+ > "I want to add user profiles. Please ask me clarifying questions before starting."
334
+
335
+ ### 3. Be Specific About Context
336
+ Provide relevant context about what you're trying to accomplish:
337
+
338
+ > "I'm working on the payment flow. I need to add support for refunds."
339
+
340
+ ### 4. Use the Right Mode
341
+ Switch to the appropriate mode for your task:
342
+ - Use **Research** for exploration
343
+ - Use **Specify** for requirements
344
+ - Use **Plan** for implementation strategy
345
+ - Use **Tasks** for actionable next steps
346
+
347
+ ---
348
+
349
+ **Remember:** Shotgun works best when you give it context and let it ask questions!
350
+ """
351
+
352
+ def _page_4_context_management(self) -> str:
353
+ """Page 4: Context management and conversation controls."""
354
+ return """
355
+ ## Managing Conversation Context
356
+
357
+ As conversations grow, you may need to manage the context sent to the AI model.
358
+
359
+ ### Clear Conversation
360
+ Completely start over with a fresh conversation.
361
+
362
+ **How to use:**
363
+ - Open Command Palette: `Ctrl+P`
364
+ - Type: "Clear Conversation"
365
+ - Confirm the action
366
+
367
+ **When to use:**
368
+ - Starting a completely new task or project
369
+ - When you want a clean slate
370
+ - Context has become too cluttered
371
+
372
+ ---
373
+
374
+ ### Compact Conversation
375
+ Intelligently compress the conversation history while preserving important context.
376
+
377
+ **How to use:**
378
+ - Open Command Palette: `Ctrl+P`
379
+ - Type: "Compact Conversation"
380
+ - Shotgun will compress older messages automatically
381
+
382
+ **When to use:**
383
+ - Conversation is getting long but you want to keep context
384
+ - Running into token limits
385
+ - Want to reduce costs while maintaining continuity
386
+
387
+ **What it does:**
388
+ - Summarizes older messages
389
+ - Keeps recent messages intact
390
+ - Preserves key information and decisions
391
+
392
+ ---
393
+
394
+ **Tip:** Use `Ctrl+U` to view your current usage and see how much context you're using!
395
+ """
396
+
397
+ @on(Button.Pressed, "#back-button")
398
+ def handle_back(self) -> None:
399
+ """Handle back button press."""
400
+ if self.current_page > 0:
401
+ self.current_page -= 1
402
+ self.update_page()
403
+
404
+ @on(Button.Pressed, "#next-button")
405
+ def handle_next(self) -> None:
406
+ """Handle next/finish button press."""
407
+ if self.current_page < self.total_pages - 1:
408
+ self.current_page += 1
409
+ self.update_page()
410
+ else:
411
+ # On last page, finish closes the modal
412
+ self.dismiss()
413
+
414
+ @on(Button.Pressed, "#close-button")
415
+ def handle_close(self) -> None:
416
+ """Handle close button press."""
417
+ self.dismiss()
418
+
419
+ @on(Button.Pressed, "#youtube-button")
420
+ def handle_youtube(self) -> None:
421
+ """Open demo section in README."""
422
+ webbrowser.open(
423
+ "https://github.com/shotgun-sh/shotgun?tab=readme-ov-file#-demo"
424
+ )
425
+
426
+ @on(Button.Pressed, "#usage-button")
427
+ def handle_usage_guide(self) -> None:
428
+ """Open usage guide in browser."""
429
+ webbrowser.open(
430
+ "https://github.com/shotgun-sh/shotgun?tab=readme-ov-file#-usage"
431
+ )