shotgun-sh 0.2.6.dev1__py3-none-any.whl → 0.2.17__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 +694 -73
- shotgun/agents/common.py +69 -70
- shotgun/agents/config/constants.py +0 -6
- shotgun/agents/config/manager.py +70 -35
- shotgun/agents/config/models.py +41 -1
- shotgun/agents/config/provider.py +33 -5
- shotgun/agents/context_analyzer/__init__.py +28 -0
- shotgun/agents/context_analyzer/analyzer.py +471 -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 +57 -19
- shotgun/agents/export.py +6 -7
- shotgun/agents/history/compaction.py +9 -4
- shotgun/agents/history/context_extraction.py +93 -6
- shotgun/agents/history/history_processors.py +113 -5
- shotgun/agents/history/token_counting/anthropic.py +39 -3
- shotgun/agents/history/token_counting/base.py +14 -3
- shotgun/agents/history/token_counting/openai.py +11 -1
- shotgun/agents/history/token_counting/sentencepiece_counter.py +8 -0
- shotgun/agents/history/token_counting/tokenizer_cache.py +3 -1
- shotgun/agents/history/token_counting/utils.py +0 -3
- shotgun/agents/models.py +50 -2
- shotgun/agents/plan.py +6 -7
- shotgun/agents/research.py +7 -8
- shotgun/agents/specify.py +6 -7
- shotgun/agents/tasks.py +6 -7
- 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 +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 +82 -16
- 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 +7 -1
- 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 +3 -3
- shotgun/cli/clear.py +53 -0
- shotgun/cli/compact.py +186 -0
- shotgun/cli/config.py +8 -5
- shotgun/cli/context.py +111 -0
- shotgun/cli/export.py +1 -1
- shotgun/cli/feedback.py +4 -2
- shotgun/cli/models.py +1 -0
- shotgun/cli/plan.py +1 -1
- shotgun/cli/research.py +1 -1
- shotgun/cli/specify.py +1 -1
- shotgun/cli/tasks.py +1 -1
- 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 +10 -8
- shotgun/codebase/core/manager.py +13 -4
- shotgun/codebase/core/nl_query.py +1 -1
- shotgun/exceptions.py +32 -0
- shotgun/logging_config.py +18 -27
- shotgun/main.py +73 -11
- shotgun/posthog_telemetry.py +37 -28
- 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/tasks.j2 +1 -1
- shotgun/sentry_telemetry.py +163 -16
- shotgun/settings.py +238 -0
- shotgun/telemetry.py +18 -33
- shotgun/tui/app.py +243 -43
- 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/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 +1254 -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 +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 +78 -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 +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 +151 -0
- shotgun/tui/screens/feedback.py +4 -4
- shotgun/tui/screens/github_issue.py +102 -0
- shotgun/tui/screens/model_picker.py +49 -24
- shotgun/tui/screens/onboarding.py +431 -0
- shotgun/tui/screens/pipx_migration.py +153 -0
- shotgun/tui/screens/provider_config.py +50 -27
- shotgun/tui/screens/shotgun_auth.py +2 -2
- shotgun/tui/screens/welcome.py +23 -12
- shotgun/tui/services/__init__.py +5 -0
- shotgun/tui/services/conversation_service.py +184 -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.2.17.dist-info/METADATA +465 -0
- shotgun_sh-0.2.17.dist-info/RECORD +194 -0
- {shotgun_sh-0.2.6.dev1.dist-info → shotgun_sh-0.2.17.dist-info}/entry_points.txt +1 -0
- {shotgun_sh-0.2.6.dev1.dist-info → shotgun_sh-0.2.17.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 -401
- shotgun_sh-0.2.6.dev1.dist-info/METADATA +0 -467
- shotgun_sh-0.2.6.dev1.dist-info/RECORD +0 -156
- {shotgun_sh-0.2.6.dev1.dist-info → shotgun_sh-0.2.17.dist-info}/WHEEL +0 -0
|
@@ -1,401 +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
|
-
|
|
219
|
-
# Check if there's an ask_user tool call
|
|
220
|
-
has_ask_user = any(
|
|
221
|
-
isinstance(part, ToolCallPart) and part.tool_name == "ask_user"
|
|
222
|
-
for part in self.item.parts
|
|
223
|
-
)
|
|
224
|
-
|
|
225
|
-
for idx, part in enumerate(self.item.parts):
|
|
226
|
-
if isinstance(part, TextPart):
|
|
227
|
-
# Skip ALL text parts if there's an ask_user tool call
|
|
228
|
-
if has_ask_user:
|
|
229
|
-
continue
|
|
230
|
-
# Only show the circle prefix if there's actual content
|
|
231
|
-
if part.content and part.content.strip():
|
|
232
|
-
acc += f"**⏺** {part.content}\n\n"
|
|
233
|
-
elif isinstance(part, ToolCallPart):
|
|
234
|
-
parts_str = self._format_tool_call_part(part)
|
|
235
|
-
acc += parts_str + "\n\n"
|
|
236
|
-
elif isinstance(part, BuiltinToolCallPart):
|
|
237
|
-
# Format builtin tool calls better
|
|
238
|
-
if part.tool_name and "search" in part.tool_name.lower():
|
|
239
|
-
args = self._parse_args(part.args)
|
|
240
|
-
if isinstance(args, dict) and "query" in args:
|
|
241
|
-
query = self._truncate(str(args.get("query", "")))
|
|
242
|
-
acc += f'Searching: "{query}"\n\n'
|
|
243
|
-
else:
|
|
244
|
-
acc += f"{part.tool_name}()\n\n"
|
|
245
|
-
else:
|
|
246
|
-
# For other builtin tools, show name only or with truncated args
|
|
247
|
-
if part.args:
|
|
248
|
-
args_str = (
|
|
249
|
-
str(part.args)[:50] + "..."
|
|
250
|
-
if len(str(part.args)) > 50
|
|
251
|
-
else str(part.args)
|
|
252
|
-
)
|
|
253
|
-
acc += f"{part.tool_name}({args_str})\n\n"
|
|
254
|
-
else:
|
|
255
|
-
acc += f"{part.tool_name}()\n\n"
|
|
256
|
-
elif isinstance(part, BuiltinToolReturnPart):
|
|
257
|
-
acc += f"builtin tool ({part.tool_name}) return: {part.content}\n\n"
|
|
258
|
-
elif isinstance(part, ThinkingPart):
|
|
259
|
-
if (
|
|
260
|
-
idx == len(self.item.parts) - 1
|
|
261
|
-
): # show the thinking part only if it's the last part
|
|
262
|
-
acc += (
|
|
263
|
-
f"thinking: {part.content}\n\n"
|
|
264
|
-
if part.content
|
|
265
|
-
else "Thinking..."
|
|
266
|
-
)
|
|
267
|
-
else:
|
|
268
|
-
continue
|
|
269
|
-
return acc.strip()
|
|
270
|
-
|
|
271
|
-
def _truncate(self, text: str, max_length: int = 100) -> str:
|
|
272
|
-
"""Truncate text to max_length characters, adding ellipsis if needed."""
|
|
273
|
-
if len(text) <= max_length:
|
|
274
|
-
return text
|
|
275
|
-
return text[: max_length - 3] + "..."
|
|
276
|
-
|
|
277
|
-
def _parse_args(self, args: dict[str, object] | str | None) -> dict[str, object]:
|
|
278
|
-
"""Parse tool call arguments, handling both dict and JSON string formats."""
|
|
279
|
-
if args is None:
|
|
280
|
-
return {}
|
|
281
|
-
if isinstance(args, str):
|
|
282
|
-
try:
|
|
283
|
-
return json.loads(args) if args.strip() else {}
|
|
284
|
-
except json.JSONDecodeError:
|
|
285
|
-
return {}
|
|
286
|
-
return args if isinstance(args, dict) else {}
|
|
287
|
-
|
|
288
|
-
def _format_tool_call_part(self, part: ToolCallPart) -> str:
|
|
289
|
-
if part.tool_name == "ask_user":
|
|
290
|
-
return self._format_ask_user_part(part)
|
|
291
|
-
|
|
292
|
-
# Parse args once (handles both JSON string and dict)
|
|
293
|
-
args = self._parse_args(part.args)
|
|
294
|
-
|
|
295
|
-
# Codebase tools - show friendly names
|
|
296
|
-
if part.tool_name == "query_graph":
|
|
297
|
-
if "query" in args:
|
|
298
|
-
query = self._truncate(str(args["query"]))
|
|
299
|
-
return f'Querying code: "{query}"'
|
|
300
|
-
return "Querying code"
|
|
301
|
-
|
|
302
|
-
if part.tool_name == "retrieve_code":
|
|
303
|
-
if "qualified_name" in args:
|
|
304
|
-
return f'Retrieving code: "{args["qualified_name"]}"'
|
|
305
|
-
return "Retrieving code"
|
|
306
|
-
|
|
307
|
-
if part.tool_name == "file_read":
|
|
308
|
-
if "file_path" in args:
|
|
309
|
-
return f'Reading file: "{args["file_path"]}"'
|
|
310
|
-
return "Reading file"
|
|
311
|
-
|
|
312
|
-
if part.tool_name == "directory_lister":
|
|
313
|
-
if "directory" in args:
|
|
314
|
-
return f'Listing directory: "{args["directory"]}"'
|
|
315
|
-
return "Listing directory"
|
|
316
|
-
|
|
317
|
-
if part.tool_name == "codebase_shell":
|
|
318
|
-
command = args.get("command", "")
|
|
319
|
-
cmd_args = args.get("args", [])
|
|
320
|
-
# Handle cmd_args as list of strings
|
|
321
|
-
if isinstance(cmd_args, list):
|
|
322
|
-
args_str = " ".join(str(arg) for arg in cmd_args)
|
|
323
|
-
else:
|
|
324
|
-
args_str = ""
|
|
325
|
-
full_cmd = f"{command} {args_str}".strip()
|
|
326
|
-
if full_cmd:
|
|
327
|
-
return f'Running shell: "{self._truncate(full_cmd)}"'
|
|
328
|
-
return "Running shell"
|
|
329
|
-
|
|
330
|
-
# File management tools
|
|
331
|
-
if part.tool_name == "read_file":
|
|
332
|
-
if "filename" in args:
|
|
333
|
-
return f'Reading file: "{args["filename"]}"'
|
|
334
|
-
return "Reading file"
|
|
335
|
-
|
|
336
|
-
# Web search tools - handle variations
|
|
337
|
-
if (
|
|
338
|
-
part.tool_name
|
|
339
|
-
in [
|
|
340
|
-
"openai_web_search_tool",
|
|
341
|
-
"anthropic_web_search_tool",
|
|
342
|
-
"gemini_web_search_tool",
|
|
343
|
-
]
|
|
344
|
-
or "search" in part.tool_name.lower()
|
|
345
|
-
): # Catch other search variations
|
|
346
|
-
if "query" in args:
|
|
347
|
-
query = self._truncate(str(args["query"]))
|
|
348
|
-
return f'Searching web: "{query}"'
|
|
349
|
-
return "Searching web"
|
|
350
|
-
|
|
351
|
-
# write_file
|
|
352
|
-
if part.tool_name == "write_file" or part.tool_name == "append_file":
|
|
353
|
-
if "filename" in args:
|
|
354
|
-
return f"{part.tool_name}({args['filename']})"
|
|
355
|
-
return f"{part.tool_name}()"
|
|
356
|
-
|
|
357
|
-
if part.tool_name == "write_artifact_section":
|
|
358
|
-
if "section_title" in args:
|
|
359
|
-
return f"{part.tool_name}({args['section_title']})"
|
|
360
|
-
return f"{part.tool_name}()"
|
|
361
|
-
|
|
362
|
-
if part.tool_name == "create_artifact":
|
|
363
|
-
if "name" in args:
|
|
364
|
-
return f"{part.tool_name}({args['name']})"
|
|
365
|
-
return f"▪ {part.tool_name}()"
|
|
366
|
-
|
|
367
|
-
# Default case for unrecognized tools - format args properly
|
|
368
|
-
args = self._parse_args(part.args)
|
|
369
|
-
if args and isinstance(args, dict):
|
|
370
|
-
# Try to extract common fields
|
|
371
|
-
if "query" in args:
|
|
372
|
-
return f'{part.tool_name}: "{self._truncate(str(args["query"]))}"'
|
|
373
|
-
elif "question" in args:
|
|
374
|
-
return f'{part.tool_name}: "{self._truncate(str(args["question"]))}"'
|
|
375
|
-
else:
|
|
376
|
-
# Show tool name with truncated args
|
|
377
|
-
args_str = (
|
|
378
|
-
str(part.args)[:50] + "..."
|
|
379
|
-
if len(str(part.args)) > 50
|
|
380
|
-
else str(part.args)
|
|
381
|
-
)
|
|
382
|
-
return f"{part.tool_name}({args_str})"
|
|
383
|
-
else:
|
|
384
|
-
return f"{part.tool_name}()"
|
|
385
|
-
|
|
386
|
-
def _format_ask_user_part(
|
|
387
|
-
self,
|
|
388
|
-
part: ToolCallPart,
|
|
389
|
-
) -> str:
|
|
390
|
-
if isinstance(part.args, str):
|
|
391
|
-
try:
|
|
392
|
-
_args = json.loads(part.args) if part.args.strip() else {}
|
|
393
|
-
except json.JSONDecodeError:
|
|
394
|
-
_args = {}
|
|
395
|
-
else:
|
|
396
|
-
_args = part.args
|
|
397
|
-
|
|
398
|
-
if isinstance(_args, dict) and "question" in _args:
|
|
399
|
-
return f"{_args['question']}"
|
|
400
|
-
else:
|
|
401
|
-
return "❓ "
|