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.

Files changed (107) hide show
  1. shotgun/agents/agent_manager.py +524 -58
  2. shotgun/agents/common.py +62 -62
  3. shotgun/agents/config/constants.py +0 -6
  4. shotgun/agents/config/manager.py +14 -3
  5. shotgun/agents/config/models.py +16 -0
  6. shotgun/agents/config/provider.py +68 -13
  7. shotgun/agents/context_analyzer/__init__.py +28 -0
  8. shotgun/agents/context_analyzer/analyzer.py +493 -0
  9. shotgun/agents/context_analyzer/constants.py +9 -0
  10. shotgun/agents/context_analyzer/formatter.py +115 -0
  11. shotgun/agents/context_analyzer/models.py +212 -0
  12. shotgun/agents/conversation_history.py +125 -2
  13. shotgun/agents/conversation_manager.py +24 -2
  14. shotgun/agents/export.py +4 -5
  15. shotgun/agents/history/compaction.py +9 -4
  16. shotgun/agents/history/context_extraction.py +93 -6
  17. shotgun/agents/history/history_processors.py +14 -2
  18. shotgun/agents/history/token_counting/anthropic.py +32 -10
  19. shotgun/agents/models.py +50 -2
  20. shotgun/agents/plan.py +4 -5
  21. shotgun/agents/research.py +4 -5
  22. shotgun/agents/specify.py +4 -5
  23. shotgun/agents/tasks.py +4 -5
  24. shotgun/agents/tools/__init__.py +0 -2
  25. shotgun/agents/tools/codebase/codebase_shell.py +6 -0
  26. shotgun/agents/tools/codebase/directory_lister.py +6 -0
  27. shotgun/agents/tools/codebase/file_read.py +6 -0
  28. shotgun/agents/tools/codebase/query_graph.py +6 -0
  29. shotgun/agents/tools/codebase/retrieve_code.py +6 -0
  30. shotgun/agents/tools/file_management.py +71 -9
  31. shotgun/agents/tools/registry.py +217 -0
  32. shotgun/agents/tools/web_search/__init__.py +24 -12
  33. shotgun/agents/tools/web_search/anthropic.py +24 -3
  34. shotgun/agents/tools/web_search/gemini.py +22 -10
  35. shotgun/agents/tools/web_search/openai.py +21 -12
  36. shotgun/api_endpoints.py +7 -3
  37. shotgun/build_constants.py +1 -1
  38. shotgun/cli/clear.py +52 -0
  39. shotgun/cli/compact.py +186 -0
  40. shotgun/cli/context.py +111 -0
  41. shotgun/cli/models.py +1 -0
  42. shotgun/cli/update.py +16 -2
  43. shotgun/codebase/core/manager.py +10 -1
  44. shotgun/llm_proxy/__init__.py +5 -2
  45. shotgun/llm_proxy/clients.py +12 -7
  46. shotgun/logging_config.py +8 -10
  47. shotgun/main.py +70 -10
  48. shotgun/posthog_telemetry.py +9 -3
  49. shotgun/prompts/agents/export.j2 +18 -1
  50. shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +5 -1
  51. shotgun/prompts/agents/partials/interactive_mode.j2 +24 -7
  52. shotgun/prompts/agents/plan.j2 +1 -1
  53. shotgun/prompts/agents/research.j2 +1 -1
  54. shotgun/prompts/agents/specify.j2 +270 -3
  55. shotgun/prompts/agents/state/system_state.j2 +4 -0
  56. shotgun/prompts/agents/tasks.j2 +1 -1
  57. shotgun/prompts/loader.py +2 -2
  58. shotgun/prompts/tools/web_search.j2 +14 -0
  59. shotgun/sentry_telemetry.py +4 -15
  60. shotgun/settings.py +238 -0
  61. shotgun/telemetry.py +15 -32
  62. shotgun/tui/app.py +203 -9
  63. shotgun/tui/commands/__init__.py +1 -1
  64. shotgun/tui/components/context_indicator.py +136 -0
  65. shotgun/tui/components/mode_indicator.py +70 -0
  66. shotgun/tui/components/status_bar.py +48 -0
  67. shotgun/tui/containers.py +93 -0
  68. shotgun/tui/dependencies.py +39 -0
  69. shotgun/tui/protocols.py +45 -0
  70. shotgun/tui/screens/chat/__init__.py +5 -0
  71. shotgun/tui/screens/chat/chat.tcss +54 -0
  72. shotgun/tui/screens/chat/chat_screen.py +1110 -0
  73. shotgun/tui/screens/chat/codebase_index_prompt_screen.py +64 -0
  74. shotgun/tui/screens/chat/codebase_index_selection.py +12 -0
  75. shotgun/tui/screens/chat/help_text.py +39 -0
  76. shotgun/tui/screens/chat/prompt_history.py +48 -0
  77. shotgun/tui/screens/chat.tcss +11 -0
  78. shotgun/tui/screens/chat_screen/command_providers.py +68 -2
  79. shotgun/tui/screens/chat_screen/history/__init__.py +22 -0
  80. shotgun/tui/screens/chat_screen/history/agent_response.py +66 -0
  81. shotgun/tui/screens/chat_screen/history/chat_history.py +116 -0
  82. shotgun/tui/screens/chat_screen/history/formatters.py +115 -0
  83. shotgun/tui/screens/chat_screen/history/partial_response.py +43 -0
  84. shotgun/tui/screens/chat_screen/history/user_question.py +42 -0
  85. shotgun/tui/screens/confirmation_dialog.py +151 -0
  86. shotgun/tui/screens/model_picker.py +30 -6
  87. shotgun/tui/screens/pipx_migration.py +153 -0
  88. shotgun/tui/screens/welcome.py +24 -5
  89. shotgun/tui/services/__init__.py +5 -0
  90. shotgun/tui/services/conversation_service.py +182 -0
  91. shotgun/tui/state/__init__.py +7 -0
  92. shotgun/tui/state/processing_state.py +185 -0
  93. shotgun/tui/widgets/__init__.py +5 -0
  94. shotgun/tui/widgets/widget_coordinator.py +247 -0
  95. shotgun/utils/datetime_utils.py +77 -0
  96. shotgun/utils/file_system_utils.py +3 -2
  97. shotgun/utils/update_checker.py +69 -14
  98. shotgun_sh-0.2.11.dev1.dist-info/METADATA +129 -0
  99. shotgun_sh-0.2.11.dev1.dist-info/RECORD +190 -0
  100. {shotgun_sh-0.2.3.dev2.dist-info → shotgun_sh-0.2.11.dev1.dist-info}/entry_points.txt +1 -0
  101. {shotgun_sh-0.2.3.dev2.dist-info → shotgun_sh-0.2.11.dev1.dist-info}/licenses/LICENSE +1 -1
  102. shotgun/agents/tools/user_interaction.py +0 -37
  103. shotgun/tui/screens/chat.py +0 -804
  104. shotgun/tui/screens/chat_screen/history.py +0 -352
  105. shotgun_sh-0.2.3.dev2.dist-info/METADATA +0 -467
  106. shotgun_sh-0.2.3.dev2.dist-info/RECORD +0 -154
  107. {shotgun_sh-0.2.3.dev2.dist-info → shotgun_sh-0.2.11.dev1.dist-info}/WHEEL +0 -0
@@ -1,352 +0,0 @@
1
- import json
2
- from collections.abc import Generator, Sequence
3
-
4
- from pydantic_ai.messages import (
5
- BuiltinToolCallPart,
6
- BuiltinToolReturnPart,
7
- ModelMessage,
8
- ModelRequest,
9
- ModelRequestPart,
10
- ModelResponse,
11
- TextPart,
12
- ThinkingPart,
13
- ToolCallPart,
14
- ToolReturnPart,
15
- UserPromptPart,
16
- )
17
- from textual.app import ComposeResult
18
- from textual.reactive import reactive
19
- from textual.widget import Widget
20
- from textual.widgets import Markdown
21
-
22
- from shotgun.agents.models import UserAnswer
23
- from shotgun.tui.components.vertical_tail import VerticalTail
24
- from shotgun.tui.screens.chat_screen.hint_message import HintMessage, HintMessageWidget
25
-
26
-
27
- class PartialResponseWidget(Widget): # TODO: doesn't work lol
28
- DEFAULT_CSS = """
29
- PartialResponseWidget {
30
- height: auto;
31
- }
32
- Markdown, AgentResponseWidget, UserQuestionWidget {
33
- height: auto;
34
- }
35
- """
36
-
37
- item: reactive[ModelMessage | None] = reactive(None, recompose=True)
38
-
39
- def __init__(self, item: ModelMessage | None) -> None:
40
- super().__init__()
41
- self.item = item
42
-
43
- def compose(self) -> ComposeResult:
44
- if self.item is None:
45
- pass
46
- elif self.item.kind == "response":
47
- yield AgentResponseWidget(self.item)
48
- elif self.item.kind == "request":
49
- yield UserQuestionWidget(self.item)
50
-
51
- def watch_item(self, item: ModelMessage | None) -> None:
52
- if item is None:
53
- self.display = False
54
- else:
55
- self.display = True
56
-
57
-
58
- class ChatHistory(Widget):
59
- DEFAULT_CSS = """
60
- VerticalTail {
61
- align: left bottom;
62
-
63
- }
64
- VerticalTail > * {
65
- height: auto;
66
- }
67
-
68
- Horizontal {
69
- height: auto;
70
- background: $secondary-muted;
71
- }
72
-
73
- Markdown {
74
- height: auto;
75
- }
76
- """
77
- partial_response: reactive[ModelMessage | None] = reactive(None)
78
-
79
- def __init__(self) -> None:
80
- super().__init__()
81
- self.items: Sequence[ModelMessage | HintMessage] = []
82
- self.vertical_tail: VerticalTail | None = None
83
- self.partial_response = None
84
- self._rendered_count = 0 # Track how many messages have been mounted
85
-
86
- def compose(self) -> ComposeResult:
87
- self.vertical_tail = VerticalTail()
88
-
89
- filtered = list(self.filtered_items())
90
- with self.vertical_tail:
91
- for item in filtered:
92
- if isinstance(item, ModelRequest):
93
- yield UserQuestionWidget(item)
94
- elif isinstance(item, HintMessage):
95
- yield HintMessageWidget(item)
96
- elif isinstance(item, ModelResponse):
97
- yield AgentResponseWidget(item)
98
- yield PartialResponseWidget(self.partial_response).data_bind(
99
- item=ChatHistory.partial_response
100
- )
101
-
102
- # Track how many messages were rendered during initial compose
103
- self._rendered_count = len(filtered)
104
-
105
- def filtered_items(self) -> Generator[ModelMessage | HintMessage, None, None]:
106
- for idx, next_item in enumerate(self.items):
107
- prev_item = self.items[idx - 1] if idx > 0 else None
108
-
109
- if isinstance(prev_item, ModelRequest) and isinstance(
110
- next_item, ModelResponse
111
- ):
112
- ask_user_tool_response_part = next(
113
- (
114
- part
115
- for part in prev_item.parts
116
- if isinstance(part, ToolReturnPart)
117
- and part.tool_name == "ask_user"
118
- ),
119
- None,
120
- )
121
-
122
- ask_user_part = next(
123
- (
124
- part
125
- for part in next_item.parts
126
- if isinstance(part, ToolCallPart)
127
- and part.tool_name == "ask_user"
128
- ),
129
- None,
130
- )
131
-
132
- if not ask_user_part or not ask_user_tool_response_part:
133
- yield next_item
134
- continue
135
- if (
136
- ask_user_tool_response_part.tool_call_id
137
- == ask_user_part.tool_call_id
138
- ):
139
- continue # don't emit tool call that happens after tool response
140
-
141
- yield next_item
142
-
143
- def update_messages(self, messages: list[ModelMessage | HintMessage]) -> None:
144
- """Update the displayed messages using incremental mounting."""
145
- if not self.vertical_tail:
146
- return
147
-
148
- self.items = messages
149
- filtered = list(self.filtered_items())
150
-
151
- # Only mount new messages that haven't been rendered yet
152
- if len(filtered) > self._rendered_count:
153
- new_messages = filtered[self._rendered_count :]
154
- for item in new_messages:
155
- widget: Widget
156
- if isinstance(item, ModelRequest):
157
- widget = UserQuestionWidget(item)
158
- elif isinstance(item, HintMessage):
159
- widget = HintMessageWidget(item)
160
- elif isinstance(item, ModelResponse):
161
- widget = AgentResponseWidget(item)
162
- else:
163
- continue
164
-
165
- # Mount before the PartialResponseWidget
166
- self.vertical_tail.mount(widget, before=self.vertical_tail.children[-1])
167
-
168
- self._rendered_count = len(filtered)
169
-
170
-
171
- class UserQuestionWidget(Widget):
172
- def __init__(self, item: ModelRequest | None) -> None:
173
- super().__init__()
174
- self.item = item
175
-
176
- def compose(self) -> ComposeResult:
177
- self.display = self.item is not None
178
- if self.item is None:
179
- yield Markdown(markdown="")
180
- else:
181
- prompt = self.format_prompt_parts(self.item.parts)
182
- yield Markdown(markdown=prompt)
183
-
184
- def format_prompt_parts(self, parts: Sequence[ModelRequestPart]) -> str:
185
- acc = ""
186
- for part in parts:
187
- if isinstance(part, UserPromptPart):
188
- acc += (
189
- f"**>** {part.content if isinstance(part.content, str) else ''}\n\n"
190
- )
191
- elif isinstance(part, ToolReturnPart):
192
- if part.tool_name == "ask_user":
193
- acc += f"**>** {part.content.answer if isinstance(part.content, UserAnswer) else part.content['answer']}\n\n"
194
- else:
195
- # acc += " ∟ finished\n\n" # let's not show anything yet
196
- pass
197
- elif isinstance(part, UserPromptPart):
198
- acc += f"**>** {part.content}\n\n"
199
- return acc
200
-
201
-
202
- class AgentResponseWidget(Widget):
203
- def __init__(self, item: ModelResponse | None) -> None:
204
- super().__init__()
205
- self.item = item
206
-
207
- def compose(self) -> ComposeResult:
208
- self.display = self.item is not None
209
- if self.item is None:
210
- yield Markdown(markdown="")
211
- else:
212
- yield Markdown(markdown=self.compute_output())
213
-
214
- def compute_output(self) -> str:
215
- acc = ""
216
- if self.item is None:
217
- return ""
218
- for idx, part in enumerate(self.item.parts):
219
- if isinstance(part, TextPart):
220
- # Only show the circle prefix if there's actual content
221
- if part.content and part.content.strip():
222
- acc += f"**⏺** {part.content}\n\n"
223
- elif isinstance(part, ToolCallPart):
224
- parts_str = self._format_tool_call_part(part)
225
- acc += parts_str + "\n\n"
226
- elif isinstance(part, BuiltinToolCallPart):
227
- acc += f"{part.tool_name}({part.args})\n\n"
228
- elif isinstance(part, BuiltinToolReturnPart):
229
- acc += f"builtin tool ({part.tool_name}) return: {part.content}\n\n"
230
- elif isinstance(part, ThinkingPart):
231
- if (
232
- idx == len(self.item.parts) - 1
233
- ): # show the thinking part only if it's the last part
234
- acc += (
235
- f"thinking: {part.content}\n\n"
236
- if part.content
237
- else "Thinking..."
238
- )
239
- else:
240
- continue
241
- return acc.strip()
242
-
243
- def _truncate(self, text: str, max_length: int = 100) -> str:
244
- """Truncate text to max_length characters, adding ellipsis if needed."""
245
- if len(text) <= max_length:
246
- return text
247
- return text[: max_length - 3] + "..."
248
-
249
- def _parse_args(self, args: dict[str, object] | str | None) -> dict[str, object]:
250
- """Parse tool call arguments, handling both dict and JSON string formats."""
251
- if args is None:
252
- return {}
253
- if isinstance(args, str):
254
- try:
255
- return json.loads(args) if args.strip() else {}
256
- except json.JSONDecodeError:
257
- return {}
258
- return args if isinstance(args, dict) else {}
259
-
260
- def _format_tool_call_part(self, part: ToolCallPart) -> str:
261
- if part.tool_name == "ask_user":
262
- return self._format_ask_user_part(part)
263
-
264
- # Parse args once (handles both JSON string and dict)
265
- args = self._parse_args(part.args)
266
-
267
- # Codebase tools - show friendly names
268
- if part.tool_name == "query_graph":
269
- if "query" in args:
270
- query = self._truncate(str(args["query"]))
271
- return f'Querying code: "{query}"'
272
- return "Querying code"
273
-
274
- if part.tool_name == "retrieve_code":
275
- if "qualified_name" in args:
276
- return f'Retrieving code: "{args["qualified_name"]}"'
277
- return "Retrieving code"
278
-
279
- if part.tool_name == "file_read":
280
- if "file_path" in args:
281
- return f'Reading file: "{args["file_path"]}"'
282
- return "Reading file"
283
-
284
- if part.tool_name == "directory_lister":
285
- if "directory" in args:
286
- return f'Listing directory: "{args["directory"]}"'
287
- return "Listing directory"
288
-
289
- if part.tool_name == "codebase_shell":
290
- command = args.get("command", "")
291
- cmd_args = args.get("args", [])
292
- # Handle cmd_args as list of strings
293
- if isinstance(cmd_args, list):
294
- args_str = " ".join(str(arg) for arg in cmd_args)
295
- else:
296
- args_str = ""
297
- full_cmd = f"{command} {args_str}".strip()
298
- if full_cmd:
299
- return f'Running shell: "{self._truncate(full_cmd)}"'
300
- return "Running shell"
301
-
302
- # File management tools
303
- if part.tool_name == "read_file":
304
- if "filename" in args:
305
- return f'Reading file: "{args["filename"]}"'
306
- return "Reading file"
307
-
308
- # Web search tools
309
- if part.tool_name in [
310
- "openai_web_search_tool",
311
- "anthropic_web_search_tool",
312
- "gemini_web_search_tool",
313
- ]:
314
- if "query" in args:
315
- query = self._truncate(str(args["query"]))
316
- return f'Searching web: "{query}"'
317
- return "Searching web"
318
-
319
- # write_file
320
- if part.tool_name == "write_file" or part.tool_name == "append_file":
321
- if "filename" in args:
322
- return f"{part.tool_name}({args['filename']})"
323
- return f"{part.tool_name}()"
324
-
325
- if part.tool_name == "write_artifact_section":
326
- if "section_title" in args:
327
- return f"{part.tool_name}({args['section_title']})"
328
- return f"{part.tool_name}()"
329
-
330
- if part.tool_name == "create_artifact":
331
- if "name" in args:
332
- return f"{part.tool_name}({args['name']})"
333
- return f"▪ {part.tool_name}()"
334
-
335
- return f"{part.tool_name}({part.args})"
336
-
337
- def _format_ask_user_part(
338
- self,
339
- part: ToolCallPart,
340
- ) -> str:
341
- if isinstance(part.args, str):
342
- try:
343
- _args = json.loads(part.args) if part.args.strip() else {}
344
- except json.JSONDecodeError:
345
- _args = {}
346
- else:
347
- _args = part.args
348
-
349
- if isinstance(_args, dict) and "question" in _args:
350
- return f"{_args['question']}"
351
- else:
352
- return "❓ "