shotgun-sh 0.1.0.dev1__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (94) hide show
  1. shotgun/__init__.py +3 -0
  2. shotgun/agents/__init__.py +1 -0
  3. shotgun/agents/agent_manager.py +196 -0
  4. shotgun/agents/common.py +295 -0
  5. shotgun/agents/config/__init__.py +13 -0
  6. shotgun/agents/config/manager.py +215 -0
  7. shotgun/agents/config/models.py +120 -0
  8. shotgun/agents/config/provider.py +91 -0
  9. shotgun/agents/history/__init__.py +5 -0
  10. shotgun/agents/history/history_processors.py +213 -0
  11. shotgun/agents/models.py +94 -0
  12. shotgun/agents/plan.py +119 -0
  13. shotgun/agents/research.py +131 -0
  14. shotgun/agents/tasks.py +122 -0
  15. shotgun/agents/tools/__init__.py +26 -0
  16. shotgun/agents/tools/codebase/__init__.py +28 -0
  17. shotgun/agents/tools/codebase/codebase_shell.py +256 -0
  18. shotgun/agents/tools/codebase/directory_lister.py +141 -0
  19. shotgun/agents/tools/codebase/file_read.py +144 -0
  20. shotgun/agents/tools/codebase/models.py +252 -0
  21. shotgun/agents/tools/codebase/query_graph.py +67 -0
  22. shotgun/agents/tools/codebase/retrieve_code.py +81 -0
  23. shotgun/agents/tools/file_management.py +130 -0
  24. shotgun/agents/tools/user_interaction.py +36 -0
  25. shotgun/agents/tools/web_search.py +69 -0
  26. shotgun/cli/__init__.py +1 -0
  27. shotgun/cli/codebase/__init__.py +5 -0
  28. shotgun/cli/codebase/commands.py +202 -0
  29. shotgun/cli/codebase/models.py +21 -0
  30. shotgun/cli/config.py +261 -0
  31. shotgun/cli/models.py +10 -0
  32. shotgun/cli/plan.py +65 -0
  33. shotgun/cli/research.py +78 -0
  34. shotgun/cli/tasks.py +71 -0
  35. shotgun/cli/utils.py +25 -0
  36. shotgun/codebase/__init__.py +12 -0
  37. shotgun/codebase/core/__init__.py +46 -0
  38. shotgun/codebase/core/change_detector.py +358 -0
  39. shotgun/codebase/core/code_retrieval.py +243 -0
  40. shotgun/codebase/core/ingestor.py +1497 -0
  41. shotgun/codebase/core/language_config.py +297 -0
  42. shotgun/codebase/core/manager.py +1554 -0
  43. shotgun/codebase/core/nl_query.py +327 -0
  44. shotgun/codebase/core/parser_loader.py +152 -0
  45. shotgun/codebase/models.py +107 -0
  46. shotgun/codebase/service.py +148 -0
  47. shotgun/logging_config.py +172 -0
  48. shotgun/main.py +73 -0
  49. shotgun/prompts/__init__.py +5 -0
  50. shotgun/prompts/agents/__init__.py +1 -0
  51. shotgun/prompts/agents/partials/codebase_understanding.j2 +79 -0
  52. shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +10 -0
  53. shotgun/prompts/agents/partials/interactive_mode.j2 +8 -0
  54. shotgun/prompts/agents/plan.j2 +57 -0
  55. shotgun/prompts/agents/research.j2 +38 -0
  56. shotgun/prompts/agents/state/codebase/codebase_graphs_available.j2 +13 -0
  57. shotgun/prompts/agents/state/system_state.j2 +1 -0
  58. shotgun/prompts/agents/tasks.j2 +67 -0
  59. shotgun/prompts/codebase/__init__.py +1 -0
  60. shotgun/prompts/codebase/cypher_query_patterns.j2 +221 -0
  61. shotgun/prompts/codebase/cypher_system.j2 +28 -0
  62. shotgun/prompts/codebase/enhanced_query_context.j2 +10 -0
  63. shotgun/prompts/codebase/partials/cypher_rules.j2 +24 -0
  64. shotgun/prompts/codebase/partials/graph_schema.j2 +28 -0
  65. shotgun/prompts/codebase/partials/temporal_context.j2 +21 -0
  66. shotgun/prompts/history/__init__.py +1 -0
  67. shotgun/prompts/history/summarization.j2 +46 -0
  68. shotgun/prompts/loader.py +140 -0
  69. shotgun/prompts/user/research.j2 +5 -0
  70. shotgun/py.typed +0 -0
  71. shotgun/sdk/__init__.py +13 -0
  72. shotgun/sdk/codebase.py +195 -0
  73. shotgun/sdk/exceptions.py +17 -0
  74. shotgun/sdk/models.py +189 -0
  75. shotgun/sdk/services.py +23 -0
  76. shotgun/telemetry.py +68 -0
  77. shotgun/tui/__init__.py +0 -0
  78. shotgun/tui/app.py +49 -0
  79. shotgun/tui/components/prompt_input.py +69 -0
  80. shotgun/tui/components/spinner.py +86 -0
  81. shotgun/tui/components/splash.py +25 -0
  82. shotgun/tui/components/vertical_tail.py +28 -0
  83. shotgun/tui/screens/chat.py +415 -0
  84. shotgun/tui/screens/chat.tcss +28 -0
  85. shotgun/tui/screens/provider_config.py +221 -0
  86. shotgun/tui/screens/splash.py +31 -0
  87. shotgun/tui/styles.tcss +10 -0
  88. shotgun/utils/__init__.py +5 -0
  89. shotgun/utils/file_system_utils.py +31 -0
  90. shotgun_sh-0.1.0.dev1.dist-info/METADATA +318 -0
  91. shotgun_sh-0.1.0.dev1.dist-info/RECORD +94 -0
  92. shotgun_sh-0.1.0.dev1.dist-info/WHEEL +4 -0
  93. shotgun_sh-0.1.0.dev1.dist-info/entry_points.txt +3 -0
  94. shotgun_sh-0.1.0.dev1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,25 @@
1
+ from textual.app import RenderResult
2
+ from textual.widgets import Static
3
+
4
+ ART = """
5
+
6
+ ███████╗██╗ ██╗ ██████╗ ████████╗ ██████╗ ██╗ ██╗███╗ ██╗
7
+ ██╔════╝██║ ██║██╔═══██╗╚══██╔══╝██╔════╝ ██║ ██║████╗ ██║
8
+ ███████╗███████║██║ ██║ ██║ ██║ ███╗██║ ██║██╔██╗ ██║
9
+ ╚════██║██╔══██║██║ ██║ ██║ ██║ ██║██║ ██║██║╚██╗██║
10
+ ███████║██║ ██║╚██████╔╝ ██║ ╚██████╔╝╚██████╔╝██║ ╚████║
11
+ ╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═══╝
12
+
13
+ """
14
+
15
+
16
+ class SplashWidget(Static):
17
+ DEFAULT_CSS = """
18
+ SplashWidget {
19
+ text-align: center;
20
+ width: 64;
21
+ }
22
+ """
23
+
24
+ def render(self) -> RenderResult:
25
+ return ART
@@ -0,0 +1,28 @@
1
+ from textual.containers import VerticalScroll
2
+ from textual.reactive import reactive
3
+
4
+
5
+ class VerticalTail(VerticalScroll):
6
+ """A vertical scroll container that automatically scrolls to the bottom when content is added."""
7
+
8
+ auto_scroll = reactive(True, layout=False)
9
+
10
+ def on_mount(self) -> None:
11
+ """Set up auto-scrolling when the widget is mounted."""
12
+ # Start at the bottom
13
+ if self.auto_scroll:
14
+ self.scroll_end(animate=False)
15
+
16
+ def on_descendant_mount(self) -> None:
17
+ """Auto-scroll when a new child is added."""
18
+ if self.auto_scroll:
19
+ # Check if we're near the bottom (within 1 line of scroll)
20
+ at_bottom = self.scroll_y >= self.max_scroll_y - 1
21
+ if at_bottom:
22
+ # Use call_after_refresh to ensure layout is updated first
23
+ self.call_after_refresh(self.scroll_end, animate=False)
24
+
25
+ def watch_auto_scroll(self, value: bool) -> None:
26
+ """Handle auto_scroll property changes."""
27
+ if value:
28
+ self.scroll_end(animate=False)
@@ -0,0 +1,415 @@
1
+ from collections.abc import AsyncGenerator
2
+ from typing import cast
3
+
4
+ from pydantic_ai import DeferredToolResults
5
+ from pydantic_ai.messages import (
6
+ BuiltinToolCallPart,
7
+ BuiltinToolReturnPart,
8
+ ModelMessage,
9
+ ModelRequest,
10
+ ModelResponse,
11
+ TextPart,
12
+ ThinkingPart,
13
+ ToolCallPart,
14
+ )
15
+ from textual import on, work
16
+ from textual.app import ComposeResult
17
+ from textual.command import DiscoveryHit, Hit, Provider
18
+ from textual.containers import Container
19
+ from textual.reactive import reactive
20
+ from textual.screen import Screen
21
+ from textual.widget import Widget
22
+ from textual.widgets import Markdown
23
+
24
+ from shotgun.agents.agent_manager import AgentManager, AgentType, MessageHistoryUpdated
25
+ from shotgun.agents.config import get_provider_model
26
+ from shotgun.agents.models import AgentDeps, UserAnswer, UserQuestion
27
+ from shotgun.sdk.services import get_codebase_service
28
+
29
+ from ..components.prompt_input import PromptInput
30
+ from ..components.spinner import Spinner
31
+ from ..components.vertical_tail import VerticalTail
32
+
33
+
34
+ class PromptHistory:
35
+ def __init__(self) -> None:
36
+ self.prompts: list[str] = ["Hello there!"]
37
+ self.curr: int | None = None
38
+
39
+ def next(self) -> str:
40
+ if self.curr is None:
41
+ self.curr = -1
42
+ else:
43
+ self.curr = -1
44
+ return self.prompts[self.curr]
45
+
46
+ def prev(self) -> str:
47
+ if self.curr is None:
48
+ raise Exception("current entry is none")
49
+ if self.curr == -1:
50
+ self.curr = None
51
+ return ""
52
+ self.curr += 1
53
+ return ""
54
+
55
+ def append(self, text: str) -> None:
56
+ self.prompts.append(text)
57
+ self.curr = None
58
+
59
+
60
+ class ChatHistory(Widget):
61
+ DEFAULT_CSS = """
62
+ VerticalTail {
63
+ align: left bottom;
64
+
65
+ }
66
+ VerticalTail > * {
67
+ height: auto;
68
+ }
69
+
70
+ Horizontal {
71
+ height: auto;
72
+ background: $secondary-muted;
73
+ }
74
+
75
+ Markdown {
76
+ height: auto;
77
+ }
78
+ """
79
+
80
+ def __init__(self) -> None:
81
+ super().__init__()
82
+ self.items: list[ModelMessage] = []
83
+ self.vertical_tail: VerticalTail | None = None
84
+
85
+ def compose(self) -> ComposeResult:
86
+ self.vertical_tail = VerticalTail()
87
+ yield self.vertical_tail
88
+
89
+ def update_messages(self, messages: list[ModelMessage]) -> None:
90
+ """Update the displayed messages without recomposing."""
91
+ if not self.vertical_tail:
92
+ return
93
+
94
+ # Clear existing widgets
95
+ self.vertical_tail.remove_children()
96
+
97
+ # Add new message widgets
98
+ for item in messages:
99
+ if isinstance(item, ModelRequest):
100
+ self.vertical_tail.mount(UserQuestionWidget(item))
101
+ elif isinstance(item, ModelResponse):
102
+ self.vertical_tail.mount(AgentResponseWidget(item))
103
+
104
+ self.items = messages
105
+
106
+
107
+ class UserQuestionWidget(Widget):
108
+ def __init__(self, item: ModelRequest) -> None:
109
+ super().__init__()
110
+ self.item = item
111
+
112
+ def compose(self) -> ComposeResult:
113
+ prompt = "".join(str(part.content) for part in self.item.parts if part.content)
114
+ yield Markdown(markdown=f"**>** {prompt}")
115
+
116
+
117
+ class AgentResponseWidget(Widget):
118
+ def __init__(self, item: ModelResponse) -> None:
119
+ super().__init__()
120
+ self.item = item
121
+
122
+ def compose(self) -> ComposeResult:
123
+ yield Markdown(markdown=f"**⏺** {self.compute_output()}")
124
+
125
+ def compute_output(self) -> str:
126
+ acc = ""
127
+ for part in self.item.parts: # TextPart | ToolCallPart | BuiltinToolCallPart | BuiltinToolReturnPart | ThinkingPart
128
+ if isinstance(part, TextPart):
129
+ acc += part.content
130
+ elif isinstance(part, ToolCallPart):
131
+ acc += f"{part.tool_name}({part.args})\n"
132
+ elif isinstance(part, BuiltinToolCallPart):
133
+ acc += f"{part.tool_name}({part.args})\n"
134
+ elif isinstance(part, BuiltinToolReturnPart):
135
+ acc += f"{part.tool_name}()\n"
136
+ elif isinstance(part, ThinkingPart):
137
+ acc += f"Thinking: {part.content}\n"
138
+ return acc
139
+
140
+
141
+ class StatusBar(Widget):
142
+ DEFAULT_CSS = """
143
+ StatusBar {
144
+ text-wrap: wrap;
145
+ }
146
+ """
147
+
148
+ def render(self) -> str:
149
+ return """[$foreground-muted]Press [bold $text]Enter[/] to send • [bold $text]Ctrl+P[/] for command palette • /help for commands[/]"""
150
+
151
+
152
+ class ModeIndicator(Widget):
153
+ """Widget to display the current agent mode."""
154
+
155
+ DEFAULT_CSS = """
156
+ ModeIndicator {
157
+ text-wrap: wrap;
158
+ }
159
+ """
160
+
161
+ def __init__(self, mode: AgentType) -> None:
162
+ """Initialize the mode indicator.
163
+
164
+ Args:
165
+ mode: The current agent type/mode.
166
+ """
167
+ super().__init__()
168
+ self.mode = mode
169
+
170
+ def render(self) -> str:
171
+ """Render the mode indicator."""
172
+ mode_display = {
173
+ AgentType.RESEARCH: "Research",
174
+ AgentType.PLAN: "Planning",
175
+ AgentType.TASKS: "Tasks",
176
+ }
177
+ mode_description = {
178
+ AgentType.RESEARCH: "Research topics with web search and synthesize findings",
179
+ AgentType.PLAN: "Create comprehensive, actionable plans with milestones",
180
+ AgentType.TASKS: "Generate specific, actionable tasks from research and plans",
181
+ }
182
+
183
+ mode_title = mode_display.get(self.mode, self.mode.value.title())
184
+ description = mode_description.get(self.mode, "")
185
+
186
+ return f"[bold $text-accent]{mode_title} mode[/][$foreground-muted] ({description})[/]"
187
+
188
+
189
+ class AgentModeProvider(Provider):
190
+ """Command provider for agent mode switching."""
191
+
192
+ @property
193
+ def chat_screen(self) -> "ChatScreen":
194
+ return cast(ChatScreen, self.screen)
195
+
196
+ def set_mode(self, mode: AgentType) -> None:
197
+ """Switch to research mode."""
198
+ self.chat_screen.mode = mode
199
+
200
+ async def discover(self) -> AsyncGenerator[DiscoveryHit, None]:
201
+ """Provide default mode switching commands when palette opens."""
202
+ yield DiscoveryHit(
203
+ "Switch to Research Mode",
204
+ lambda: self.set_mode(AgentType.RESEARCH),
205
+ help="🔬 Research topics with web search and synthesize findings",
206
+ )
207
+ yield DiscoveryHit(
208
+ "Switch to Plan Mode",
209
+ lambda: self.set_mode(AgentType.PLAN),
210
+ help="📋 Create comprehensive, actionable plans with milestones",
211
+ )
212
+ yield DiscoveryHit(
213
+ "Switch to Tasks Mode",
214
+ lambda: self.set_mode(AgentType.TASKS),
215
+ help="✅ Generate specific, actionable tasks from research and plans",
216
+ )
217
+
218
+ async def search(self, query: str) -> AsyncGenerator[Hit, None]:
219
+ """Search for mode commands."""
220
+ matcher = self.matcher(query)
221
+
222
+ commands = [
223
+ (
224
+ "Switch to Research Mode",
225
+ "🔬 Research topics with web search and synthesize findings",
226
+ lambda: self.set_mode(AgentType.RESEARCH),
227
+ AgentType.RESEARCH,
228
+ ),
229
+ (
230
+ "Switch to Plan Mode",
231
+ "📋 Create comprehensive, actionable plans with milestones",
232
+ lambda: self.set_mode(AgentType.PLAN),
233
+ AgentType.PLAN,
234
+ ),
235
+ (
236
+ "Switch to Tasks Mode",
237
+ "✅ Generate specific, actionable tasks from research and plans",
238
+ lambda: self.set_mode(AgentType.TASKS),
239
+ AgentType.TASKS,
240
+ ),
241
+ ]
242
+
243
+ for title, help_text, callback, mode in commands:
244
+ if self.chat_screen.mode == mode:
245
+ continue
246
+ score = matcher.match(title)
247
+ if score > 0:
248
+ yield Hit(score, matcher.highlight(title), callback, help=help_text)
249
+
250
+
251
+ class ProviderSetupProvider(Provider):
252
+ """Command palette entries for provider configuration."""
253
+
254
+ @property
255
+ def chat_screen(self) -> "ChatScreen":
256
+ return cast(ChatScreen, self.screen)
257
+
258
+ def open_provider_config(self) -> None:
259
+ """Show the provider configuration screen."""
260
+ self.chat_screen.app.push_screen("provider_config")
261
+
262
+ async def discover(self) -> AsyncGenerator[DiscoveryHit, None]:
263
+ yield DiscoveryHit(
264
+ "Open Provider Setup",
265
+ self.open_provider_config,
266
+ help="⚙️ Manage API keys for available providers",
267
+ )
268
+
269
+ async def search(self, query: str) -> AsyncGenerator[Hit, None]:
270
+ matcher = self.matcher(query)
271
+ title = "Open Provider Setup"
272
+ score = matcher.match(title)
273
+ if score > 0:
274
+ yield Hit(
275
+ score,
276
+ matcher.highlight(title),
277
+ self.open_provider_config,
278
+ help="⚙️ Manage API keys for available providers",
279
+ )
280
+
281
+
282
+ class ChatScreen(Screen[None]):
283
+ CSS_PATH = "chat.tcss"
284
+
285
+ BINDINGS = [
286
+ ("ctrl+p", "command_palette", "Command Palette"),
287
+ ]
288
+
289
+ COMMANDS = {AgentModeProvider, ProviderSetupProvider}
290
+
291
+ value = reactive("")
292
+ mode = reactive(AgentType.RESEARCH, recompose=True)
293
+ history: PromptHistory = PromptHistory()
294
+ messages = reactive(list[ModelMessage]())
295
+ working = reactive(False)
296
+ question: reactive[UserQuestion | None] = reactive(None)
297
+
298
+ def __init__(self) -> None:
299
+ super().__init__()
300
+ # Get the model configuration and codebase service
301
+ model_config = get_provider_model()
302
+ codebase_service = get_codebase_service()
303
+ self.deps = AgentDeps(
304
+ interactive_mode=True,
305
+ llm_model=model_config,
306
+ codebase_service=codebase_service,
307
+ )
308
+ self.agent_manager = AgentManager(deps=self.deps, initial_type=self.mode)
309
+
310
+ def on_mount(self) -> None:
311
+ self.query_one(PromptInput).focus(scroll_visible=True)
312
+ # Hide spinner initially
313
+ self.query_one("#spinner").display = False
314
+
315
+ def watch_mode(self, new_mode: AgentType) -> None:
316
+ """React to mode changes by updating the agent manager."""
317
+ if hasattr(self, "agent_manager"):
318
+ self.agent_manager.set_agent(new_mode)
319
+
320
+ def watch_working(self, is_working: bool) -> None:
321
+ """Show or hide the spinner based on working state."""
322
+ if self.is_mounted:
323
+ spinner = self.query_one("#spinner")
324
+ spinner.display = is_working
325
+
326
+ def watch_messages(self, messages: list[ModelMessage]) -> None:
327
+ """Update the chat history when messages change."""
328
+ if self.is_mounted:
329
+ chat_history = self.query_one(ChatHistory)
330
+ chat_history.update_messages(messages)
331
+
332
+ def watch_question(self, question: UserQuestion | None) -> None:
333
+ """Update the question display."""
334
+ if self.is_mounted:
335
+ question_display = self.query_one("#question-display", Markdown)
336
+ if question:
337
+ question_display.update(f"Question:\n\n{question.question}")
338
+ question_display.display = True
339
+ else:
340
+ question_display.update("")
341
+ question_display.display = False
342
+
343
+ @work
344
+ async def add_question_listener(self) -> None:
345
+ while True:
346
+ question = await self.deps.queue.get()
347
+ self.question = question
348
+ await question.result
349
+ self.deps.queue.task_done()
350
+
351
+ def compose(self) -> ComposeResult:
352
+ """Create child widgets for the app."""
353
+ with Container(id="window"):
354
+ yield ChatHistory()
355
+ yield Markdown(markdown="", id="question-display")
356
+ yield self.agent_manager
357
+ with Container(id="footer"):
358
+ yield Spinner(text="Processing...", id="spinner")
359
+ yield StatusBar()
360
+ yield PromptInput(
361
+ text=self.value,
362
+ highlight_cursor_line=False,
363
+ id="prompt-input",
364
+ placeholder="Type your message",
365
+ )
366
+ yield ModeIndicator(mode=self.mode)
367
+
368
+ @on(MessageHistoryUpdated)
369
+ def handle_message_history_updated(self, event: MessageHistoryUpdated) -> None:
370
+ """Handle message history updates from the agent manager."""
371
+ self.messages = event.messages
372
+
373
+ @on(PromptInput.Submitted)
374
+ async def handle_submit(self, message: PromptInput.Submitted) -> None:
375
+ self.history.append(message.text)
376
+
377
+ # Clear the input
378
+ self.value = ""
379
+ self.run_agent(message.text)
380
+
381
+ prompt_input = self.query_one(PromptInput)
382
+ prompt_input.clear()
383
+ prompt_input.focus()
384
+
385
+ @work
386
+ async def run_agent(self, message: str) -> None:
387
+ deferred_tool_results = None
388
+ prompt = None
389
+ self.working = True
390
+
391
+ if self.question:
392
+ # This is a response to a question from the agent
393
+ self.question.result.set_result(
394
+ UserAnswer(answer=message, tool_call_id=self.question.tool_call_id)
395
+ )
396
+
397
+ deferred_tool_results = DeferredToolResults()
398
+
399
+ deferred_tool_results.calls[self.question.tool_call_id] = UserAnswer(
400
+ answer=message, tool_call_id=self.question.tool_call_id
401
+ )
402
+
403
+ self.question = None
404
+ else:
405
+ # This is a new user prompt
406
+ prompt = message
407
+
408
+ await self.agent_manager.run(
409
+ prompt=prompt,
410
+ deferred_tool_results=deferred_tool_results,
411
+ )
412
+ self.working = False
413
+
414
+ prompt_input = self.query_one(PromptInput)
415
+ prompt_input.focus()
@@ -0,0 +1,28 @@
1
+ ChatHistory {
2
+ height: auto;
3
+ }
4
+
5
+ PromptInput {
6
+ min-height: 3;
7
+ max-height: 7;
8
+ height: auto;
9
+ }
10
+
11
+ StatusBar {
12
+ height: auto;
13
+ }
14
+
15
+ ModeIndicator {
16
+ height: auto;
17
+ }
18
+
19
+ #footer {
20
+ dock: bottom;
21
+ height: auto;
22
+ padding: 1 1 1 2;
23
+ max-height: 14;
24
+ }
25
+
26
+ #window {
27
+ align: left bottom;
28
+ }