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.
- shotgun/agents/agent_manager.py +382 -60
- shotgun/agents/common.py +15 -9
- shotgun/agents/config/README.md +89 -0
- shotgun/agents/config/__init__.py +10 -1
- shotgun/agents/config/constants.py +0 -6
- shotgun/agents/config/manager.py +383 -82
- shotgun/agents/config/models.py +122 -18
- shotgun/agents/config/provider.py +81 -15
- shotgun/agents/config/streaming_test.py +119 -0
- shotgun/agents/context_analyzer/__init__.py +28 -0
- shotgun/agents/context_analyzer/analyzer.py +475 -0
- shotgun/agents/context_analyzer/constants.py +9 -0
- shotgun/agents/context_analyzer/formatter.py +115 -0
- shotgun/agents/context_analyzer/models.py +212 -0
- shotgun/agents/conversation/__init__.py +18 -0
- shotgun/agents/conversation/filters.py +164 -0
- shotgun/agents/conversation/history/chunking.py +278 -0
- shotgun/agents/{history → conversation/history}/compaction.py +36 -5
- shotgun/agents/{history → conversation/history}/constants.py +5 -0
- shotgun/agents/conversation/history/file_content_deduplication.py +216 -0
- shotgun/agents/{history → conversation/history}/history_processors.py +380 -8
- shotgun/agents/{history → conversation/history}/token_counting/anthropic.py +25 -1
- shotgun/agents/{history → conversation/history}/token_counting/base.py +14 -3
- shotgun/agents/{history → conversation/history}/token_counting/openai.py +11 -1
- shotgun/agents/{history → conversation/history}/token_counting/sentencepiece_counter.py +8 -0
- shotgun/agents/{history → conversation/history}/token_counting/tokenizer_cache.py +3 -1
- shotgun/agents/{history → conversation/history}/token_counting/utils.py +0 -3
- shotgun/agents/{conversation_manager.py → conversation/manager.py} +36 -20
- shotgun/agents/{conversation_history.py → conversation/models.py} +8 -92
- shotgun/agents/error/__init__.py +11 -0
- shotgun/agents/error/models.py +19 -0
- shotgun/agents/export.py +2 -2
- shotgun/agents/plan.py +2 -2
- shotgun/agents/research.py +3 -3
- shotgun/agents/runner.py +230 -0
- shotgun/agents/specify.py +2 -2
- shotgun/agents/tasks.py +2 -2
- shotgun/agents/tools/codebase/codebase_shell.py +6 -0
- shotgun/agents/tools/codebase/directory_lister.py +6 -0
- shotgun/agents/tools/codebase/file_read.py +11 -2
- shotgun/agents/tools/codebase/query_graph.py +6 -0
- shotgun/agents/tools/codebase/retrieve_code.py +6 -0
- shotgun/agents/tools/file_management.py +27 -7
- shotgun/agents/tools/registry.py +217 -0
- shotgun/agents/tools/web_search/__init__.py +8 -8
- shotgun/agents/tools/web_search/anthropic.py +8 -2
- shotgun/agents/tools/web_search/gemini.py +7 -1
- shotgun/agents/tools/web_search/openai.py +8 -2
- shotgun/agents/tools/web_search/utils.py +2 -2
- shotgun/agents/usage_manager.py +16 -11
- shotgun/api_endpoints.py +7 -3
- shotgun/build_constants.py +2 -2
- shotgun/cli/clear.py +53 -0
- shotgun/cli/compact.py +188 -0
- shotgun/cli/config.py +8 -5
- shotgun/cli/context.py +154 -0
- shotgun/cli/error_handler.py +24 -0
- shotgun/cli/export.py +34 -34
- shotgun/cli/feedback.py +4 -2
- shotgun/cli/models.py +1 -0
- shotgun/cli/plan.py +34 -34
- shotgun/cli/research.py +18 -10
- shotgun/cli/spec/__init__.py +5 -0
- shotgun/cli/spec/backup.py +81 -0
- shotgun/cli/spec/commands.py +132 -0
- shotgun/cli/spec/models.py +48 -0
- shotgun/cli/spec/pull_service.py +219 -0
- shotgun/cli/specify.py +20 -19
- shotgun/cli/tasks.py +34 -34
- shotgun/cli/update.py +16 -2
- shotgun/codebase/core/change_detector.py +5 -3
- shotgun/codebase/core/code_retrieval.py +4 -2
- shotgun/codebase/core/ingestor.py +163 -15
- shotgun/codebase/core/manager.py +13 -4
- shotgun/codebase/core/nl_query.py +1 -1
- shotgun/codebase/models.py +2 -0
- shotgun/exceptions.py +357 -0
- shotgun/llm_proxy/__init__.py +17 -0
- shotgun/llm_proxy/client.py +215 -0
- shotgun/llm_proxy/models.py +137 -0
- shotgun/logging_config.py +60 -27
- shotgun/main.py +77 -11
- shotgun/posthog_telemetry.py +38 -29
- shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +28 -2
- shotgun/prompts/agents/partials/interactive_mode.j2 +3 -3
- shotgun/prompts/agents/plan.j2 +16 -0
- shotgun/prompts/agents/research.j2 +16 -3
- shotgun/prompts/agents/specify.j2 +54 -1
- shotgun/prompts/agents/state/system_state.j2 +0 -2
- shotgun/prompts/agents/tasks.j2 +16 -0
- shotgun/prompts/history/chunk_summarization.j2 +34 -0
- shotgun/prompts/history/combine_summaries.j2 +53 -0
- shotgun/sdk/codebase.py +14 -3
- shotgun/sentry_telemetry.py +163 -16
- shotgun/settings.py +243 -0
- shotgun/shotgun_web/__init__.py +67 -1
- shotgun/shotgun_web/client.py +42 -1
- shotgun/shotgun_web/constants.py +46 -0
- shotgun/shotgun_web/exceptions.py +29 -0
- shotgun/shotgun_web/models.py +390 -0
- shotgun/shotgun_web/shared_specs/__init__.py +32 -0
- shotgun/shotgun_web/shared_specs/file_scanner.py +175 -0
- shotgun/shotgun_web/shared_specs/hasher.py +83 -0
- shotgun/shotgun_web/shared_specs/models.py +71 -0
- shotgun/shotgun_web/shared_specs/upload_pipeline.py +329 -0
- shotgun/shotgun_web/shared_specs/utils.py +34 -0
- shotgun/shotgun_web/specs_client.py +703 -0
- shotgun/shotgun_web/supabase_client.py +31 -0
- shotgun/telemetry.py +10 -33
- shotgun/tui/app.py +310 -46
- shotgun/tui/commands/__init__.py +1 -1
- shotgun/tui/components/context_indicator.py +179 -0
- shotgun/tui/components/mode_indicator.py +70 -0
- shotgun/tui/components/status_bar.py +48 -0
- shotgun/tui/containers.py +91 -0
- shotgun/tui/dependencies.py +39 -0
- shotgun/tui/layout.py +5 -0
- shotgun/tui/protocols.py +45 -0
- shotgun/tui/screens/chat/__init__.py +5 -0
- shotgun/tui/screens/chat/chat.tcss +54 -0
- shotgun/tui/screens/chat/chat_screen.py +1531 -0
- shotgun/tui/screens/chat/codebase_index_prompt_screen.py +243 -0
- shotgun/tui/screens/chat/codebase_index_selection.py +12 -0
- shotgun/tui/screens/chat/help_text.py +40 -0
- shotgun/tui/screens/chat/prompt_history.py +48 -0
- shotgun/tui/screens/chat.tcss +11 -0
- shotgun/tui/screens/chat_screen/command_providers.py +91 -4
- shotgun/tui/screens/chat_screen/hint_message.py +76 -1
- shotgun/tui/screens/chat_screen/history/__init__.py +22 -0
- shotgun/tui/screens/chat_screen/history/agent_response.py +66 -0
- shotgun/tui/screens/chat_screen/history/chat_history.py +115 -0
- shotgun/tui/screens/chat_screen/history/formatters.py +115 -0
- shotgun/tui/screens/chat_screen/history/partial_response.py +43 -0
- shotgun/tui/screens/chat_screen/history/user_question.py +42 -0
- shotgun/tui/screens/confirmation_dialog.py +191 -0
- shotgun/tui/screens/directory_setup.py +45 -41
- shotgun/tui/screens/feedback.py +14 -7
- shotgun/tui/screens/github_issue.py +111 -0
- shotgun/tui/screens/model_picker.py +77 -32
- shotgun/tui/screens/onboarding.py +580 -0
- shotgun/tui/screens/pipx_migration.py +205 -0
- shotgun/tui/screens/provider_config.py +116 -35
- shotgun/tui/screens/shared_specs/__init__.py +21 -0
- shotgun/tui/screens/shared_specs/create_spec_dialog.py +273 -0
- shotgun/tui/screens/shared_specs/models.py +56 -0
- shotgun/tui/screens/shared_specs/share_specs_dialog.py +390 -0
- shotgun/tui/screens/shared_specs/upload_progress_screen.py +452 -0
- shotgun/tui/screens/shotgun_auth.py +112 -18
- shotgun/tui/screens/spec_pull.py +288 -0
- shotgun/tui/screens/welcome.py +137 -11
- shotgun/tui/services/__init__.py +5 -0
- shotgun/tui/services/conversation_service.py +187 -0
- shotgun/tui/state/__init__.py +7 -0
- shotgun/tui/state/processing_state.py +185 -0
- shotgun/tui/utils/mode_progress.py +14 -7
- shotgun/tui/widgets/__init__.py +5 -0
- shotgun/tui/widgets/widget_coordinator.py +263 -0
- shotgun/utils/file_system_utils.py +22 -2
- shotgun/utils/marketing.py +110 -0
- shotgun/utils/update_checker.py +69 -14
- shotgun_sh-0.3.3.dev1.dist-info/METADATA +472 -0
- shotgun_sh-0.3.3.dev1.dist-info/RECORD +229 -0
- {shotgun_sh-0.2.8.dev2.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/WHEEL +1 -1
- {shotgun_sh-0.2.8.dev2.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/entry_points.txt +1 -0
- {shotgun_sh-0.2.8.dev2.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/licenses/LICENSE +1 -1
- shotgun/tui/screens/chat.py +0 -996
- shotgun/tui/screens/chat_screen/history.py +0 -335
- shotgun_sh-0.2.8.dev2.dist-info/METADATA +0 -126
- shotgun_sh-0.2.8.dev2.dist-info/RECORD +0 -155
- /shotgun/agents/{history → conversation/history}/__init__.py +0 -0
- /shotgun/agents/{history → conversation/history}/context_extraction.py +0 -0
- /shotgun/agents/{history → conversation/history}/history_building.py +0 -0
- /shotgun/agents/{history → conversation/history}/message_utils.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_counting/__init__.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_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,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
|
shotgun/tui/screens/chat.tcss
CHANGED
|
@@ -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(
|
|
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.
|
|
241
|
-
f"Unable to load codebases: {exc}"
|
|
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(
|
|
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
|
+
]
|