shotgun-sh 0.2.3.dev2__py3-none-any.whl → 0.2.11.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.
Potentially problematic release.
This version of shotgun-sh might be problematic. Click here for more details.
- shotgun/agents/agent_manager.py +524 -58
- shotgun/agents/common.py +62 -62
- shotgun/agents/config/constants.py +0 -6
- shotgun/agents/config/manager.py +14 -3
- shotgun/agents/config/models.py +16 -0
- shotgun/agents/config/provider.py +68 -13
- shotgun/agents/context_analyzer/__init__.py +28 -0
- shotgun/agents/context_analyzer/analyzer.py +493 -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_history.py +125 -2
- shotgun/agents/conversation_manager.py +24 -2
- shotgun/agents/export.py +4 -5
- shotgun/agents/history/compaction.py +9 -4
- shotgun/agents/history/context_extraction.py +93 -6
- shotgun/agents/history/history_processors.py +14 -2
- shotgun/agents/history/token_counting/anthropic.py +32 -10
- shotgun/agents/models.py +50 -2
- shotgun/agents/plan.py +4 -5
- shotgun/agents/research.py +4 -5
- shotgun/agents/specify.py +4 -5
- shotgun/agents/tasks.py +4 -5
- shotgun/agents/tools/__init__.py +0 -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 +6 -0
- shotgun/agents/tools/codebase/query_graph.py +6 -0
- shotgun/agents/tools/codebase/retrieve_code.py +6 -0
- shotgun/agents/tools/file_management.py +71 -9
- shotgun/agents/tools/registry.py +217 -0
- shotgun/agents/tools/web_search/__init__.py +24 -12
- shotgun/agents/tools/web_search/anthropic.py +24 -3
- shotgun/agents/tools/web_search/gemini.py +22 -10
- shotgun/agents/tools/web_search/openai.py +21 -12
- shotgun/api_endpoints.py +7 -3
- shotgun/build_constants.py +1 -1
- shotgun/cli/clear.py +52 -0
- shotgun/cli/compact.py +186 -0
- shotgun/cli/context.py +111 -0
- shotgun/cli/models.py +1 -0
- shotgun/cli/update.py +16 -2
- shotgun/codebase/core/manager.py +10 -1
- shotgun/llm_proxy/__init__.py +5 -2
- shotgun/llm_proxy/clients.py +12 -7
- shotgun/logging_config.py +8 -10
- shotgun/main.py +70 -10
- shotgun/posthog_telemetry.py +9 -3
- shotgun/prompts/agents/export.j2 +18 -1
- shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +5 -1
- shotgun/prompts/agents/partials/interactive_mode.j2 +24 -7
- shotgun/prompts/agents/plan.j2 +1 -1
- shotgun/prompts/agents/research.j2 +1 -1
- shotgun/prompts/agents/specify.j2 +270 -3
- shotgun/prompts/agents/state/system_state.j2 +4 -0
- shotgun/prompts/agents/tasks.j2 +1 -1
- shotgun/prompts/loader.py +2 -2
- shotgun/prompts/tools/web_search.j2 +14 -0
- shotgun/sentry_telemetry.py +4 -15
- shotgun/settings.py +238 -0
- shotgun/telemetry.py +15 -32
- shotgun/tui/app.py +203 -9
- shotgun/tui/commands/__init__.py +1 -1
- shotgun/tui/components/context_indicator.py +136 -0
- shotgun/tui/components/mode_indicator.py +70 -0
- shotgun/tui/components/status_bar.py +48 -0
- shotgun/tui/containers.py +93 -0
- shotgun/tui/dependencies.py +39 -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 +1110 -0
- shotgun/tui/screens/chat/codebase_index_prompt_screen.py +64 -0
- shotgun/tui/screens/chat/codebase_index_selection.py +12 -0
- shotgun/tui/screens/chat/help_text.py +39 -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 +68 -2
- 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 +116 -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 +151 -0
- shotgun/tui/screens/model_picker.py +30 -6
- shotgun/tui/screens/pipx_migration.py +153 -0
- shotgun/tui/screens/welcome.py +24 -5
- shotgun/tui/services/__init__.py +5 -0
- shotgun/tui/services/conversation_service.py +182 -0
- shotgun/tui/state/__init__.py +7 -0
- shotgun/tui/state/processing_state.py +185 -0
- shotgun/tui/widgets/__init__.py +5 -0
- shotgun/tui/widgets/widget_coordinator.py +247 -0
- shotgun/utils/datetime_utils.py +77 -0
- shotgun/utils/file_system_utils.py +3 -2
- shotgun/utils/update_checker.py +69 -14
- shotgun_sh-0.2.11.dev1.dist-info/METADATA +129 -0
- shotgun_sh-0.2.11.dev1.dist-info/RECORD +190 -0
- {shotgun_sh-0.2.3.dev2.dist-info → shotgun_sh-0.2.11.dev1.dist-info}/entry_points.txt +1 -0
- {shotgun_sh-0.2.3.dev2.dist-info → shotgun_sh-0.2.11.dev1.dist-info}/licenses/LICENSE +1 -1
- shotgun/agents/tools/user_interaction.py +0 -37
- shotgun/tui/screens/chat.py +0 -804
- shotgun/tui/screens/chat_screen/history.py +0 -352
- shotgun_sh-0.2.3.dev2.dist-info/METADATA +0 -467
- shotgun_sh-0.2.3.dev2.dist-info/RECORD +0 -154
- {shotgun_sh-0.2.3.dev2.dist-info → shotgun_sh-0.2.11.dev1.dist-info}/WHEEL +0 -0
shotgun/tui/screens/chat.py
DELETED
|
@@ -1,804 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
import logging
|
|
3
|
-
from dataclasses import dataclass
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
from typing import cast
|
|
6
|
-
|
|
7
|
-
from pydantic_ai import RunContext
|
|
8
|
-
from pydantic_ai.messages import (
|
|
9
|
-
ModelMessage,
|
|
10
|
-
ModelRequest,
|
|
11
|
-
ModelResponse,
|
|
12
|
-
TextPart,
|
|
13
|
-
UserPromptPart,
|
|
14
|
-
)
|
|
15
|
-
from textual import events, on, work
|
|
16
|
-
from textual.app import ComposeResult
|
|
17
|
-
from textual.command import CommandPalette
|
|
18
|
-
from textual.containers import Container, Grid
|
|
19
|
-
from textual.keys import Keys
|
|
20
|
-
from textual.reactive import reactive
|
|
21
|
-
from textual.screen import ModalScreen, Screen
|
|
22
|
-
from textual.widget import Widget
|
|
23
|
-
from textual.widgets import Button, Label, Markdown, Static
|
|
24
|
-
|
|
25
|
-
from shotgun.agents.agent_manager import (
|
|
26
|
-
AgentManager,
|
|
27
|
-
MessageHistoryUpdated,
|
|
28
|
-
PartialResponseMessage,
|
|
29
|
-
)
|
|
30
|
-
from shotgun.agents.config import get_provider_model
|
|
31
|
-
from shotgun.agents.conversation_history import (
|
|
32
|
-
ConversationHistory,
|
|
33
|
-
ConversationState,
|
|
34
|
-
)
|
|
35
|
-
from shotgun.agents.conversation_manager import ConversationManager
|
|
36
|
-
from shotgun.agents.models import (
|
|
37
|
-
AgentDeps,
|
|
38
|
-
AgentType,
|
|
39
|
-
FileOperationTracker,
|
|
40
|
-
UserQuestion,
|
|
41
|
-
)
|
|
42
|
-
from shotgun.codebase.core.manager import CodebaseAlreadyIndexedError
|
|
43
|
-
from shotgun.codebase.models import IndexProgress, ProgressPhase
|
|
44
|
-
from shotgun.posthog_telemetry import track_event
|
|
45
|
-
from shotgun.sdk.codebase import CodebaseSDK
|
|
46
|
-
from shotgun.sdk.exceptions import CodebaseNotFoundError, InvalidPathError
|
|
47
|
-
from shotgun.tui.commands import CommandHandler
|
|
48
|
-
from shotgun.tui.filtered_codebase_service import FilteredCodebaseService
|
|
49
|
-
from shotgun.tui.screens.chat_screen.hint_message import HintMessage
|
|
50
|
-
from shotgun.tui.screens.chat_screen.history import ChatHistory
|
|
51
|
-
from shotgun.utils import get_shotgun_home
|
|
52
|
-
|
|
53
|
-
from ..components.prompt_input import PromptInput
|
|
54
|
-
from ..components.spinner import Spinner
|
|
55
|
-
from ..utils.mode_progress import PlaceholderHints
|
|
56
|
-
from .chat_screen.command_providers import (
|
|
57
|
-
DeleteCodebasePaletteProvider,
|
|
58
|
-
UnifiedCommandProvider,
|
|
59
|
-
)
|
|
60
|
-
|
|
61
|
-
logger = logging.getLogger(__name__)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
class PromptHistory:
|
|
65
|
-
def __init__(self) -> None:
|
|
66
|
-
self.prompts: list[str] = ["Hello there!"]
|
|
67
|
-
self.curr: int | None = None
|
|
68
|
-
|
|
69
|
-
def next(self) -> str:
|
|
70
|
-
if self.curr is None:
|
|
71
|
-
self.curr = -1
|
|
72
|
-
else:
|
|
73
|
-
self.curr = -1
|
|
74
|
-
return self.prompts[self.curr]
|
|
75
|
-
|
|
76
|
-
def prev(self) -> str:
|
|
77
|
-
if self.curr is None:
|
|
78
|
-
raise Exception("current entry is none")
|
|
79
|
-
if self.curr == -1:
|
|
80
|
-
self.curr = None
|
|
81
|
-
return ""
|
|
82
|
-
self.curr += 1
|
|
83
|
-
return ""
|
|
84
|
-
|
|
85
|
-
def append(self, text: str) -> None:
|
|
86
|
-
self.prompts.append(text)
|
|
87
|
-
self.curr = None
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
@dataclass
|
|
91
|
-
class CodebaseIndexSelection:
|
|
92
|
-
"""User-selected repository path and name for indexing."""
|
|
93
|
-
|
|
94
|
-
repo_path: Path
|
|
95
|
-
name: str
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
class StatusBar(Widget):
|
|
99
|
-
DEFAULT_CSS = """
|
|
100
|
-
StatusBar {
|
|
101
|
-
text-wrap: wrap;
|
|
102
|
-
padding-left: 1;
|
|
103
|
-
}
|
|
104
|
-
"""
|
|
105
|
-
|
|
106
|
-
def __init__(self, working: bool = False) -> None:
|
|
107
|
-
"""Initialize the status bar.
|
|
108
|
-
|
|
109
|
-
Args:
|
|
110
|
-
working: Whether an agent is currently working.
|
|
111
|
-
"""
|
|
112
|
-
super().__init__()
|
|
113
|
-
self.working = working
|
|
114
|
-
|
|
115
|
-
def render(self) -> str:
|
|
116
|
-
if self.working:
|
|
117
|
-
return """[$foreground-muted][bold $text]esc[/] to stop • [bold $text]enter[/] to send • [bold $text]ctrl+j[/] for newline • [bold $text]ctrl+p[/] command palette • [bold $text]shift+tab[/] cycle modes • /help for commands[/]"""
|
|
118
|
-
else:
|
|
119
|
-
return """[$foreground-muted][bold $text]enter[/] to send • [bold $text]ctrl+j[/] for newline • [bold $text]ctrl+p[/] command palette • [bold $text]shift+tab[/] cycle modes • /help for commands[/]"""
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
class ModeIndicator(Widget):
|
|
123
|
-
"""Widget to display the current agent mode."""
|
|
124
|
-
|
|
125
|
-
DEFAULT_CSS = """
|
|
126
|
-
ModeIndicator {
|
|
127
|
-
text-wrap: wrap;
|
|
128
|
-
padding-left: 1;
|
|
129
|
-
}
|
|
130
|
-
"""
|
|
131
|
-
|
|
132
|
-
def __init__(self, mode: AgentType) -> None:
|
|
133
|
-
"""Initialize the mode indicator.
|
|
134
|
-
|
|
135
|
-
Args:
|
|
136
|
-
mode: The current agent type/mode.
|
|
137
|
-
"""
|
|
138
|
-
super().__init__()
|
|
139
|
-
self.mode = mode
|
|
140
|
-
self.progress_checker = PlaceholderHints().progress_checker
|
|
141
|
-
|
|
142
|
-
def render(self) -> str:
|
|
143
|
-
"""Render the mode indicator."""
|
|
144
|
-
mode_display = {
|
|
145
|
-
AgentType.RESEARCH: "Research",
|
|
146
|
-
AgentType.PLAN: "Planning",
|
|
147
|
-
AgentType.TASKS: "Tasks",
|
|
148
|
-
AgentType.SPECIFY: "Specify",
|
|
149
|
-
AgentType.EXPORT: "Export",
|
|
150
|
-
}
|
|
151
|
-
mode_description = {
|
|
152
|
-
AgentType.RESEARCH: "Research topics with web search and synthesize findings",
|
|
153
|
-
AgentType.PLAN: "Create comprehensive, actionable plans with milestones",
|
|
154
|
-
AgentType.TASKS: "Generate specific, actionable tasks from research and plans",
|
|
155
|
-
AgentType.SPECIFY: "Create detailed specifications and requirements documents",
|
|
156
|
-
AgentType.EXPORT: "Export artifacts and findings to various formats",
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
mode_title = mode_display.get(self.mode, self.mode.value.title())
|
|
160
|
-
description = mode_description.get(self.mode, "")
|
|
161
|
-
|
|
162
|
-
# Check if mode has content
|
|
163
|
-
has_content = self.progress_checker.has_mode_content(self.mode)
|
|
164
|
-
status_icon = " ✓" if has_content else ""
|
|
165
|
-
|
|
166
|
-
return f"[bold $text-accent]{mode_title}{status_icon} mode[/][$foreground-muted] ({description})[/]"
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
class CodebaseIndexPromptScreen(ModalScreen[bool]):
|
|
170
|
-
"""Modal dialog asking whether to index the detected codebase."""
|
|
171
|
-
|
|
172
|
-
DEFAULT_CSS = """
|
|
173
|
-
CodebaseIndexPromptScreen {
|
|
174
|
-
align: center middle;
|
|
175
|
-
background: rgba(0, 0, 0, 0.0);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
CodebaseIndexPromptScreen > #index-prompt-dialog {
|
|
179
|
-
width: 60%;
|
|
180
|
-
max-width: 60;
|
|
181
|
-
height: auto;
|
|
182
|
-
border: wide $primary;
|
|
183
|
-
padding: 1 2;
|
|
184
|
-
layout: vertical;
|
|
185
|
-
background: $surface;
|
|
186
|
-
height: auto;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
#index-prompt-buttons {
|
|
190
|
-
layout: horizontal;
|
|
191
|
-
align-horizontal: right;
|
|
192
|
-
height: auto;
|
|
193
|
-
}
|
|
194
|
-
"""
|
|
195
|
-
|
|
196
|
-
def compose(self) -> ComposeResult:
|
|
197
|
-
with Container(id="index-prompt-dialog"):
|
|
198
|
-
yield Label("Index this codebase?", id="index-prompt-title")
|
|
199
|
-
yield Static(
|
|
200
|
-
f"Would you like to index the codebase at:\n{Path.cwd()}\n\n"
|
|
201
|
-
"This is required for the agent to understand your code and answer "
|
|
202
|
-
"questions about it. Without indexing, the agent cannot analyze your codebase."
|
|
203
|
-
)
|
|
204
|
-
with Container(id="index-prompt-buttons"):
|
|
205
|
-
yield Button(
|
|
206
|
-
"Index now",
|
|
207
|
-
id="index-prompt-confirm",
|
|
208
|
-
variant="primary",
|
|
209
|
-
)
|
|
210
|
-
yield Button("Not now", id="index-prompt-cancel")
|
|
211
|
-
|
|
212
|
-
@on(Button.Pressed, "#index-prompt-cancel")
|
|
213
|
-
def handle_cancel(self, event: Button.Pressed) -> None:
|
|
214
|
-
event.stop()
|
|
215
|
-
self.dismiss(False)
|
|
216
|
-
|
|
217
|
-
@on(Button.Pressed, "#index-prompt-confirm")
|
|
218
|
-
def handle_confirm(self, event: Button.Pressed) -> None:
|
|
219
|
-
event.stop()
|
|
220
|
-
self.dismiss(True)
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
class ChatScreen(Screen[None]):
|
|
224
|
-
CSS_PATH = "chat.tcss"
|
|
225
|
-
|
|
226
|
-
BINDINGS = [
|
|
227
|
-
("ctrl+p", "command_palette", "Command Palette"),
|
|
228
|
-
("shift+tab", "toggle_mode", "Toggle mode"),
|
|
229
|
-
("ctrl+u", "show_usage", "Show usage"),
|
|
230
|
-
]
|
|
231
|
-
|
|
232
|
-
COMMANDS = {
|
|
233
|
-
UnifiedCommandProvider,
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
value = reactive("")
|
|
237
|
-
mode = reactive(AgentType.RESEARCH)
|
|
238
|
-
history: PromptHistory = PromptHistory()
|
|
239
|
-
messages = reactive(list[ModelMessage | HintMessage]())
|
|
240
|
-
working = reactive(False)
|
|
241
|
-
question: reactive[UserQuestion | None] = reactive(None)
|
|
242
|
-
indexing_job: reactive[CodebaseIndexSelection | None] = reactive(None)
|
|
243
|
-
partial_message: reactive[ModelMessage | None] = reactive(None)
|
|
244
|
-
_current_worker = None # Track the current running worker for cancellation
|
|
245
|
-
|
|
246
|
-
def __init__(self, continue_session: bool = False) -> None:
|
|
247
|
-
super().__init__()
|
|
248
|
-
# Get the model configuration and services
|
|
249
|
-
model_config = get_provider_model()
|
|
250
|
-
# Use filtered service in TUI to restrict access to CWD codebase only
|
|
251
|
-
storage_dir = get_shotgun_home() / "codebases"
|
|
252
|
-
codebase_service = FilteredCodebaseService(storage_dir)
|
|
253
|
-
self.codebase_sdk = CodebaseSDK()
|
|
254
|
-
|
|
255
|
-
# Create shared deps without system_prompt_fn (agents provide their own)
|
|
256
|
-
# We need a placeholder system_prompt_fn to satisfy the field requirement
|
|
257
|
-
def _placeholder_system_prompt_fn(ctx: RunContext[AgentDeps]) -> str:
|
|
258
|
-
raise RuntimeError(
|
|
259
|
-
"This should not be called - agents provide their own system_prompt_fn"
|
|
260
|
-
)
|
|
261
|
-
|
|
262
|
-
self.deps = AgentDeps(
|
|
263
|
-
interactive_mode=True,
|
|
264
|
-
is_tui_context=True,
|
|
265
|
-
llm_model=model_config,
|
|
266
|
-
codebase_service=codebase_service,
|
|
267
|
-
system_prompt_fn=_placeholder_system_prompt_fn,
|
|
268
|
-
)
|
|
269
|
-
self.agent_manager = AgentManager(deps=self.deps, initial_type=self.mode)
|
|
270
|
-
self.command_handler = CommandHandler()
|
|
271
|
-
self.placeholder_hints = PlaceholderHints()
|
|
272
|
-
self.conversation_manager = ConversationManager()
|
|
273
|
-
self.continue_session = continue_session
|
|
274
|
-
|
|
275
|
-
def on_mount(self) -> None:
|
|
276
|
-
self.query_one(PromptInput).focus(scroll_visible=True)
|
|
277
|
-
# Hide spinner initially
|
|
278
|
-
self.query_one("#spinner").display = False
|
|
279
|
-
|
|
280
|
-
# Load conversation history if --continue flag was provided
|
|
281
|
-
if self.continue_session and self.conversation_manager.exists():
|
|
282
|
-
self._load_conversation()
|
|
283
|
-
|
|
284
|
-
self.call_later(self.check_if_codebase_is_indexed)
|
|
285
|
-
# Start the question listener worker to handle ask_user interactions
|
|
286
|
-
self.call_later(self.add_question_listener)
|
|
287
|
-
|
|
288
|
-
async def on_key(self, event: events.Key) -> None:
|
|
289
|
-
"""Handle key presses for cancellation."""
|
|
290
|
-
# If escape or ctrl+c is pressed while agent is working, cancel the operation
|
|
291
|
-
if (
|
|
292
|
-
event.key in (Keys.Escape, Keys.ControlC)
|
|
293
|
-
and self.working
|
|
294
|
-
and self._current_worker
|
|
295
|
-
):
|
|
296
|
-
# Track cancellation event
|
|
297
|
-
track_event(
|
|
298
|
-
"agent_cancelled",
|
|
299
|
-
{
|
|
300
|
-
"agent_mode": self.mode.value,
|
|
301
|
-
"cancel_key": event.key,
|
|
302
|
-
},
|
|
303
|
-
)
|
|
304
|
-
|
|
305
|
-
# Cancel the running agent worker
|
|
306
|
-
self._current_worker.cancel()
|
|
307
|
-
# Show cancellation message
|
|
308
|
-
self.mount_hint("⚠️ Cancelling operation...")
|
|
309
|
-
# Re-enable the input
|
|
310
|
-
prompt_input = self.query_one(PromptInput)
|
|
311
|
-
prompt_input.focus()
|
|
312
|
-
# Prevent the event from propagating (don't quit the app)
|
|
313
|
-
event.stop()
|
|
314
|
-
|
|
315
|
-
@work
|
|
316
|
-
async def check_if_codebase_is_indexed(self) -> None:
|
|
317
|
-
cur_dir = Path.cwd().resolve()
|
|
318
|
-
is_empty = all(
|
|
319
|
-
dir.is_dir() and dir.name in ["__pycache__", ".git", ".shotgun"]
|
|
320
|
-
for dir in cur_dir.iterdir()
|
|
321
|
-
)
|
|
322
|
-
if is_empty or self.continue_session:
|
|
323
|
-
return
|
|
324
|
-
|
|
325
|
-
# Check if the current directory has any accessible codebases
|
|
326
|
-
accessible_graphs = (
|
|
327
|
-
await self.codebase_sdk.list_codebases_for_directory()
|
|
328
|
-
).graphs
|
|
329
|
-
if accessible_graphs:
|
|
330
|
-
self.mount_hint(help_text_with_codebase(already_indexed=True))
|
|
331
|
-
return
|
|
332
|
-
|
|
333
|
-
# Ask user if they want to index the current directory
|
|
334
|
-
should_index = await self.app.push_screen_wait(CodebaseIndexPromptScreen())
|
|
335
|
-
if not should_index:
|
|
336
|
-
self.mount_hint(help_text_empty_dir())
|
|
337
|
-
return
|
|
338
|
-
|
|
339
|
-
self.mount_hint(help_text_with_codebase(already_indexed=False))
|
|
340
|
-
|
|
341
|
-
# Auto-index the current directory with its name
|
|
342
|
-
cwd_name = cur_dir.name
|
|
343
|
-
selection = CodebaseIndexSelection(repo_path=cur_dir, name=cwd_name)
|
|
344
|
-
self.call_later(lambda: self.index_codebase(selection))
|
|
345
|
-
|
|
346
|
-
def watch_mode(self, new_mode: AgentType) -> None:
|
|
347
|
-
"""React to mode changes by updating the agent manager."""
|
|
348
|
-
|
|
349
|
-
if self.is_mounted:
|
|
350
|
-
self.agent_manager.set_agent(new_mode)
|
|
351
|
-
|
|
352
|
-
mode_indicator = self.query_one(ModeIndicator)
|
|
353
|
-
mode_indicator.mode = new_mode
|
|
354
|
-
mode_indicator.refresh()
|
|
355
|
-
|
|
356
|
-
prompt_input = self.query_one(PromptInput)
|
|
357
|
-
# Force new hint selection when mode changes
|
|
358
|
-
prompt_input.placeholder = self._placeholder_for_mode(
|
|
359
|
-
new_mode, force_new=True
|
|
360
|
-
)
|
|
361
|
-
prompt_input.refresh()
|
|
362
|
-
|
|
363
|
-
def watch_working(self, is_working: bool) -> None:
|
|
364
|
-
"""Show or hide the spinner based on working state."""
|
|
365
|
-
if self.is_mounted:
|
|
366
|
-
spinner = self.query_one("#spinner")
|
|
367
|
-
spinner.set_classes("" if is_working else "hidden")
|
|
368
|
-
spinner.display = is_working
|
|
369
|
-
|
|
370
|
-
# Update the status bar to show/hide "ESC to stop"
|
|
371
|
-
status_bar = self.query_one(StatusBar)
|
|
372
|
-
status_bar.working = is_working
|
|
373
|
-
status_bar.refresh()
|
|
374
|
-
|
|
375
|
-
def watch_messages(self, messages: list[ModelMessage | HintMessage]) -> None:
|
|
376
|
-
"""Update the chat history when messages change."""
|
|
377
|
-
if self.is_mounted:
|
|
378
|
-
chat_history = self.query_one(ChatHistory)
|
|
379
|
-
chat_history.update_messages(messages)
|
|
380
|
-
|
|
381
|
-
def watch_question(self, question: UserQuestion | None) -> None:
|
|
382
|
-
"""Update the question display."""
|
|
383
|
-
if self.is_mounted:
|
|
384
|
-
question_display = self.query_one("#question-display", Markdown)
|
|
385
|
-
if question:
|
|
386
|
-
question_display.update(f"Question:\n\n{question.question}")
|
|
387
|
-
question_display.display = True
|
|
388
|
-
else:
|
|
389
|
-
question_display.update("")
|
|
390
|
-
question_display.display = False
|
|
391
|
-
|
|
392
|
-
def action_toggle_mode(self) -> None:
|
|
393
|
-
modes = [
|
|
394
|
-
AgentType.RESEARCH,
|
|
395
|
-
AgentType.SPECIFY,
|
|
396
|
-
AgentType.PLAN,
|
|
397
|
-
AgentType.TASKS,
|
|
398
|
-
AgentType.EXPORT,
|
|
399
|
-
]
|
|
400
|
-
self.mode = modes[(modes.index(self.mode) + 1) % len(modes)]
|
|
401
|
-
self.agent_manager.set_agent(self.mode)
|
|
402
|
-
# whoops it actually changes focus. Let's be brutal for now
|
|
403
|
-
self.call_later(lambda: self.query_one(PromptInput).focus())
|
|
404
|
-
|
|
405
|
-
def action_show_usage(self) -> None:
|
|
406
|
-
usage_hint = self.agent_manager.get_usage_hint()
|
|
407
|
-
logger.info(f"Usage hint: {usage_hint}")
|
|
408
|
-
if usage_hint:
|
|
409
|
-
self.mount_hint(usage_hint)
|
|
410
|
-
else:
|
|
411
|
-
self.notify("No usage hint available", severity="error")
|
|
412
|
-
|
|
413
|
-
@work
|
|
414
|
-
async def add_question_listener(self) -> None:
|
|
415
|
-
while True:
|
|
416
|
-
question = await self.deps.queue.get()
|
|
417
|
-
self.question = question
|
|
418
|
-
await question.result
|
|
419
|
-
self.deps.queue.task_done()
|
|
420
|
-
|
|
421
|
-
def compose(self) -> ComposeResult:
|
|
422
|
-
"""Create child widgets for the app."""
|
|
423
|
-
with Container(id="window"):
|
|
424
|
-
yield self.agent_manager
|
|
425
|
-
yield ChatHistory()
|
|
426
|
-
yield Markdown(markdown="", id="question-display")
|
|
427
|
-
with Container(id="footer"):
|
|
428
|
-
yield Spinner(
|
|
429
|
-
text="Processing...",
|
|
430
|
-
id="spinner",
|
|
431
|
-
classes="" if self.working else "hidden",
|
|
432
|
-
)
|
|
433
|
-
yield StatusBar(working=self.working)
|
|
434
|
-
yield PromptInput(
|
|
435
|
-
text=self.value,
|
|
436
|
-
highlight_cursor_line=False,
|
|
437
|
-
id="prompt-input",
|
|
438
|
-
placeholder=self._placeholder_for_mode(self.mode),
|
|
439
|
-
)
|
|
440
|
-
with Grid():
|
|
441
|
-
yield ModeIndicator(mode=self.mode)
|
|
442
|
-
yield Static("", id="indexing-job-display")
|
|
443
|
-
|
|
444
|
-
def mount_hint(self, markdown: str) -> None:
|
|
445
|
-
hint = HintMessage(message=markdown)
|
|
446
|
-
self.agent_manager.add_hint_message(hint)
|
|
447
|
-
|
|
448
|
-
@on(PartialResponseMessage)
|
|
449
|
-
def handle_partial_response(self, event: PartialResponseMessage) -> None:
|
|
450
|
-
self.partial_message = event.message
|
|
451
|
-
history = self.query_one(ChatHistory)
|
|
452
|
-
|
|
453
|
-
# Only update messages if the message list changed
|
|
454
|
-
new_message_list = self.messages + cast(
|
|
455
|
-
list[ModelMessage | HintMessage], event.messages
|
|
456
|
-
)
|
|
457
|
-
if len(new_message_list) != len(history.items):
|
|
458
|
-
history.update_messages(new_message_list)
|
|
459
|
-
|
|
460
|
-
# Always update the partial response (reactive property handles the update)
|
|
461
|
-
history.partial_response = self.partial_message
|
|
462
|
-
|
|
463
|
-
def _clear_partial_response(self) -> None:
|
|
464
|
-
partial_response_widget = self.query_one(ChatHistory)
|
|
465
|
-
partial_response_widget.partial_response = None
|
|
466
|
-
|
|
467
|
-
@on(MessageHistoryUpdated)
|
|
468
|
-
def handle_message_history_updated(self, event: MessageHistoryUpdated) -> None:
|
|
469
|
-
"""Handle message history updates from the agent manager."""
|
|
470
|
-
self._clear_partial_response()
|
|
471
|
-
self.messages = event.messages
|
|
472
|
-
|
|
473
|
-
# Refresh placeholder and mode indicator in case artifacts were created
|
|
474
|
-
prompt_input = self.query_one(PromptInput)
|
|
475
|
-
prompt_input.placeholder = self._placeholder_for_mode(self.mode)
|
|
476
|
-
prompt_input.refresh()
|
|
477
|
-
|
|
478
|
-
mode_indicator = self.query_one(ModeIndicator)
|
|
479
|
-
mode_indicator.refresh()
|
|
480
|
-
|
|
481
|
-
# If there are file operations, add a message showing the modified files
|
|
482
|
-
if event.file_operations:
|
|
483
|
-
chat_history = self.query_one(ChatHistory)
|
|
484
|
-
if chat_history.vertical_tail:
|
|
485
|
-
tracker = FileOperationTracker(operations=event.file_operations)
|
|
486
|
-
display_path = tracker.get_display_path()
|
|
487
|
-
|
|
488
|
-
if display_path:
|
|
489
|
-
# Create a simple markdown message with the file path
|
|
490
|
-
# The terminal emulator will make this clickable automatically
|
|
491
|
-
from pathlib import Path
|
|
492
|
-
|
|
493
|
-
path_obj = Path(display_path)
|
|
494
|
-
|
|
495
|
-
if len(event.file_operations) == 1:
|
|
496
|
-
message = f"📝 Modified: `{display_path}`"
|
|
497
|
-
else:
|
|
498
|
-
num_files = len({op.file_path for op in event.file_operations})
|
|
499
|
-
if path_obj.is_dir():
|
|
500
|
-
message = (
|
|
501
|
-
f"📁 Modified {num_files} files in: `{display_path}`"
|
|
502
|
-
)
|
|
503
|
-
else:
|
|
504
|
-
# Common path is a file, show parent directory
|
|
505
|
-
message = (
|
|
506
|
-
f"📁 Modified {num_files} files in: `{path_obj.parent}`"
|
|
507
|
-
)
|
|
508
|
-
|
|
509
|
-
self.mount_hint(message)
|
|
510
|
-
|
|
511
|
-
@on(PromptInput.Submitted)
|
|
512
|
-
async def handle_submit(self, message: PromptInput.Submitted) -> None:
|
|
513
|
-
text = message.text.strip()
|
|
514
|
-
|
|
515
|
-
# If empty text, just clear input and return
|
|
516
|
-
if not text:
|
|
517
|
-
prompt_input = self.query_one(PromptInput)
|
|
518
|
-
prompt_input.clear()
|
|
519
|
-
self.value = ""
|
|
520
|
-
return
|
|
521
|
-
|
|
522
|
-
# Check if it's a command
|
|
523
|
-
if self.command_handler.is_command(text):
|
|
524
|
-
success, response = self.command_handler.handle_command(text)
|
|
525
|
-
|
|
526
|
-
# Add the command to history
|
|
527
|
-
self.history.append(message.text)
|
|
528
|
-
|
|
529
|
-
# Display the command in chat history
|
|
530
|
-
user_message = ModelRequest(parts=[UserPromptPart(content=text)])
|
|
531
|
-
self.messages = self.messages + [user_message]
|
|
532
|
-
|
|
533
|
-
# Display the response (help text or error message)
|
|
534
|
-
response_message = ModelResponse(parts=[TextPart(content=response)])
|
|
535
|
-
self.messages = self.messages + [response_message]
|
|
536
|
-
|
|
537
|
-
# Clear the input
|
|
538
|
-
prompt_input = self.query_one(PromptInput)
|
|
539
|
-
prompt_input.clear()
|
|
540
|
-
self.value = ""
|
|
541
|
-
return
|
|
542
|
-
|
|
543
|
-
# Not a command, process as normal
|
|
544
|
-
self.history.append(message.text)
|
|
545
|
-
|
|
546
|
-
# Clear the input
|
|
547
|
-
self.value = ""
|
|
548
|
-
self.run_agent(text) # Use stripped text
|
|
549
|
-
|
|
550
|
-
prompt_input = self.query_one(PromptInput)
|
|
551
|
-
prompt_input.clear()
|
|
552
|
-
|
|
553
|
-
def _placeholder_for_mode(self, mode: AgentType, force_new: bool = False) -> str:
|
|
554
|
-
"""Return the placeholder text appropriate for the current mode.
|
|
555
|
-
|
|
556
|
-
Args:
|
|
557
|
-
mode: The current agent mode.
|
|
558
|
-
force_new: If True, force selection of a new random hint.
|
|
559
|
-
|
|
560
|
-
Returns:
|
|
561
|
-
Dynamic placeholder hint based on mode and progress.
|
|
562
|
-
"""
|
|
563
|
-
return self.placeholder_hints.get_placeholder_for_mode(mode)
|
|
564
|
-
|
|
565
|
-
def index_codebase_command(self) -> None:
|
|
566
|
-
# Simplified: always index current working directory with its name
|
|
567
|
-
cur_dir = Path.cwd().resolve()
|
|
568
|
-
cwd_name = cur_dir.name
|
|
569
|
-
selection = CodebaseIndexSelection(repo_path=cur_dir, name=cwd_name)
|
|
570
|
-
self.call_later(lambda: self.index_codebase(selection))
|
|
571
|
-
|
|
572
|
-
def delete_codebase_command(self) -> None:
|
|
573
|
-
self.app.push_screen(
|
|
574
|
-
CommandPalette(
|
|
575
|
-
providers=[DeleteCodebasePaletteProvider],
|
|
576
|
-
placeholder="Select a codebase to delete…",
|
|
577
|
-
)
|
|
578
|
-
)
|
|
579
|
-
|
|
580
|
-
def delete_codebase_from_palette(self, graph_id: str) -> None:
|
|
581
|
-
stack = getattr(self.app, "screen_stack", None)
|
|
582
|
-
if stack and isinstance(stack[-1], CommandPalette):
|
|
583
|
-
self.app.pop_screen()
|
|
584
|
-
|
|
585
|
-
self.call_later(lambda: self.delete_codebase(graph_id))
|
|
586
|
-
|
|
587
|
-
@work
|
|
588
|
-
async def delete_codebase(self, graph_id: str) -> None:
|
|
589
|
-
try:
|
|
590
|
-
await self.codebase_sdk.delete_codebase(graph_id)
|
|
591
|
-
self.notify(f"Deleted codebase: {graph_id}", severity="information")
|
|
592
|
-
except CodebaseNotFoundError as exc:
|
|
593
|
-
self.notify(str(exc), severity="error")
|
|
594
|
-
except Exception as exc: # pragma: no cover - defensive UI path
|
|
595
|
-
self.notify(f"Failed to delete codebase: {exc}", severity="error")
|
|
596
|
-
|
|
597
|
-
@work
|
|
598
|
-
async def index_codebase(self, selection: CodebaseIndexSelection) -> None:
|
|
599
|
-
label = self.query_one("#indexing-job-display", Static)
|
|
600
|
-
label.update(
|
|
601
|
-
f"[$foreground-muted]Indexing codebase: [bold $text-accent]{selection.name}[/][/]"
|
|
602
|
-
)
|
|
603
|
-
label.refresh()
|
|
604
|
-
|
|
605
|
-
def create_progress_bar(percentage: float, width: int = 20) -> str:
|
|
606
|
-
"""Create a visual progress bar using Unicode block characters."""
|
|
607
|
-
filled = int((percentage / 100) * width)
|
|
608
|
-
empty = width - filled
|
|
609
|
-
return "▓" * filled + "░" * empty
|
|
610
|
-
|
|
611
|
-
# Spinner animation frames
|
|
612
|
-
spinner_frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
|
|
613
|
-
|
|
614
|
-
# Progress state (shared between timer and progress callback)
|
|
615
|
-
progress_state: dict[str, int | float] = {
|
|
616
|
-
"frame_index": 0,
|
|
617
|
-
"percentage": 0.0,
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
def update_progress_display() -> None:
|
|
621
|
-
"""Update progress bar on timer - runs every 100ms."""
|
|
622
|
-
# Advance spinner frame
|
|
623
|
-
frame_idx = int(progress_state["frame_index"])
|
|
624
|
-
progress_state["frame_index"] = (frame_idx + 1) % len(spinner_frames)
|
|
625
|
-
spinner = spinner_frames[frame_idx]
|
|
626
|
-
|
|
627
|
-
# Get current state
|
|
628
|
-
pct = float(progress_state["percentage"])
|
|
629
|
-
bar = create_progress_bar(pct)
|
|
630
|
-
|
|
631
|
-
# Update label
|
|
632
|
-
label.update(
|
|
633
|
-
f"[$foreground-muted]Indexing codebase: {spinner} {bar} {pct:.0f}%[/]"
|
|
634
|
-
)
|
|
635
|
-
|
|
636
|
-
def progress_callback(progress_info: IndexProgress) -> None:
|
|
637
|
-
"""Update progress state (timer renders it independently)."""
|
|
638
|
-
# Calculate overall percentage (0-95%, reserve 95-100% for finalization)
|
|
639
|
-
if progress_info.phase == ProgressPhase.STRUCTURE:
|
|
640
|
-
# Phase 1: 0-10%, always show 5% while running, 10% when complete
|
|
641
|
-
overall_pct = 10.0 if progress_info.phase_complete else 5.0
|
|
642
|
-
elif progress_info.phase == ProgressPhase.DEFINITIONS:
|
|
643
|
-
# Phase 2: 10-80% based on files processed
|
|
644
|
-
if progress_info.total and progress_info.total > 0:
|
|
645
|
-
phase_pct = (progress_info.current / progress_info.total) * 70.0
|
|
646
|
-
overall_pct = 10.0 + phase_pct
|
|
647
|
-
else:
|
|
648
|
-
overall_pct = 10.0
|
|
649
|
-
elif progress_info.phase == ProgressPhase.RELATIONSHIPS:
|
|
650
|
-
# Phase 3: 80-95% based on relationships processed (cap at 95%)
|
|
651
|
-
if progress_info.total and progress_info.total > 0:
|
|
652
|
-
phase_pct = (progress_info.current / progress_info.total) * 15.0
|
|
653
|
-
overall_pct = 80.0 + phase_pct
|
|
654
|
-
else:
|
|
655
|
-
overall_pct = 80.0
|
|
656
|
-
else:
|
|
657
|
-
overall_pct = 0.0
|
|
658
|
-
|
|
659
|
-
# Update shared state (timer will render it)
|
|
660
|
-
progress_state["percentage"] = overall_pct
|
|
661
|
-
|
|
662
|
-
# Start progress animation timer (10 fps = 100ms interval)
|
|
663
|
-
progress_timer = self.set_interval(0.1, update_progress_display)
|
|
664
|
-
|
|
665
|
-
try:
|
|
666
|
-
# Pass the current working directory as the indexed_from_cwd
|
|
667
|
-
logger.debug(
|
|
668
|
-
f"Starting indexing - repo_path: {selection.repo_path}, "
|
|
669
|
-
f"name: {selection.name}, cwd: {Path.cwd().resolve()}"
|
|
670
|
-
)
|
|
671
|
-
result = await self.codebase_sdk.index_codebase(
|
|
672
|
-
selection.repo_path,
|
|
673
|
-
selection.name,
|
|
674
|
-
indexed_from_cwd=str(Path.cwd().resolve()),
|
|
675
|
-
progress_callback=progress_callback,
|
|
676
|
-
)
|
|
677
|
-
|
|
678
|
-
# Stop progress animation
|
|
679
|
-
progress_timer.stop()
|
|
680
|
-
|
|
681
|
-
# Show 100% completion after indexing finishes
|
|
682
|
-
final_bar = create_progress_bar(100.0)
|
|
683
|
-
label.update(f"[$foreground-muted]Indexing codebase: {final_bar} 100%[/]")
|
|
684
|
-
label.refresh()
|
|
685
|
-
|
|
686
|
-
logger.info(
|
|
687
|
-
f"Successfully indexed codebase '{result.name}' (ID: {result.graph_id})"
|
|
688
|
-
)
|
|
689
|
-
self.notify(
|
|
690
|
-
f"Indexed codebase '{result.name}' (ID: {result.graph_id})",
|
|
691
|
-
severity="information",
|
|
692
|
-
timeout=8,
|
|
693
|
-
)
|
|
694
|
-
|
|
695
|
-
except CodebaseAlreadyIndexedError as exc:
|
|
696
|
-
progress_timer.stop()
|
|
697
|
-
logger.warning(f"Codebase already indexed: {exc}")
|
|
698
|
-
self.notify(str(exc), severity="warning")
|
|
699
|
-
return
|
|
700
|
-
except InvalidPathError as exc:
|
|
701
|
-
progress_timer.stop()
|
|
702
|
-
logger.error(f"Invalid path error: {exc}")
|
|
703
|
-
self.notify(str(exc), severity="error")
|
|
704
|
-
|
|
705
|
-
except Exception as exc: # pragma: no cover - defensive UI path
|
|
706
|
-
# Log full exception details with stack trace
|
|
707
|
-
logger.exception(
|
|
708
|
-
f"Failed to index codebase - repo_path: {selection.repo_path}, "
|
|
709
|
-
f"name: {selection.name}, error: {exc}"
|
|
710
|
-
)
|
|
711
|
-
self.notify(f"Failed to index codebase: {exc}", severity="error")
|
|
712
|
-
finally:
|
|
713
|
-
# Always stop the progress timer
|
|
714
|
-
progress_timer.stop()
|
|
715
|
-
label.update("")
|
|
716
|
-
label.refresh()
|
|
717
|
-
|
|
718
|
-
@work
|
|
719
|
-
async def run_agent(self, message: str) -> None:
|
|
720
|
-
prompt = None
|
|
721
|
-
self.working = True
|
|
722
|
-
|
|
723
|
-
# Store the worker so we can cancel it if needed
|
|
724
|
-
from textual.worker import get_current_worker
|
|
725
|
-
|
|
726
|
-
self._current_worker = get_current_worker()
|
|
727
|
-
|
|
728
|
-
prompt = message
|
|
729
|
-
|
|
730
|
-
try:
|
|
731
|
-
await self.agent_manager.run(
|
|
732
|
-
prompt=prompt,
|
|
733
|
-
)
|
|
734
|
-
except asyncio.CancelledError:
|
|
735
|
-
# Handle cancellation gracefully - DO NOT re-raise
|
|
736
|
-
self.mount_hint("⚠️ Operation cancelled by user")
|
|
737
|
-
finally:
|
|
738
|
-
self.working = False
|
|
739
|
-
self._current_worker = None
|
|
740
|
-
|
|
741
|
-
# Save conversation after each interaction
|
|
742
|
-
self._save_conversation()
|
|
743
|
-
|
|
744
|
-
prompt_input = self.query_one(PromptInput)
|
|
745
|
-
prompt_input.focus()
|
|
746
|
-
|
|
747
|
-
def _save_conversation(self) -> None:
|
|
748
|
-
"""Save the current conversation to persistent storage."""
|
|
749
|
-
# Get conversation state from agent manager
|
|
750
|
-
state = self.agent_manager.get_conversation_state()
|
|
751
|
-
|
|
752
|
-
# Create conversation history object
|
|
753
|
-
conversation = ConversationHistory(
|
|
754
|
-
last_agent_model=state.agent_type,
|
|
755
|
-
)
|
|
756
|
-
conversation.set_agent_messages(state.agent_messages)
|
|
757
|
-
conversation.set_ui_messages(state.ui_messages)
|
|
758
|
-
|
|
759
|
-
# Save to file
|
|
760
|
-
self.conversation_manager.save(conversation)
|
|
761
|
-
|
|
762
|
-
def _load_conversation(self) -> None:
|
|
763
|
-
"""Load conversation from persistent storage."""
|
|
764
|
-
conversation = self.conversation_manager.load()
|
|
765
|
-
if conversation is None:
|
|
766
|
-
return
|
|
767
|
-
|
|
768
|
-
# Restore agent state
|
|
769
|
-
agent_messages = conversation.get_agent_messages()
|
|
770
|
-
ui_messages = conversation.get_ui_messages()
|
|
771
|
-
|
|
772
|
-
# Create ConversationState for restoration
|
|
773
|
-
state = ConversationState(
|
|
774
|
-
agent_messages=agent_messages,
|
|
775
|
-
ui_messages=ui_messages,
|
|
776
|
-
agent_type=conversation.last_agent_model,
|
|
777
|
-
)
|
|
778
|
-
|
|
779
|
-
self.agent_manager.restore_conversation_state(state)
|
|
780
|
-
|
|
781
|
-
# Update the current mode
|
|
782
|
-
self.mode = AgentType(conversation.last_agent_model)
|
|
783
|
-
self.deps.usage_manager.restore_usage_state()
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
def help_text_with_codebase(already_indexed: bool = False) -> str:
|
|
787
|
-
return (
|
|
788
|
-
"Howdy! Welcome to Shotgun - the context tool for software engineering. \n\nYou can research, build specs, plan, create tasks, and export context to your favorite code-gen agents.\n\n"
|
|
789
|
-
f"{'' if already_indexed else 'Once your codebase is indexed, '}I can help with:\n\n"
|
|
790
|
-
"- Speccing out a new feature\n"
|
|
791
|
-
"- Onboarding you onto this project\n"
|
|
792
|
-
"- Helping with a refactor spec\n"
|
|
793
|
-
"- Creating AGENTS.md file for this project\n"
|
|
794
|
-
)
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
def help_text_empty_dir() -> str:
|
|
798
|
-
return (
|
|
799
|
-
"Howdy! Welcome to Shotgun - the context tool for software engineering.\n\nYou can research, build specs, plan, create tasks, and export context to your favorite code-gen agents.\n\n"
|
|
800
|
-
"What would you like to build? Here are some examples:\n\n"
|
|
801
|
-
"- Research FastAPI vs Django\n"
|
|
802
|
-
"- Plan my new web app using React\n"
|
|
803
|
-
"- Create PRD for my planned product\n"
|
|
804
|
-
)
|