shotgun-sh 0.2.5.dev1__py3-none-any.whl → 0.2.6.dev2__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.

@@ -36,6 +36,7 @@ from textual.message import Message
36
36
  from textual.widget import Widget
37
37
 
38
38
  from shotgun.agents.common import add_system_prompt_message, add_system_status_message
39
+ from shotgun.agents.config.models import KeyProvider
39
40
  from shotgun.agents.models import AgentType, FileOperation
40
41
  from shotgun.posthog_telemetry import track_event
41
42
  from shotgun.tui.screens.chat_screen.hint_message import HintMessage
@@ -359,10 +360,18 @@ class AgentManager(Widget):
359
360
  model_name = ""
360
361
  if hasattr(deps, "llm_model") and deps.llm_model is not None:
361
362
  model_name = deps.llm_model.name
362
- is_gpt5 = ( # streaming is likely not supported for gpt5. It varies between keys.
363
- "gpt-5" in model_name.lower()
363
+
364
+ # Check if it's a Shotgun account
365
+ is_shotgun_account = (
366
+ hasattr(deps, "llm_model")
367
+ and deps.llm_model is not None
368
+ and deps.llm_model.key_provider == KeyProvider.SHOTGUN
364
369
  )
365
370
 
371
+ # Only disable streaming for GPT-5 if NOT a Shotgun account
372
+ # Shotgun accounts support streaming for GPT-5
373
+ is_gpt5_byok = "gpt-5" in model_name.lower() and not is_shotgun_account
374
+
366
375
  # Track message send event
367
376
  event_name = f"message_send_{self._current_agent_type.value}"
368
377
  track_event(
@@ -383,7 +392,9 @@ class AgentManager(Widget):
383
392
  usage_limits=usage_limits,
384
393
  message_history=message_history,
385
394
  deferred_tool_results=deferred_tool_results,
386
- event_stream_handler=self._handle_event_stream if not is_gpt5 else None,
395
+ event_stream_handler=self._handle_event_stream
396
+ if not is_gpt5_byok
397
+ else None,
387
398
  **kwargs,
388
399
  )
389
400
  finally:
@@ -695,12 +706,13 @@ class AgentManager(Widget):
695
706
  if not self.message_history:
696
707
  return None
697
708
  self.last_response = self.message_history[-1]
698
- ## we're searching for unanswered ask_user parts
709
+ ## we're searching for unanswered ask_user or ask_questions parts
699
710
  found_tool = next(
700
711
  (
701
712
  part
702
713
  for part in self.message_history[-1].parts
703
- if isinstance(part, ToolCallPart) and part.tool_name == "ask_user"
714
+ if isinstance(part, ToolCallPart)
715
+ and part.tool_name in ("ask_user", "ask_questions")
704
716
  ),
705
717
  None,
706
718
  )
shotgun/agents/common.py CHANGED
@@ -33,6 +33,7 @@ from .messages import AgentSystemPrompt, SystemStatusPrompt
33
33
  from .models import AgentDeps, AgentRuntimeOptions, PipelineConfigEntry
34
34
  from .tools import (
35
35
  append_file,
36
+ ask_questions,
36
37
  ask_user,
37
38
  codebase_shell,
38
39
  directory_lister,
@@ -179,10 +180,13 @@ def create_base_agent(
179
180
  for tool in additional_tools or []:
180
181
  agent.tool_plain(tool)
181
182
 
182
- # Register interactive tool conditionally based on deps
183
+ # Register interactive tools conditionally based on deps
183
184
  if deps.interactive_mode:
184
185
  agent.tool(ask_user)
185
- logger.debug("📞 Interactive mode enabled - ask_user tool registered")
186
+ agent.tool(ask_questions)
187
+ logger.debug(
188
+ "📞 Interactive mode enabled - ask_user and ask_questions tools registered"
189
+ )
186
190
 
187
191
  # Register common file management tools (always available)
188
192
  agent.tool(write_file)
@@ -323,7 +327,9 @@ def extract_markdown_toc(agent_mode: AgentType | None) -> str | None:
323
327
  if prior_toc:
324
328
  # Add section with XML tags
325
329
  toc_sections.append(
326
- f'<TABLE_OF_CONTENTS file_name="{prior_file}">\n{prior_toc}\n</TABLE_OF_CONTENTS>'
330
+ f'<TABLE_OF_CONTENTS file_name="{prior_file}">\n'
331
+ f"{prior_toc}\n"
332
+ f"</TABLE_OF_CONTENTS>"
327
333
  )
328
334
 
329
335
  # Extract TOC from own file (full detail)
@@ -334,7 +340,9 @@ def extract_markdown_toc(agent_mode: AgentType | None) -> str | None:
334
340
  # Put own file TOC at the beginning with XML tags
335
341
  toc_sections.insert(
336
342
  0,
337
- f'<TABLE_OF_CONTENTS file_name="{config.own_file}">\n{own_toc}\n</TABLE_OF_CONTENTS>',
343
+ f'<TABLE_OF_CONTENTS file_name="{config.own_file}">\n'
344
+ f"{own_toc}\n"
345
+ f"</TABLE_OF_CONTENTS>",
338
346
  )
339
347
 
340
348
  # Combine all sections
@@ -476,7 +484,8 @@ async def add_system_prompt_message(
476
484
  message_history = message_history or []
477
485
 
478
486
  # Create a minimal RunContext to call the system prompt function
479
- # We'll pass None for model and usage since they're not used by our system prompt functions
487
+ # We'll pass None for model and usage since they're not used
488
+ # by our system prompt functions
480
489
  context = type(
481
490
  "RunContext", (), {"deps": deps, "retry": 0, "model": None, "usage": None}
482
491
  )()
@@ -544,7 +553,8 @@ async def run_agent(
544
553
  message_history=messages,
545
554
  deferred_tool_results=results,
546
555
  )
547
- # Apply persistent compaction to prevent cascading token growth in multi-turn loops
556
+ # Apply persistent compaction to prevent cascading token growth
557
+ # in multi-turn loops
548
558
  messages = await apply_persistent_compaction(result.all_messages(), deps)
549
559
 
550
560
  # Log file operations summary if any files were modified
shotgun/agents/models.py CHANGED
@@ -73,6 +73,30 @@ class UserQuestion(BaseModel):
73
73
  )
74
74
 
75
75
 
76
+ class MultipleUserQuestions(BaseModel):
77
+ """Multiple questions to ask the user sequentially."""
78
+
79
+ model_config = ConfigDict(arbitrary_types_allowed=True)
80
+
81
+ questions: list[str] = Field(
82
+ description="List of questions to ask the user",
83
+ )
84
+ current_index: int = Field(
85
+ default=0,
86
+ description="Current question index being asked",
87
+ )
88
+ answers: list[str] = Field(
89
+ default_factory=list,
90
+ description="Accumulated answers from the user",
91
+ )
92
+ tool_call_id: str = Field(
93
+ description="Tool call id",
94
+ )
95
+ result: Future[UserAnswer] = Field(
96
+ description="Future that will contain all answers formatted as Q&A pairs"
97
+ )
98
+
99
+
76
100
  class AgentRuntimeOptions(BaseModel):
77
101
  """User interface options for agents."""
78
102
 
@@ -100,9 +124,9 @@ class AgentRuntimeOptions(BaseModel):
100
124
  description="Maximum number of iterations for agent loops",
101
125
  )
102
126
 
103
- queue: Queue[UserQuestion] = Field(
127
+ queue: Queue[UserQuestion | MultipleUserQuestions] = Field(
104
128
  default_factory=Queue,
105
- description="Queue for storing user responses",
129
+ description="Queue for storing user questions (single or multiple)",
106
130
  )
107
131
 
108
132
  tasks: list[Future[UserAnswer]] = Field(
@@ -1,5 +1,7 @@
1
1
  """Tools package for Pydantic AI agents."""
2
2
 
3
+ from .ask_questions import ask_questions
4
+ from .ask_user import ask_user
3
5
  from .codebase import (
4
6
  codebase_shell,
5
7
  directory_lister,
@@ -8,7 +10,6 @@ from .codebase import (
8
10
  retrieve_code,
9
11
  )
10
12
  from .file_management import append_file, read_file, write_file
11
- from .user_interaction import ask_user
12
13
  from .web_search import (
13
14
  anthropic_web_search_tool,
14
15
  gemini_web_search_tool,
@@ -22,6 +23,7 @@ __all__ = [
22
23
  "gemini_web_search_tool",
23
24
  "get_available_web_search_tools",
24
25
  "ask_user",
26
+ "ask_questions",
25
27
  "read_file",
26
28
  "write_file",
27
29
  "append_file",
@@ -0,0 +1,55 @@
1
+ """Ask multiple questions tool for Pydantic AI agents."""
2
+
3
+ from asyncio import get_running_loop
4
+
5
+ from pydantic_ai import CallDeferred, RunContext
6
+
7
+ from shotgun.agents.models import AgentDeps, MultipleUserQuestions
8
+ from shotgun.logging_config import get_logger
9
+
10
+ logger = get_logger(__name__)
11
+
12
+
13
+ async def ask_questions(ctx: RunContext[AgentDeps], questions: list[str]) -> str:
14
+ """Ask the human multiple questions sequentially and return all Q&A pairs.
15
+
16
+ This tool will display questions one at a time with progress indicators
17
+ (e.g., "Question 1 of 5") and collect all answers before returning them
18
+ formatted together. This provides a better UX than asking questions one by one,
19
+ as users can see their progress and don't need to use multiline input.
20
+
21
+ Do not ask 1 question with multiple parts, or multiple questions inside of it.
22
+ Structure it so each question can be answered independently.
23
+
24
+ Args:
25
+ questions: List of questions to ask the user. Each question should be readable,
26
+ clear, and easy to understand. Use Markdown formatting. Make key phrases
27
+ and words stand out. Questions will be asked in the order provided.
28
+
29
+ Returns:
30
+ All questions and answers formatted as:
31
+ "Q1: {question1}\\nA1: {answer1}\\n\\nQ2: {question2}\\nA2: {answer2}\\n\\n..."
32
+ """
33
+ tool_call_id = ctx.tool_call_id
34
+ assert tool_call_id is not None # noqa: S101
35
+
36
+ try:
37
+ logger.debug("\n👉 Asking %d questions\n", len(questions))
38
+ future = get_running_loop().create_future()
39
+ await ctx.deps.queue.put(
40
+ MultipleUserQuestions(
41
+ questions=questions,
42
+ tool_call_id=tool_call_id,
43
+ result=future,
44
+ )
45
+ )
46
+ ctx.deps.tasks.append(future)
47
+ # Use first question as deferred message preview
48
+ preview = questions[0] if questions else "No questions"
49
+ raise CallDeferred(
50
+ f"Asking {len(questions)} questions starting with: {preview}"
51
+ )
52
+
53
+ except (EOFError, KeyboardInterrupt):
54
+ logger.warning("User input interrupted or unavailable")
55
+ return "User input not available or interrupted"
@@ -1,4 +1,4 @@
1
- """User interaction tools for Pydantic AI agents."""
1
+ """Ask user tool for Pydantic AI agents."""
2
2
 
3
3
  from asyncio import get_running_loop
4
4
 
@@ -7,7 +7,7 @@ Your extensive expertise spans, among other things:
7
7
  ## KEY RULES
8
8
 
9
9
  {% if interactive_mode %}
10
- 0. Always ask CLARIFYING QUESTIONS if the user's request is ambiguous or lacks sufficient detail. Do not make assumptions about what the user wants.
10
+ 0. Always ask CLARIFYING QUESTIONS using the ask_questions() tool if the user's request is ambiguous or lacks sufficient detail. Do not make assumptions about what the user wants.
11
11
  {% endif %}
12
12
  1. Above all, prefer using tools to do the work and NEVER respond with text.
13
13
  2. IMPORTANT: Always ask for review and go ahead to move forward after using write_file().
@@ -20,6 +20,7 @@ Your extensive expertise spans, among other things:
20
20
  9. **Be Creative**: If the user seems not to know something, always be creative and come up with ideas that fit their thinking.
21
21
  10. Greet the user when you're just starting to work.
22
22
  11. DO NOT repeat yourself.
23
+ 12. If a user has agreed to a plan, you DO NOT NEED TO FOLLOW UP with them after every step to ask "is this search query ok?".
23
24
 
24
25
 
25
26
  ## Quality Standards
@@ -20,7 +20,7 @@ from textual.keys import Keys
20
20
  from textual.reactive import reactive
21
21
  from textual.screen import ModalScreen, Screen
22
22
  from textual.widget import Widget
23
- from textual.widgets import Button, Label, Markdown, Static
23
+ from textual.widgets import Button, Label, Static
24
24
 
25
25
  from shotgun.agents.agent_manager import (
26
26
  AgentManager,
@@ -37,6 +37,7 @@ from shotgun.agents.models import (
37
37
  AgentDeps,
38
38
  AgentType,
39
39
  FileOperationTracker,
40
+ MultipleUserQuestions,
40
41
  UserQuestion,
41
42
  )
42
43
  from shotgun.codebase.core.manager import CodebaseAlreadyIndexedError
@@ -114,9 +115,18 @@ class StatusBar(Widget):
114
115
 
115
116
  def render(self) -> str:
116
117
  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
+ return (
119
+ "[$foreground-muted][bold $text]esc[/] to stop • "
120
+ "[bold $text]enter[/] to send • [bold $text]ctrl+j[/] for newline • "
121
+ "[bold $text]ctrl+p[/] command palette • [bold $text]shift+tab[/] cycle modes • "
122
+ "/help for commands[/]"
123
+ )
118
124
  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[/]"""
125
+ return (
126
+ "[$foreground-muted][bold $text]enter[/] to send • "
127
+ "[bold $text]ctrl+j[/] for newline • [bold $text]ctrl+p[/] command palette • "
128
+ "[bold $text]shift+tab[/] cycle modes • /help for commands[/]"
129
+ )
120
130
 
121
131
 
122
132
  class ModeIndicator(Widget):
@@ -149,10 +159,16 @@ class ModeIndicator(Widget):
149
159
  AgentType.EXPORT: "Export",
150
160
  }
151
161
  mode_description = {
152
- AgentType.RESEARCH: "Research topics with web search and synthesize findings",
162
+ AgentType.RESEARCH: (
163
+ "Research topics with web search and synthesize findings"
164
+ ),
153
165
  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",
166
+ AgentType.TASKS: (
167
+ "Generate specific, actionable tasks from research and plans"
168
+ ),
169
+ AgentType.SPECIFY: (
170
+ "Create detailed specifications and requirements documents"
171
+ ),
156
172
  AgentType.EXPORT: "Export artifacts and findings to various formats",
157
173
  }
158
174
 
@@ -163,7 +179,10 @@ class ModeIndicator(Widget):
163
179
  has_content = self.progress_checker.has_mode_content(self.mode)
164
180
  status_icon = " ✓" if has_content else ""
165
181
 
166
- return f"[bold $text-accent]{mode_title}{status_icon} mode[/][$foreground-muted] ({description})[/]"
182
+ return (
183
+ f"[bold $text-accent]{mode_title}{status_icon} mode[/]"
184
+ f"[$foreground-muted] ({description})[/]"
185
+ )
167
186
 
168
187
 
169
188
  class CodebaseIndexPromptScreen(ModalScreen[bool]):
@@ -199,7 +218,8 @@ class CodebaseIndexPromptScreen(ModalScreen[bool]):
199
218
  yield Static(
200
219
  f"Would you like to index the codebase at:\n{Path.cwd()}\n\n"
201
220
  "This is required for the agent to understand your code and answer "
202
- "questions about it. Without indexing, the agent cannot analyze your codebase."
221
+ "questions about it. Without indexing, the agent cannot analyze "
222
+ "your codebase."
203
223
  )
204
224
  with Container(id="index-prompt-buttons"):
205
225
  yield Button(
@@ -238,7 +258,7 @@ class ChatScreen(Screen[None]):
238
258
  history: PromptHistory = PromptHistory()
239
259
  messages = reactive(list[ModelMessage | HintMessage]())
240
260
  working = reactive(False)
241
- question: reactive[UserQuestion | None] = reactive(None)
261
+ question: reactive[UserQuestion | MultipleUserQuestions | None] = reactive(None)
242
262
  indexing_job: reactive[CodebaseIndexSelection | None] = reactive(None)
243
263
  partial_message: reactive[ModelMessage | None] = reactive(None)
244
264
  _current_worker = None # Track the current running worker for cancellation
@@ -378,17 +398,6 @@ class ChatScreen(Screen[None]):
378
398
  chat_history = self.query_one(ChatHistory)
379
399
  chat_history.update_messages(messages)
380
400
 
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
401
  def action_toggle_mode(self) -> None:
393
402
  modes = [
394
403
  AgentType.RESEARCH,
@@ -414,8 +423,38 @@ class ChatScreen(Screen[None]):
414
423
  async def add_question_listener(self) -> None:
415
424
  while True:
416
425
  question = await self.deps.queue.get()
417
- self.question = question
418
- await question.result
426
+
427
+ if isinstance(question, MultipleUserQuestions):
428
+ # Set question state - handle_submit will add Q&A to chat
429
+ question.current_index = 0
430
+ self.question = question
431
+
432
+ # Show intro message with total question count
433
+ num_questions = len(question.questions)
434
+ self.agent_manager.add_hint_message(
435
+ HintMessage(message=f"I'm going to ask {num_questions} questions:")
436
+ )
437
+
438
+ # Show all questions in a numbered list so users can see what's coming
439
+ if question.questions:
440
+ questions_list = "\n".join(
441
+ f"{i + 1}. {q}" for i, q in enumerate(question.questions)
442
+ )
443
+ self.agent_manager.add_hint_message(
444
+ HintMessage(message=questions_list)
445
+ )
446
+
447
+ # Now show the first question prompt to indicate where to start answering
448
+ first_q = question.questions[0]
449
+ self.agent_manager.add_hint_message(
450
+ HintMessage(message=f"**Q1:** {first_q}")
451
+ )
452
+ else:
453
+ # Handle single question (original behavior)
454
+ self.question = question
455
+ await question.result
456
+ self.question = None
457
+
419
458
  self.deps.queue.task_done()
420
459
 
421
460
  def compose(self) -> ComposeResult:
@@ -423,7 +462,6 @@ class ChatScreen(Screen[None]):
423
462
  with Container(id="window"):
424
463
  yield self.agent_manager
425
464
  yield ChatHistory()
426
- yield Markdown(markdown="", id="question-display")
427
465
  with Container(id="footer"):
428
466
  yield Spinner(
429
467
  text="Processing...",
@@ -510,6 +548,8 @@ class ChatScreen(Screen[None]):
510
548
 
511
549
  @on(PromptInput.Submitted)
512
550
  async def handle_submit(self, message: PromptInput.Submitted) -> None:
551
+ from shotgun.agents.models import UserAnswer
552
+
513
553
  text = message.text.strip()
514
554
 
515
555
  # If empty text, just clear input and return
@@ -519,6 +559,63 @@ class ChatScreen(Screen[None]):
519
559
  self.value = ""
520
560
  return
521
561
 
562
+ # Check if we're in a multi-question flow
563
+ if self.question and isinstance(self.question, MultipleUserQuestions):
564
+ q_num = self.question.current_index + 1
565
+
566
+ # Q1 already shown by handle_message_history_updated,
567
+ # Q2+ shown after previous answer. So we only need to add the answer to chat
568
+ self.agent_manager.add_hint_message(
569
+ HintMessage(message=f"**A{q_num}:** {text}")
570
+ )
571
+
572
+ # Store the answer
573
+ self.question.answers.append(text)
574
+
575
+ # Move to next question or finish
576
+ self.question.current_index += 1
577
+
578
+ if self.question.current_index < len(self.question.questions):
579
+ # Show the next question immediately after the answer
580
+ next_q = self.question.questions[self.question.current_index]
581
+ next_q_num = self.question.current_index + 1
582
+ self.agent_manager.add_hint_message(
583
+ HintMessage(message=f"**Q{next_q_num}:** {next_q}")
584
+ )
585
+ else:
586
+ # All questions answered! Format and resolve
587
+ formatted_qa = "\n\n".join(
588
+ f"Q{i + 1}: {q}\nA{i + 1}: {a}"
589
+ for i, (q, a) in enumerate(
590
+ zip(self.question.questions, self.question.answers, strict=True)
591
+ )
592
+ )
593
+
594
+ # Resolve the original future with formatted Q&A (this goes to the agent)
595
+ final_answer = UserAnswer(
596
+ answer=formatted_qa,
597
+ tool_call_id=self.question.tool_call_id,
598
+ )
599
+ self.question.result.set_result(final_answer)
600
+
601
+ # Clear question state
602
+ self.question = None
603
+
604
+ # Clear input first
605
+ prompt_input = self.query_one(PromptInput)
606
+ prompt_input.clear()
607
+ self.value = ""
608
+
609
+ # Send the formatted Q&A directly to the agent (no prefix needed)
610
+ self.run_agent(formatted_qa)
611
+ return
612
+
613
+ # Clear input and return
614
+ prompt_input = self.query_one(PromptInput)
615
+ prompt_input.clear()
616
+ self.value = ""
617
+ return
618
+
522
619
  # Check if it's a command
523
620
  if self.command_handler.is_command(text):
524
621
  success, response = self.command_handler.handle_command(text)
@@ -785,7 +882,9 @@ class ChatScreen(Screen[None]):
785
882
 
786
883
  def help_text_with_codebase(already_indexed: bool = False) -> str:
787
884
  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"
885
+ "Howdy! Welcome to Shotgun - the context tool for software engineering. \n\n"
886
+ "You can research, build specs, plan, create tasks, and export context to your "
887
+ "favorite code-gen agents.\n\n"
789
888
  f"{'' if already_indexed else 'Once your codebase is indexed, '}I can help with:\n\n"
790
889
  "- Speccing out a new feature\n"
791
890
  "- Onboarding you onto this project\n"
@@ -796,7 +895,9 @@ def help_text_with_codebase(already_indexed: bool = False) -> str:
796
895
 
797
896
  def help_text_empty_dir() -> str:
798
897
  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"
898
+ "Howdy! Welcome to Shotgun - the context tool for software engineering.\n\n"
899
+ "You can research, build specs, plan, create tasks, and export context to your "
900
+ "favorite code-gen agents.\n\n"
800
901
  "What would you like to build? Here are some examples:\n\n"
801
902
  "- Research FastAPI vs Django\n"
802
903
  "- Plan my new web app using React\n"
@@ -114,7 +114,7 @@ class ChatHistory(Widget):
114
114
  part
115
115
  for part in prev_item.parts
116
116
  if isinstance(part, ToolReturnPart)
117
- and part.tool_name == "ask_user"
117
+ and part.tool_name in ("ask_user", "ask_questions")
118
118
  ),
119
119
  None,
120
120
  )
@@ -124,7 +124,7 @@ class ChatHistory(Widget):
124
124
  part
125
125
  for part in next_item.parts
126
126
  if isinstance(part, ToolCallPart)
127
- and part.tool_name == "ask_user"
127
+ and part.tool_name in ("ask_user", "ask_questions")
128
128
  ),
129
129
  None,
130
130
  )
@@ -215,8 +215,18 @@ class AgentResponseWidget(Widget):
215
215
  acc = ""
216
216
  if self.item is None:
217
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
+
218
225
  for idx, part in enumerate(self.item.parts):
219
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
220
230
  # Only show the circle prefix if there's actual content
221
231
  if part.content and part.content.strip():
222
232
  acc += f"**⏺** {part.content}\n\n"
@@ -224,7 +234,25 @@ class AgentResponseWidget(Widget):
224
234
  parts_str = self._format_tool_call_part(part)
225
235
  acc += parts_str + "\n\n"
226
236
  elif isinstance(part, BuiltinToolCallPart):
227
- acc += f"{part.tool_name}({part.args})\n\n"
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"
228
256
  elif isinstance(part, BuiltinToolReturnPart):
229
257
  acc += f"builtin tool ({part.tool_name}) return: {part.content}\n\n"
230
258
  elif isinstance(part, ThinkingPart):
@@ -261,6 +289,9 @@ class AgentResponseWidget(Widget):
261
289
  if part.tool_name == "ask_user":
262
290
  return self._format_ask_user_part(part)
263
291
 
292
+ if part.tool_name == "ask_questions":
293
+ return self._format_ask_questions_part(part)
294
+
264
295
  # Parse args once (handles both JSON string and dict)
265
296
  args = self._parse_args(part.args)
266
297
 
@@ -305,12 +336,16 @@ class AgentResponseWidget(Widget):
305
336
  return f'Reading file: "{args["filename"]}"'
306
337
  return "Reading file"
307
338
 
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
- ]:
339
+ # Web search tools - handle variations
340
+ if (
341
+ part.tool_name
342
+ in [
343
+ "openai_web_search_tool",
344
+ "anthropic_web_search_tool",
345
+ "gemini_web_search_tool",
346
+ ]
347
+ or "search" in part.tool_name.lower()
348
+ ): # Catch other search variations
314
349
  if "query" in args:
315
350
  query = self._truncate(str(args["query"]))
316
351
  return f'Searching web: "{query}"'
@@ -332,7 +367,24 @@ class AgentResponseWidget(Widget):
332
367
  return f"{part.tool_name}({args['name']})"
333
368
  return f"▪ {part.tool_name}()"
334
369
 
335
- return f"{part.tool_name}({part.args})"
370
+ # Default case for unrecognized tools - format args properly
371
+ args = self._parse_args(part.args)
372
+ if args and isinstance(args, dict):
373
+ # Try to extract common fields
374
+ if "query" in args:
375
+ return f'{part.tool_name}: "{self._truncate(str(args["query"]))}"'
376
+ elif "question" in args:
377
+ return f'{part.tool_name}: "{self._truncate(str(args["question"]))}"'
378
+ else:
379
+ # Show tool name with truncated args
380
+ args_str = (
381
+ str(part.args)[:50] + "..."
382
+ if len(str(part.args)) > 50
383
+ else str(part.args)
384
+ )
385
+ return f"{part.tool_name}({args_str})"
386
+ else:
387
+ return f"{part.tool_name}()"
336
388
 
337
389
  def _format_ask_user_part(
338
390
  self,
@@ -350,3 +402,10 @@ class AgentResponseWidget(Widget):
350
402
  return f"{_args['question']}"
351
403
  else:
352
404
  return "❓ "
405
+
406
+ def _format_ask_questions_part(
407
+ self,
408
+ part: ToolCallPart,
409
+ ) -> str:
410
+ """Hide ask_questions tool calls - Q&A shown as HintMessages instead."""
411
+ return ""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shotgun-sh
3
- Version: 0.2.5.dev1
3
+ Version: 0.2.6.dev2
4
4
  Summary: AI-powered research, planning, and task management CLI tool
5
5
  Project-URL: Homepage, https://shotgun.sh/
6
6
  Project-URL: Repository, https://github.com/shotgun-sh/shotgun
@@ -8,14 +8,14 @@ shotgun/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
8
  shotgun/sentry_telemetry.py,sha256=VD8es-tREfgtRKhDsEVvqpo0_kM_ab6iVm2lkOEmTlI,2950
9
9
  shotgun/telemetry.py,sha256=WfxdHALh5_51nw783ZZvD-LEyC6ypHxSUTMXUioZhTQ,3339
10
10
  shotgun/agents/__init__.py,sha256=8Jzv1YsDuLyNPFJyckSr_qI4ehTVeDyIMDW4omsfPGc,25
11
- shotgun/agents/agent_manager.py,sha256=xq8L0oAFgtFCpKVsyUoMtYJqUyz5XxjWLKNnxoe1zo4,26577
12
- shotgun/agents/common.py,sha256=BINebrcS9BcHu7yDH2yxTjChCmQoUCUrmUMlRnz4_JY,19265
11
+ shotgun/agents/agent_manager.py,sha256=3Ww9PyYtRCdi84-u8O9cV5zyHC_Np4R1HiJ2JZbA4hY,27013
12
+ shotgun/agents/common.py,sha256=PAcVimooOhB4j8iCQkAgs72gGEIJELbT3HoF7UH-ESs,19456
13
13
  shotgun/agents/conversation_history.py,sha256=5J8_1yxdZiiWTq22aDio88DkBDZ4_Lh_p5Iy5_ENszc,3898
14
14
  shotgun/agents/conversation_manager.py,sha256=fxAvXbEl3Cl2ugJ4N9aWXaqZtkrnfj3QzwjWC4LFXwI,3514
15
15
  shotgun/agents/export.py,sha256=Zke952DbJ_lOBUmN-TPHw7qmjbfqsFu1uycBRQI_pkg,2969
16
16
  shotgun/agents/llm.py,sha256=hs8j1wwTczGtehzahL1Z_5D4qus5QUx4-h9-m5ZPzm4,2209
17
17
  shotgun/agents/messages.py,sha256=wNn0qC5AqASM8LMaSGFOerZEJPn5FsIOmaJs1bdosuU,1036
18
- shotgun/agents/models.py,sha256=IvwwjbJYi5wi9S-budg8g1ezi1VaO57Q-XtegkbTrXg,8096
18
+ shotgun/agents/models.py,sha256=Dg6ZTYxdwuTEZjfMATORoyWImMNRkXWS1Ec6o2m1kXI,8842
19
19
  shotgun/agents/plan.py,sha256=s-WfILBOW4l8kY59RUOVtX5MJSuSzFm1nGp6b17If78,3030
20
20
  shotgun/agents/research.py,sha256=lYG7Rytcitop8mXs3isMI3XvYzzI3JH9u0VZz6K9zfo,3274
21
21
  shotgun/agents/specify.py,sha256=7MoMxfIn34G27mw6wrp_F0i2O5rid476L3kHFONDCd0,3137
@@ -41,9 +41,10 @@ shotgun/agents/history/token_counting/openai.py,sha256=XJ2z2HaUG6f3Cw9tCK_yaOsaM
41
41
  shotgun/agents/history/token_counting/sentencepiece_counter.py,sha256=qj1bT7J5nCd5y6Mr42O9K1KTaele0rjdd09FeyyEA70,3987
42
42
  shotgun/agents/history/token_counting/tokenizer_cache.py,sha256=Y0V6KMtEwn42M5-zJGAc7YudM8X6m5-j2ekA6YGL5Xk,2868
43
43
  shotgun/agents/history/token_counting/utils.py,sha256=d124IDjtd0IYBYrr3gDJGWxSbdP10Vrc7ZistbUosMg,5002
44
- shotgun/agents/tools/__init__.py,sha256=QaN80IqWvB5qEcjHqri1-PYvYlO74vdhcwLugoEdblo,772
44
+ shotgun/agents/tools/__init__.py,sha256=O1re6tyVIXDI2Jbuakb33njJeJoNMXHiwsmJxWjbNqo,826
45
+ shotgun/agents/tools/ask_questions.py,sha256=527gRWt97I-MGknSDC3UHDMTVLlCEBAqV22tglFI-xo,2193
46
+ shotgun/agents/tools/ask_user.py,sha256=u_5s3SwnIhmfk5IIRBmqUA7TspvDeDya_pqoP2ACd3I,1227
45
47
  shotgun/agents/tools/file_management.py,sha256=HYNe_QA4T3_bPzSWBYcFZcnWdj8eb4aQ3GB735-G8Nw,7138
46
- shotgun/agents/tools/user_interaction.py,sha256=b3ncEpvoD06Cz4hwsS-ppVbQajQj640iWnVfA5WBjAA,1236
47
48
  shotgun/agents/tools/codebase/__init__.py,sha256=ceAGkK006NeOYaIJBLQsw7Q46sAyCRK9PYDs8feMQVw,661
48
49
  shotgun/agents/tools/codebase/codebase_shell.py,sha256=9b7ZStAVFprdGqp1O23ZgwkToMytlUdp_R4MhvmENhc,8584
49
50
  shotgun/agents/tools/codebase/directory_lister.py,sha256=eX5GKDSmbKggKDvjPpYMa2WPSGPYQAtUEZ4eN01T0t8,4703
@@ -94,7 +95,7 @@ shotgun/prompts/agents/research.j2,sha256=JBtjXaMVDRuNTt7-Ai8gUb2InfolfqCkQoEkn9
94
95
  shotgun/prompts/agents/specify.j2,sha256=AP7XrA3KE7GZsCvW4guASxZHBM2mnrMw3irdZ3RUOBs,2808
95
96
  shotgun/prompts/agents/tasks.j2,sha256=9gGCimCWVvpaQSxkAjt7WmIxFHXJY2FlmqhqomFxQTA,5949
96
97
  shotgun/prompts/agents/partials/codebase_understanding.j2,sha256=7WH-PVd-TRBFQUdOdKkwwn9hAUaJznFZMAGHhO7IGGU,5633
97
- shotgun/prompts/agents/partials/common_agent_system_prompt.j2,sha256=eFuc3z1pSJzQtPJfjMIDNHv5XX9lP6YVrmKcbbskJj8,1877
98
+ shotgun/prompts/agents/partials/common_agent_system_prompt.j2,sha256=iWYWOboW7osxfUmC82W9vhyKYbylZjHGF70hr51IUos,2035
98
99
  shotgun/prompts/agents/partials/content_formatting.j2,sha256=MG0JB7SSp8YV5akDWpbs2f9DcdREIYqLp38NnoWLeQ0,1854
99
100
  shotgun/prompts/agents/partials/interactive_mode.j2,sha256=9sYPbyc46HXg3k1FT_LugIQvOyNDnMQwsMIgOgN-_aY,1100
100
101
  shotgun/prompts/agents/state/system_state.j2,sha256=RPweqBYmgWMiDuOjdEDl6NLgYU7HMaUGW1jBSnD5UzQ,1270
@@ -128,7 +129,7 @@ shotgun/tui/components/prompt_input.py,sha256=Ss-htqraHZAPaehGE4x86ij0veMjc4Ugad
128
129
  shotgun/tui/components/spinner.py,sha256=ovTDeaJ6FD6chZx_Aepia6R3UkPOVJ77EKHfRmn39MY,2427
129
130
  shotgun/tui/components/splash.py,sha256=vppy9vEIEvywuUKRXn2y11HwXSRkQZHLYoVjhDVdJeU,1267
130
131
  shotgun/tui/components/vertical_tail.py,sha256=kROwTaRjUwVB7H35dtmNcUVPQqNYvvfq7K2tXBKEb6c,638
131
- shotgun/tui/screens/chat.py,sha256=Yb5zWpWVmvtIFjO1jkhU6piJyGVc9XdTErNd6kUbjjw,30389
132
+ shotgun/tui/screens/chat.py,sha256=ICDW3s-pkWvEzUC6YL3BLZcb2H8Dd5uzDtDWaqtZqms,33920
132
133
  shotgun/tui/screens/chat.tcss,sha256=2Yq3E23jxsySYsgZf4G1AYrYVcpX0UDW6kNNI0tDmtM,437
133
134
  shotgun/tui/screens/directory_setup.py,sha256=lIZ1J4A6g5Q2ZBX8epW7BhR96Dmdcg22CyiM5S-I5WU,3237
134
135
  shotgun/tui/screens/feedback.py,sha256=VxpW0PVxMp22ZvSfQkTtgixNrpEOlfWtekjqlVfYEjA,5708
@@ -140,7 +141,7 @@ shotgun/tui/screens/welcome.py,sha256=r-RjtzjMhAaA0XKgnYKdZOuE07bcBi223fleKEf-Fa
140
141
  shotgun/tui/screens/chat_screen/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
141
142
  shotgun/tui/screens/chat_screen/command_providers.py,sha256=7Xnxd4k30bpLOMZSX32bcugU4IgpqU4Y8f6eHWKXd4o,12694
142
143
  shotgun/tui/screens/chat_screen/hint_message.py,sha256=WOpbk8q7qt7eOHTyyHvh_IQIaublVDeJGaLpsxEk9FA,933
143
- shotgun/tui/screens/chat_screen/history.py,sha256=Go859iEjw0s5aELKpF42MjLXy7UFQ52XnJMTIkV3aLo,12406
144
+ shotgun/tui/screens/chat_screen/history.py,sha256=N_dUXA6KaX3O3Km2wXQWeAODIMIYUmYiwboP3lA-dns,14904
144
145
  shotgun/tui/utils/__init__.py,sha256=cFjDfoXTRBq29wgP7TGRWUu1eFfiIG-LLOzjIGfadgI,150
145
146
  shotgun/tui/utils/mode_progress.py,sha256=lseRRo7kMWLkBzI3cU5vqJmS2ZcCjyRYf9Zwtvc-v58,10931
146
147
  shotgun/utils/__init__.py,sha256=WinIEp9oL2iMrWaDkXz2QX4nYVPAm8C9aBSKTeEwLtE,198
@@ -149,8 +150,8 @@ shotgun/utils/env_utils.py,sha256=ulM3BRi9ZhS7uC-zorGeDQm4SHvsyFuuU9BtVPqdrHY,14
149
150
  shotgun/utils/file_system_utils.py,sha256=l-0p1bEHF34OU19MahnRFdClHufThfGAjQ431teAIp0,1004
150
151
  shotgun/utils/source_detection.py,sha256=Co6Q03R3fT771TF3RzB-70stfjNP2S4F_ArZKibwzm8,454
151
152
  shotgun/utils/update_checker.py,sha256=IgzPHRhS1ETH7PnJR_dIx6lxgr1qHpCkMTgzUxvGjhI,7586
152
- shotgun_sh-0.2.5.dev1.dist-info/METADATA,sha256=a7NtlC-7j0n4tJwtUOF49DcSZ4qWCL9IxV9GvsOLoFQ,11226
153
- shotgun_sh-0.2.5.dev1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
154
- shotgun_sh-0.2.5.dev1.dist-info/entry_points.txt,sha256=asZxLU4QILneq0MWW10saVCZc4VWhZfb0wFZvERnzfA,45
155
- shotgun_sh-0.2.5.dev1.dist-info/licenses/LICENSE,sha256=YebsZl590zCHrF_acCU5pmNt0pnAfD2DmAnevJPB1tY,1065
156
- shotgun_sh-0.2.5.dev1.dist-info/RECORD,,
153
+ shotgun_sh-0.2.6.dev2.dist-info/METADATA,sha256=bgbHWiHo8sV8ypBzgCq8SxOfYfg19-uETwVRMRWcGnY,11226
154
+ shotgun_sh-0.2.6.dev2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
155
+ shotgun_sh-0.2.6.dev2.dist-info/entry_points.txt,sha256=asZxLU4QILneq0MWW10saVCZc4VWhZfb0wFZvERnzfA,45
156
+ shotgun_sh-0.2.6.dev2.dist-info/licenses/LICENSE,sha256=YebsZl590zCHrF_acCU5pmNt0pnAfD2DmAnevJPB1tY,1065
157
+ shotgun_sh-0.2.6.dev2.dist-info/RECORD,,