shotgun-sh 0.2.8.dev2__py3-none-any.whl → 0.3.3.dev1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. shotgun/agents/agent_manager.py +382 -60
  2. shotgun/agents/common.py +15 -9
  3. shotgun/agents/config/README.md +89 -0
  4. shotgun/agents/config/__init__.py +10 -1
  5. shotgun/agents/config/constants.py +0 -6
  6. shotgun/agents/config/manager.py +383 -82
  7. shotgun/agents/config/models.py +122 -18
  8. shotgun/agents/config/provider.py +81 -15
  9. shotgun/agents/config/streaming_test.py +119 -0
  10. shotgun/agents/context_analyzer/__init__.py +28 -0
  11. shotgun/agents/context_analyzer/analyzer.py +475 -0
  12. shotgun/agents/context_analyzer/constants.py +9 -0
  13. shotgun/agents/context_analyzer/formatter.py +115 -0
  14. shotgun/agents/context_analyzer/models.py +212 -0
  15. shotgun/agents/conversation/__init__.py +18 -0
  16. shotgun/agents/conversation/filters.py +164 -0
  17. shotgun/agents/conversation/history/chunking.py +278 -0
  18. shotgun/agents/{history → conversation/history}/compaction.py +36 -5
  19. shotgun/agents/{history → conversation/history}/constants.py +5 -0
  20. shotgun/agents/conversation/history/file_content_deduplication.py +216 -0
  21. shotgun/agents/{history → conversation/history}/history_processors.py +380 -8
  22. shotgun/agents/{history → conversation/history}/token_counting/anthropic.py +25 -1
  23. shotgun/agents/{history → conversation/history}/token_counting/base.py +14 -3
  24. shotgun/agents/{history → conversation/history}/token_counting/openai.py +11 -1
  25. shotgun/agents/{history → conversation/history}/token_counting/sentencepiece_counter.py +8 -0
  26. shotgun/agents/{history → conversation/history}/token_counting/tokenizer_cache.py +3 -1
  27. shotgun/agents/{history → conversation/history}/token_counting/utils.py +0 -3
  28. shotgun/agents/{conversation_manager.py → conversation/manager.py} +36 -20
  29. shotgun/agents/{conversation_history.py → conversation/models.py} +8 -92
  30. shotgun/agents/error/__init__.py +11 -0
  31. shotgun/agents/error/models.py +19 -0
  32. shotgun/agents/export.py +2 -2
  33. shotgun/agents/plan.py +2 -2
  34. shotgun/agents/research.py +3 -3
  35. shotgun/agents/runner.py +230 -0
  36. shotgun/agents/specify.py +2 -2
  37. shotgun/agents/tasks.py +2 -2
  38. shotgun/agents/tools/codebase/codebase_shell.py +6 -0
  39. shotgun/agents/tools/codebase/directory_lister.py +6 -0
  40. shotgun/agents/tools/codebase/file_read.py +11 -2
  41. shotgun/agents/tools/codebase/query_graph.py +6 -0
  42. shotgun/agents/tools/codebase/retrieve_code.py +6 -0
  43. shotgun/agents/tools/file_management.py +27 -7
  44. shotgun/agents/tools/registry.py +217 -0
  45. shotgun/agents/tools/web_search/__init__.py +8 -8
  46. shotgun/agents/tools/web_search/anthropic.py +8 -2
  47. shotgun/agents/tools/web_search/gemini.py +7 -1
  48. shotgun/agents/tools/web_search/openai.py +8 -2
  49. shotgun/agents/tools/web_search/utils.py +2 -2
  50. shotgun/agents/usage_manager.py +16 -11
  51. shotgun/api_endpoints.py +7 -3
  52. shotgun/build_constants.py +2 -2
  53. shotgun/cli/clear.py +53 -0
  54. shotgun/cli/compact.py +188 -0
  55. shotgun/cli/config.py +8 -5
  56. shotgun/cli/context.py +154 -0
  57. shotgun/cli/error_handler.py +24 -0
  58. shotgun/cli/export.py +34 -34
  59. shotgun/cli/feedback.py +4 -2
  60. shotgun/cli/models.py +1 -0
  61. shotgun/cli/plan.py +34 -34
  62. shotgun/cli/research.py +18 -10
  63. shotgun/cli/spec/__init__.py +5 -0
  64. shotgun/cli/spec/backup.py +81 -0
  65. shotgun/cli/spec/commands.py +132 -0
  66. shotgun/cli/spec/models.py +48 -0
  67. shotgun/cli/spec/pull_service.py +219 -0
  68. shotgun/cli/specify.py +20 -19
  69. shotgun/cli/tasks.py +34 -34
  70. shotgun/cli/update.py +16 -2
  71. shotgun/codebase/core/change_detector.py +5 -3
  72. shotgun/codebase/core/code_retrieval.py +4 -2
  73. shotgun/codebase/core/ingestor.py +163 -15
  74. shotgun/codebase/core/manager.py +13 -4
  75. shotgun/codebase/core/nl_query.py +1 -1
  76. shotgun/codebase/models.py +2 -0
  77. shotgun/exceptions.py +357 -0
  78. shotgun/llm_proxy/__init__.py +17 -0
  79. shotgun/llm_proxy/client.py +215 -0
  80. shotgun/llm_proxy/models.py +137 -0
  81. shotgun/logging_config.py +60 -27
  82. shotgun/main.py +77 -11
  83. shotgun/posthog_telemetry.py +38 -29
  84. shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +28 -2
  85. shotgun/prompts/agents/partials/interactive_mode.j2 +3 -3
  86. shotgun/prompts/agents/plan.j2 +16 -0
  87. shotgun/prompts/agents/research.j2 +16 -3
  88. shotgun/prompts/agents/specify.j2 +54 -1
  89. shotgun/prompts/agents/state/system_state.j2 +0 -2
  90. shotgun/prompts/agents/tasks.j2 +16 -0
  91. shotgun/prompts/history/chunk_summarization.j2 +34 -0
  92. shotgun/prompts/history/combine_summaries.j2 +53 -0
  93. shotgun/sdk/codebase.py +14 -3
  94. shotgun/sentry_telemetry.py +163 -16
  95. shotgun/settings.py +243 -0
  96. shotgun/shotgun_web/__init__.py +67 -1
  97. shotgun/shotgun_web/client.py +42 -1
  98. shotgun/shotgun_web/constants.py +46 -0
  99. shotgun/shotgun_web/exceptions.py +29 -0
  100. shotgun/shotgun_web/models.py +390 -0
  101. shotgun/shotgun_web/shared_specs/__init__.py +32 -0
  102. shotgun/shotgun_web/shared_specs/file_scanner.py +175 -0
  103. shotgun/shotgun_web/shared_specs/hasher.py +83 -0
  104. shotgun/shotgun_web/shared_specs/models.py +71 -0
  105. shotgun/shotgun_web/shared_specs/upload_pipeline.py +329 -0
  106. shotgun/shotgun_web/shared_specs/utils.py +34 -0
  107. shotgun/shotgun_web/specs_client.py +703 -0
  108. shotgun/shotgun_web/supabase_client.py +31 -0
  109. shotgun/telemetry.py +10 -33
  110. shotgun/tui/app.py +310 -46
  111. shotgun/tui/commands/__init__.py +1 -1
  112. shotgun/tui/components/context_indicator.py +179 -0
  113. shotgun/tui/components/mode_indicator.py +70 -0
  114. shotgun/tui/components/status_bar.py +48 -0
  115. shotgun/tui/containers.py +91 -0
  116. shotgun/tui/dependencies.py +39 -0
  117. shotgun/tui/layout.py +5 -0
  118. shotgun/tui/protocols.py +45 -0
  119. shotgun/tui/screens/chat/__init__.py +5 -0
  120. shotgun/tui/screens/chat/chat.tcss +54 -0
  121. shotgun/tui/screens/chat/chat_screen.py +1531 -0
  122. shotgun/tui/screens/chat/codebase_index_prompt_screen.py +243 -0
  123. shotgun/tui/screens/chat/codebase_index_selection.py +12 -0
  124. shotgun/tui/screens/chat/help_text.py +40 -0
  125. shotgun/tui/screens/chat/prompt_history.py +48 -0
  126. shotgun/tui/screens/chat.tcss +11 -0
  127. shotgun/tui/screens/chat_screen/command_providers.py +91 -4
  128. shotgun/tui/screens/chat_screen/hint_message.py +76 -1
  129. shotgun/tui/screens/chat_screen/history/__init__.py +22 -0
  130. shotgun/tui/screens/chat_screen/history/agent_response.py +66 -0
  131. shotgun/tui/screens/chat_screen/history/chat_history.py +115 -0
  132. shotgun/tui/screens/chat_screen/history/formatters.py +115 -0
  133. shotgun/tui/screens/chat_screen/history/partial_response.py +43 -0
  134. shotgun/tui/screens/chat_screen/history/user_question.py +42 -0
  135. shotgun/tui/screens/confirmation_dialog.py +191 -0
  136. shotgun/tui/screens/directory_setup.py +45 -41
  137. shotgun/tui/screens/feedback.py +14 -7
  138. shotgun/tui/screens/github_issue.py +111 -0
  139. shotgun/tui/screens/model_picker.py +77 -32
  140. shotgun/tui/screens/onboarding.py +580 -0
  141. shotgun/tui/screens/pipx_migration.py +205 -0
  142. shotgun/tui/screens/provider_config.py +116 -35
  143. shotgun/tui/screens/shared_specs/__init__.py +21 -0
  144. shotgun/tui/screens/shared_specs/create_spec_dialog.py +273 -0
  145. shotgun/tui/screens/shared_specs/models.py +56 -0
  146. shotgun/tui/screens/shared_specs/share_specs_dialog.py +390 -0
  147. shotgun/tui/screens/shared_specs/upload_progress_screen.py +452 -0
  148. shotgun/tui/screens/shotgun_auth.py +112 -18
  149. shotgun/tui/screens/spec_pull.py +288 -0
  150. shotgun/tui/screens/welcome.py +137 -11
  151. shotgun/tui/services/__init__.py +5 -0
  152. shotgun/tui/services/conversation_service.py +187 -0
  153. shotgun/tui/state/__init__.py +7 -0
  154. shotgun/tui/state/processing_state.py +185 -0
  155. shotgun/tui/utils/mode_progress.py +14 -7
  156. shotgun/tui/widgets/__init__.py +5 -0
  157. shotgun/tui/widgets/widget_coordinator.py +263 -0
  158. shotgun/utils/file_system_utils.py +22 -2
  159. shotgun/utils/marketing.py +110 -0
  160. shotgun/utils/update_checker.py +69 -14
  161. shotgun_sh-0.3.3.dev1.dist-info/METADATA +472 -0
  162. shotgun_sh-0.3.3.dev1.dist-info/RECORD +229 -0
  163. {shotgun_sh-0.2.8.dev2.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/WHEEL +1 -1
  164. {shotgun_sh-0.2.8.dev2.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/entry_points.txt +1 -0
  165. {shotgun_sh-0.2.8.dev2.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/licenses/LICENSE +1 -1
  166. shotgun/tui/screens/chat.py +0 -996
  167. shotgun/tui/screens/chat_screen/history.py +0 -335
  168. shotgun_sh-0.2.8.dev2.dist-info/METADATA +0 -126
  169. shotgun_sh-0.2.8.dev2.dist-info/RECORD +0 -155
  170. /shotgun/agents/{history → conversation/history}/__init__.py +0 -0
  171. /shotgun/agents/{history → conversation/history}/context_extraction.py +0 -0
  172. /shotgun/agents/{history → conversation/history}/history_building.py +0 -0
  173. /shotgun/agents/{history → conversation/history}/message_utils.py +0 -0
  174. /shotgun/agents/{history → conversation/history}/token_counting/__init__.py +0 -0
  175. /shotgun/agents/{history → conversation/history}/token_estimation.py +0 -0
@@ -0,0 +1,243 @@
1
+ """Modal dialog for codebase indexing prompts."""
2
+
3
+ import os
4
+ import webbrowser
5
+ from pathlib import Path
6
+
7
+ from textual import on
8
+ from textual.app import ComposeResult
9
+ from textual.containers import Container, VerticalScroll
10
+ from textual.events import Resize
11
+ from textual.screen import ModalScreen
12
+ from textual.widgets import Button, Label, Markdown, Static
13
+
14
+ from shotgun.tui.layout import COMPACT_HEIGHT_THRESHOLD
15
+ from shotgun.utils.file_system_utils import get_shotgun_home
16
+
17
+
18
+ def _is_home_directory() -> bool:
19
+ """Check if cwd is user's home directory.
20
+
21
+ Can be simulated with HOME_DIRECTORY_SIMULATE=true env var for testing.
22
+ """
23
+ if os.environ.get("HOME_DIRECTORY_SIMULATE", "").lower() == "true":
24
+ return True
25
+ return Path.cwd() == Path.home()
26
+
27
+
28
+ def _track_event(event_name: str) -> None:
29
+ """Track an event to PostHog."""
30
+ from shotgun.posthog_telemetry import track_event
31
+
32
+ track_event(event_name)
33
+
34
+
35
+ class CodebaseIndexPromptScreen(ModalScreen[bool]):
36
+ """Modal dialog asking whether to index the detected codebase."""
37
+
38
+ DEFAULT_CSS = """
39
+ CodebaseIndexPromptScreen {
40
+ align: center middle;
41
+ background: rgba(0, 0, 0, 0.0);
42
+ }
43
+
44
+ CodebaseIndexPromptScreen > #index-prompt-dialog {
45
+ width: 80%;
46
+ max-width: 90;
47
+ height: auto;
48
+ max-height: 85%;
49
+ border: wide $primary;
50
+ padding: 1 2;
51
+ layout: vertical;
52
+ background: $surface;
53
+ }
54
+
55
+ #index-prompt-title {
56
+ text-style: bold;
57
+ color: $text-accent;
58
+ text-align: center;
59
+ padding-bottom: 1;
60
+ }
61
+
62
+ #index-prompt-content {
63
+ height: auto;
64
+ max-height: 1fr;
65
+ }
66
+
67
+ #index-prompt-info {
68
+ padding: 0 1;
69
+ }
70
+
71
+ #index-prompt-buttons {
72
+ layout: horizontal;
73
+ align-horizontal: right;
74
+ height: auto;
75
+ padding-top: 1;
76
+ }
77
+
78
+ #index-prompt-buttons Button {
79
+ margin: 0 1;
80
+ min-width: 12;
81
+ }
82
+
83
+ #index-prompt-warning {
84
+ background: $surface-lighten-1;
85
+ color: $text;
86
+ padding: 1 2;
87
+ margin-bottom: 1;
88
+ text-align: center;
89
+ }
90
+
91
+ #compact-link {
92
+ text-align: center;
93
+ padding: 1 0;
94
+ display: none;
95
+ }
96
+
97
+ /* Compact styles for short terminals */
98
+ #index-prompt-dialog.compact {
99
+ padding: 0 1;
100
+ border: none;
101
+ max-height: 100%;
102
+ }
103
+
104
+ #index-prompt-dialog.compact #index-prompt-content {
105
+ display: none;
106
+ }
107
+
108
+ #index-prompt-dialog.compact #compact-link {
109
+ display: block;
110
+ }
111
+
112
+ #index-prompt-dialog.compact #index-prompt-warning {
113
+ padding: 0;
114
+ margin-bottom: 0;
115
+ background: transparent;
116
+ }
117
+
118
+ #index-prompt-dialog.compact #index-prompt-title {
119
+ padding-bottom: 0;
120
+ }
121
+
122
+ #index-prompt-dialog.compact #index-prompt-buttons {
123
+ padding-top: 0;
124
+ }
125
+ """
126
+
127
+ def compose(self) -> ComposeResult:
128
+ storage_path = get_shotgun_home() / "codebases"
129
+ cwd = Path.cwd()
130
+ is_home = _is_home_directory()
131
+
132
+ with Container(id="index-prompt-dialog"):
133
+ if is_home:
134
+ # Show warning for home directory
135
+ yield Label(
136
+ "Home directory detected",
137
+ id="index-prompt-title",
138
+ )
139
+ yield Static(
140
+ "Running from home directory isn't recommended.",
141
+ id="index-prompt-warning",
142
+ )
143
+ with Container(id="index-prompt-buttons"):
144
+ yield Button(
145
+ "Quit",
146
+ id="index-prompt-quit",
147
+ )
148
+ yield Button(
149
+ "Continue without indexing",
150
+ id="index-prompt-continue",
151
+ )
152
+ else:
153
+ # Normal indexing prompt
154
+ content = f"""
155
+ ## 🔒 Your code never leaves your computer
156
+
157
+ Shotgun will index the codebase at:
158
+ **`{cwd}`**
159
+ _(This is the current working directory where you started Shotgun)_
160
+
161
+ ### What happens during indexing:
162
+
163
+ - **Stays on your computer**: Index is stored locally at `{storage_path}` - it will not be stored on a server
164
+ - **Zero cost**: Indexing runs entirely on your machine
165
+ - **Runs in the background**: Usually takes 1-3 minutes, and you can continue using Shotgun while it indexes
166
+ - **Enable code understanding**: Allows Shotgun to answer questions about your codebase
167
+
168
+ ---
169
+
170
+ If you're curious, you can review how Shotgun indexes/queries code by taking a look at the [source code](https://github.com/shotgun-sh/shotgun).
171
+
172
+ We take your privacy seriously. You can read our full [privacy policy](https://app.shotgun.sh/privacy) for more details.
173
+ """
174
+ yield Label(
175
+ "Want to index your codebase?",
176
+ id="index-prompt-title",
177
+ )
178
+ # Compact mode: show only a link
179
+ yield Static(
180
+ "[@click=screen.open_faq]Learn more about indexing[/]",
181
+ id="compact-link",
182
+ markup=True,
183
+ )
184
+ # Full mode: show detailed content
185
+ with VerticalScroll(id="index-prompt-content"):
186
+ yield Markdown(content, id="index-prompt-info")
187
+ with Container(id="index-prompt-buttons"):
188
+ yield Button(
189
+ "Not now",
190
+ id="index-prompt-cancel",
191
+ )
192
+ yield Button(
193
+ "Index now",
194
+ id="index-prompt-confirm",
195
+ variant="primary",
196
+ )
197
+
198
+ def on_mount(self) -> None:
199
+ """Track when the home directory warning screen is shown and apply compact layout."""
200
+ if _is_home_directory():
201
+ _track_event("home_directory_warning_shown")
202
+ # Apply compact layout if starting in a short terminal
203
+ self._apply_compact_layout(self.app.size.height < COMPACT_HEIGHT_THRESHOLD)
204
+
205
+ @on(Resize)
206
+ def handle_resize(self, event: Resize) -> None:
207
+ """Adjust layout based on terminal height."""
208
+ self._apply_compact_layout(event.size.height < COMPACT_HEIGHT_THRESHOLD)
209
+
210
+ def _apply_compact_layout(self, compact: bool) -> None:
211
+ """Apply or remove compact layout classes for short terminals."""
212
+ dialog = self.query_one("#index-prompt-dialog")
213
+ if compact:
214
+ dialog.add_class("compact")
215
+ else:
216
+ dialog.remove_class("compact")
217
+
218
+ def action_open_faq(self) -> None:
219
+ """Open the FAQ page in a browser."""
220
+ webbrowser.open("https://github.com/shotgun-sh/shotgun?tab=readme-ov-file#faq")
221
+
222
+ @on(Button.Pressed, "#index-prompt-cancel")
223
+ def handle_cancel(self, event: Button.Pressed) -> None:
224
+ event.stop()
225
+ self.dismiss(False)
226
+
227
+ @on(Button.Pressed, "#index-prompt-confirm")
228
+ def handle_confirm(self, event: Button.Pressed) -> None:
229
+ event.stop()
230
+ self.dismiss(True)
231
+
232
+ @on(Button.Pressed, "#index-prompt-continue")
233
+ def handle_continue(self, event: Button.Pressed) -> None:
234
+ """Continue without indexing when in home directory."""
235
+ event.stop()
236
+ _track_event("home_directory_warning_continue")
237
+ self.dismiss(False)
238
+
239
+ @on(Button.Pressed, "#index-prompt-quit")
240
+ def handle_quit(self, event: Button.Pressed) -> None:
241
+ event.stop()
242
+ _track_event("home_directory_warning_quit")
243
+ self.app.exit()
@@ -0,0 +1,12 @@
1
+ """Codebase indexing selection models."""
2
+
3
+ from pathlib import Path
4
+
5
+ from pydantic import BaseModel
6
+
7
+
8
+ class CodebaseIndexSelection(BaseModel):
9
+ """User-selected repository path and name for indexing."""
10
+
11
+ repo_path: Path
12
+ name: str
@@ -0,0 +1,40 @@
1
+ """Helper functions for chat screen help text."""
2
+
3
+
4
+ def help_text_with_codebase(already_indexed: bool = False) -> str:
5
+ """Generate help text for when a codebase is available.
6
+
7
+ Args:
8
+ already_indexed: Whether the codebase is already indexed.
9
+
10
+ Returns:
11
+ Formatted help text string.
12
+ """
13
+ return (
14
+ "Howdy! Welcome to Shotgun - Spec Driven Development for Developers and AI Agents.\n\n"
15
+ "Shotgun writes codebase-aware specs for your AI coding agents so they don't derail.\n\n"
16
+ f"{'It' if already_indexed else 'Once your codebase is indexed, it'} can help you:\n"
17
+ "- Research your codebase and spec out new features\n"
18
+ "- Create implementation plans that fit your architecture\n"
19
+ "- Generate AGENTS.md files for AI coding agents\n"
20
+ "- Onboard to existing projects or plan refactors\n\n"
21
+ "Ready to build something? Let's go.\n"
22
+ )
23
+
24
+
25
+ def help_text_empty_dir() -> str:
26
+ """Generate help text for empty directory.
27
+
28
+ Returns:
29
+ Formatted help text string.
30
+ """
31
+ return (
32
+ "Howdy! Welcome to Shotgun - Spec Driven Development for Developers and AI Agents.\n\n"
33
+ "Shotgun writes codebase-aware specs for your AI coding agents so they don't derail.\n\n"
34
+ "It can help you:\n"
35
+ "- Research your codebase and spec out new features\n"
36
+ "- Create implementation plans that fit your architecture\n"
37
+ "- Generate AGENTS.md files for AI coding agents\n"
38
+ "- Onboard to existing projects or plan refactors\n\n"
39
+ "Ready to build something? Let's go.\n"
40
+ )
@@ -0,0 +1,48 @@
1
+ """Prompt history management for chat screen."""
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+
6
+ class PromptHistory(BaseModel):
7
+ """Manages prompt history for navigation in chat input."""
8
+
9
+ prompts: list[str] = Field(default_factory=lambda: ["Hello there!"])
10
+ curr: int | None = None
11
+
12
+ def next(self) -> str:
13
+ """Navigate to next prompt in history.
14
+
15
+ Returns:
16
+ The next prompt in history.
17
+ """
18
+ if self.curr is None:
19
+ self.curr = -1
20
+ else:
21
+ self.curr = -1
22
+ return self.prompts[self.curr]
23
+
24
+ def prev(self) -> str:
25
+ """Navigate to previous prompt in history.
26
+
27
+ Returns:
28
+ The previous prompt in history.
29
+
30
+ Raises:
31
+ Exception: If current entry is None.
32
+ """
33
+ if self.curr is None:
34
+ raise Exception("current entry is none")
35
+ if self.curr == -1:
36
+ self.curr = None
37
+ return ""
38
+ self.curr += 1
39
+ return ""
40
+
41
+ def append(self, text: str) -> None:
42
+ """Add a new prompt to history.
43
+
44
+ Args:
45
+ text: The prompt text to add.
46
+ """
47
+ self.prompts.append(text)
48
+ self.curr = None
@@ -38,6 +38,17 @@ ModeIndicator {
38
38
  }
39
39
 
40
40
 
41
+ #right-footer-indicators {
42
+ width: auto;
43
+ height: auto;
44
+ layout: vertical;
45
+ }
46
+
47
+ #context-indicator {
48
+ text-align: end;
49
+ height: 1;
50
+ }
51
+
41
52
  #indexing-job-display {
42
53
  text-align: end;
43
54
  }
@@ -5,6 +5,7 @@ from textual.command import DiscoveryHit, Hit, Provider
5
5
 
6
6
  from shotgun.agents.models import AgentType
7
7
  from shotgun.codebase.models import CodebaseGraph
8
+ from shotgun.tui.screens.chat_screen.hint_message import HintMessage
8
9
  from shotgun.tui.screens.model_picker import ModelPickerScreen
9
10
  from shotgun.tui.screens.provider_config import ProviderConfigScreen
10
11
 
@@ -130,6 +131,38 @@ class UsageProvider(Provider):
130
131
  )
131
132
 
132
133
 
134
+ class ContextProvider(Provider):
135
+ """Command provider for showing conversation context analysis."""
136
+
137
+ @property
138
+ def chat_screen(self) -> "ChatScreen":
139
+ from shotgun.tui.screens.chat import ChatScreen
140
+
141
+ return cast(ChatScreen, self.screen)
142
+
143
+ async def discover(self) -> AsyncGenerator[DiscoveryHit, None]:
144
+ """Provide context command when palette opens."""
145
+ yield DiscoveryHit(
146
+ "Show context",
147
+ self.chat_screen.action_show_context,
148
+ help="Display conversation context composition and statistics",
149
+ )
150
+
151
+ async def search(self, query: str) -> AsyncGenerator[Hit, None]:
152
+ """Search for context command."""
153
+ matcher = self.matcher(query)
154
+
155
+ async for discovery_hit in self.discover():
156
+ score = matcher.match(discovery_hit.text or "")
157
+ if score > 0:
158
+ yield Hit(
159
+ score,
160
+ matcher.highlight(discovery_hit.text or ""),
161
+ discovery_hit.command,
162
+ help=discovery_hit.help,
163
+ )
164
+
165
+
133
166
  class ProviderSetupProvider(Provider):
134
167
  """Command palette entries for provider configuration."""
135
168
 
@@ -145,7 +178,9 @@ class ProviderSetupProvider(Provider):
145
178
 
146
179
  def open_model_picker(self) -> None:
147
180
  """Show the model picker screen."""
148
- self.chat_screen.app.push_screen(ModelPickerScreen())
181
+ self.chat_screen.app.push_screen(
182
+ ModelPickerScreen(), callback=self.chat_screen.handle_model_selected
183
+ )
149
184
 
150
185
  async def discover(self) -> AsyncGenerator[DiscoveryHit, None]:
151
186
  yield DiscoveryHit(
@@ -237,8 +272,8 @@ class DeleteCodebasePaletteProvider(Provider):
237
272
  try:
238
273
  result = await self.chat_screen.codebase_sdk.list_codebases()
239
274
  except Exception as exc: # pragma: no cover - defensive UI path
240
- self.chat_screen.notify(
241
- f"Unable to load codebases: {exc}", severity="error"
275
+ self.chat_screen.agent_manager.add_hint_message(
276
+ HintMessage(message=f"Unable to load codebases: {exc}")
242
277
  )
243
278
  return []
244
279
  return result.graphs
@@ -288,11 +323,18 @@ class UnifiedCommandProvider(Provider):
288
323
 
289
324
  def open_model_picker(self) -> None:
290
325
  """Show the model picker screen."""
291
- self.chat_screen.app.push_screen(ModelPickerScreen())
326
+ self.chat_screen.app.push_screen(
327
+ ModelPickerScreen(), callback=self.chat_screen.handle_model_selected
328
+ )
292
329
 
293
330
  async def discover(self) -> AsyncGenerator[DiscoveryHit, None]:
294
331
  """Provide commands in alphabetical order when palette opens."""
295
332
  # Alphabetically ordered commands
333
+ yield DiscoveryHit(
334
+ "Clear Conversation",
335
+ self.chat_screen.action_clear_conversation,
336
+ help="Clear the entire conversation history",
337
+ )
296
338
  yield DiscoveryHit(
297
339
  "Codebase: Delete Codebase Index",
298
340
  self.chat_screen.delete_codebase_command,
@@ -303,6 +345,11 @@ class UnifiedCommandProvider(Provider):
303
345
  self.chat_screen.index_codebase_command,
304
346
  help="Index a repository into the codebase graph",
305
347
  )
348
+ yield DiscoveryHit(
349
+ "Compact Conversation",
350
+ self.chat_screen.action_compact_conversation,
351
+ help="Reduce conversation size by compacting message history",
352
+ )
306
353
  yield DiscoveryHit(
307
354
  "Open Provider Setup",
308
355
  self.open_provider_config,
@@ -313,11 +360,26 @@ class UnifiedCommandProvider(Provider):
313
360
  self.open_model_picker,
314
361
  help="🤖 Choose which AI model to use",
315
362
  )
363
+ yield DiscoveryHit(
364
+ "Share specs to workspace",
365
+ self.chat_screen.share_specs_command,
366
+ help="📤 Upload .shotgun/ files to share with your team",
367
+ )
368
+ yield DiscoveryHit(
369
+ "Show context",
370
+ self.chat_screen.action_show_context,
371
+ help="Display conversation context composition and statistics",
372
+ )
316
373
  yield DiscoveryHit(
317
374
  "Show usage",
318
375
  self.chat_screen.action_show_usage,
319
376
  help="Display usage information for the current session",
320
377
  )
378
+ yield DiscoveryHit(
379
+ "View Onboarding",
380
+ self.chat_screen.action_view_onboarding,
381
+ help="View the onboarding tutorial and helpful resources",
382
+ )
321
383
 
322
384
  async def search(self, query: str) -> AsyncGenerator[Hit, None]:
323
385
  """Search for commands in alphabetical order."""
@@ -325,6 +387,11 @@ class UnifiedCommandProvider(Provider):
325
387
 
326
388
  # Define all commands in alphabetical order
327
389
  commands = [
390
+ (
391
+ "Clear Conversation",
392
+ self.chat_screen.action_clear_conversation,
393
+ "Clear the entire conversation history",
394
+ ),
328
395
  (
329
396
  "Codebase: Delete Codebase Index",
330
397
  self.chat_screen.delete_codebase_command,
@@ -335,6 +402,11 @@ class UnifiedCommandProvider(Provider):
335
402
  self.chat_screen.index_codebase_command,
336
403
  "Index a repository into the codebase graph",
337
404
  ),
405
+ (
406
+ "Compact Conversation",
407
+ self.chat_screen.action_compact_conversation,
408
+ "Reduce conversation size by compacting message history",
409
+ ),
338
410
  (
339
411
  "Open Provider Setup",
340
412
  self.open_provider_config,
@@ -345,11 +417,26 @@ class UnifiedCommandProvider(Provider):
345
417
  self.open_model_picker,
346
418
  "🤖 Choose which AI model to use",
347
419
  ),
420
+ (
421
+ "Share specs to workspace",
422
+ self.chat_screen.share_specs_command,
423
+ "📤 Upload .shotgun/ files to share with your team",
424
+ ),
425
+ (
426
+ "Show context",
427
+ self.chat_screen.action_show_context,
428
+ "Display conversation context composition and statistics",
429
+ ),
348
430
  (
349
431
  "Show usage",
350
432
  self.chat_screen.action_show_usage,
351
433
  "Display usage information for the current session",
352
434
  ),
435
+ (
436
+ "View Onboarding",
437
+ self.chat_screen.action_view_onboarding,
438
+ "View the onboarding tutorial and helpful resources",
439
+ ),
353
440
  ]
354
441
 
355
442
  for title, callback, help_text in commands:
@@ -1,14 +1,23 @@
1
1
  from typing import Literal
2
2
 
3
3
  from pydantic import BaseModel
4
+ from textual import on
4
5
  from textual.app import ComposeResult
6
+ from textual.containers import Horizontal
5
7
  from textual.widget import Widget
6
- from textual.widgets import Markdown
8
+ from textual.widgets import Button, Label, Markdown, Static
9
+
10
+ from shotgun.logging_config import get_logger
11
+
12
+ logger = get_logger(__name__)
7
13
 
8
14
 
9
15
  class HintMessage(BaseModel):
10
16
  message: str
11
17
  kind: Literal["hint"] = "hint"
18
+ # Optional email copy functionality
19
+ email: str | None = None
20
+ markdown_after: str | None = None
12
21
 
13
22
 
14
23
  class HintMessageWidget(Widget):
@@ -30,6 +39,30 @@ class HintMessageWidget(Widget):
30
39
  }
31
40
  }
32
41
 
42
+ HintMessageWidget .email-copy-row {
43
+ width: auto;
44
+ height: auto;
45
+ margin: 1 0;
46
+ }
47
+
48
+ HintMessageWidget .email-text {
49
+ width: auto;
50
+ margin-right: 1;
51
+ content-align: left middle;
52
+ }
53
+
54
+ HintMessageWidget .copy-btn {
55
+ width: auto;
56
+ min-width: 12;
57
+ }
58
+
59
+ HintMessageWidget #copy-status {
60
+ height: 1;
61
+ width: 100%;
62
+ margin-top: 1;
63
+ content-align: left middle;
64
+ }
65
+
33
66
  """
34
67
 
35
68
  def __init__(self, message: HintMessage) -> None:
@@ -37,4 +70,46 @@ class HintMessageWidget(Widget):
37
70
  self.message = message
38
71
 
39
72
  def compose(self) -> ComposeResult:
73
+ # Main message markdown
40
74
  yield Markdown(markdown=f"{self.message.message}")
75
+
76
+ # Optional email copy section
77
+ if self.message.email:
78
+ # Email + copy button on same line
79
+ with Horizontal(classes="email-copy-row"):
80
+ yield Static(f"Contact: {self.message.email}", classes="email-text")
81
+ yield Button("Copy email", id="copy-email-btn", classes="copy-btn")
82
+
83
+ # Status feedback label
84
+ yield Label("", id="copy-status")
85
+
86
+ # Optional markdown after email
87
+ if self.message.markdown_after:
88
+ yield Markdown(self.message.markdown_after)
89
+
90
+ @on(Button.Pressed, "#copy-email-btn")
91
+ def _copy_email(self) -> None:
92
+ """Copy email address to clipboard when button is pressed."""
93
+ if not self.message.email:
94
+ return
95
+
96
+ status_label = self.query_one("#copy-status", Label)
97
+
98
+ try:
99
+ import pyperclip # type: ignore[import-untyped] # noqa: PGH003
100
+
101
+ pyperclip.copy(self.message.email)
102
+ status_label.update("✓ Copied to clipboard!")
103
+ logger.debug(
104
+ f"Successfully copied email to clipboard: {self.message.email}"
105
+ )
106
+
107
+ except ImportError:
108
+ status_label.update(
109
+ f"⚠️ Clipboard unavailable. Please manually copy: {self.message.email}"
110
+ )
111
+ logger.warning("pyperclip not available for clipboard operations")
112
+
113
+ except Exception as e:
114
+ status_label.update(f"⚠️ Copy failed: {e}")
115
+ logger.error(f"Failed to copy email to clipboard: {e}", exc_info=True)
@@ -0,0 +1,22 @@
1
+ """Chat history package - displays conversation messages in the TUI.
2
+
3
+ This package provides widgets for displaying chat history including:
4
+ - User questions
5
+ - Agent responses
6
+ - Tool calls
7
+ - Streaming/partial responses
8
+ """
9
+
10
+ from .agent_response import AgentResponseWidget
11
+ from .chat_history import ChatHistory
12
+ from .formatters import ToolFormatter
13
+ from .partial_response import PartialResponseWidget
14
+ from .user_question import UserQuestionWidget
15
+
16
+ __all__ = [
17
+ "ChatHistory",
18
+ "PartialResponseWidget",
19
+ "AgentResponseWidget",
20
+ "UserQuestionWidget",
21
+ "ToolFormatter",
22
+ ]