shotgun-sh 0.1.0.dev20__py3-none-any.whl → 0.1.0.dev23__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 (60) hide show
  1. shotgun/agents/agent_manager.py +100 -16
  2. shotgun/agents/common.py +142 -28
  3. shotgun/agents/conversation_history.py +56 -0
  4. shotgun/agents/conversation_manager.py +105 -0
  5. shotgun/agents/export.py +5 -2
  6. shotgun/agents/models.py +21 -7
  7. shotgun/agents/plan.py +2 -1
  8. shotgun/agents/research.py +2 -1
  9. shotgun/agents/specify.py +2 -1
  10. shotgun/agents/tasks.py +5 -2
  11. shotgun/agents/tools/codebase/codebase_shell.py +2 -2
  12. shotgun/agents/tools/codebase/directory_lister.py +1 -1
  13. shotgun/agents/tools/codebase/file_read.py +1 -1
  14. shotgun/agents/tools/codebase/query_graph.py +1 -1
  15. shotgun/agents/tools/codebase/retrieve_code.py +1 -1
  16. shotgun/agents/tools/file_management.py +67 -2
  17. shotgun/main.py +9 -1
  18. shotgun/prompts/agents/export.j2 +14 -11
  19. shotgun/prompts/agents/partials/codebase_understanding.j2 +9 -0
  20. shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +6 -9
  21. shotgun/prompts/agents/plan.j2 +9 -13
  22. shotgun/prompts/agents/research.j2 +11 -14
  23. shotgun/prompts/agents/specify.j2 +9 -12
  24. shotgun/prompts/agents/state/codebase/codebase_graphs_available.j2 +5 -1
  25. shotgun/prompts/agents/state/system_state.j2 +27 -5
  26. shotgun/prompts/agents/tasks.j2 +12 -12
  27. shotgun/sdk/models.py +1 -1
  28. shotgun/sdk/services.py +0 -14
  29. shotgun/tui/app.py +9 -4
  30. shotgun/tui/screens/chat.py +92 -30
  31. shotgun/tui/screens/chat_screen/command_providers.py +1 -1
  32. shotgun/tui/screens/chat_screen/history.py +6 -0
  33. shotgun/tui/utils/__init__.py +5 -0
  34. shotgun/tui/utils/mode_progress.py +257 -0
  35. {shotgun_sh-0.1.0.dev20.dist-info → shotgun_sh-0.1.0.dev23.dist-info}/METADATA +8 -9
  36. {shotgun_sh-0.1.0.dev20.dist-info → shotgun_sh-0.1.0.dev23.dist-info}/RECORD +39 -56
  37. shotgun/agents/artifact_state.py +0 -58
  38. shotgun/agents/tools/artifact_management.py +0 -481
  39. shotgun/artifacts/__init__.py +0 -17
  40. shotgun/artifacts/exceptions.py +0 -89
  41. shotgun/artifacts/manager.py +0 -530
  42. shotgun/artifacts/models.py +0 -334
  43. shotgun/artifacts/service.py +0 -463
  44. shotgun/artifacts/templates/__init__.py +0 -10
  45. shotgun/artifacts/templates/loader.py +0 -252
  46. shotgun/artifacts/templates/models.py +0 -136
  47. shotgun/artifacts/templates/plan/delivery_and_release_plan.yaml +0 -66
  48. shotgun/artifacts/templates/research/market_research.yaml +0 -585
  49. shotgun/artifacts/templates/research/sdk_comparison.yaml +0 -257
  50. shotgun/artifacts/templates/specify/prd.yaml +0 -331
  51. shotgun/artifacts/templates/specify/product_spec.yaml +0 -301
  52. shotgun/artifacts/utils.py +0 -76
  53. shotgun/prompts/agents/partials/artifact_system.j2 +0 -32
  54. shotgun/prompts/agents/state/artifact_templates_available.j2 +0 -20
  55. shotgun/prompts/agents/state/existing_artifacts_available.j2 +0 -25
  56. shotgun/sdk/artifact_models.py +0 -186
  57. shotgun/sdk/artifacts.py +0 -448
  58. {shotgun_sh-0.1.0.dev20.dist-info → shotgun_sh-0.1.0.dev23.dist-info}/WHEEL +0 -0
  59. {shotgun_sh-0.1.0.dev20.dist-info → shotgun_sh-0.1.0.dev23.dist-info}/entry_points.txt +0 -0
  60. {shotgun_sh-0.1.0.dev20.dist-info → shotgun_sh-0.1.0.dev23.dist-info}/licenses/LICENSE +0 -0
shotgun/tui/app.py CHANGED
@@ -29,10 +29,13 @@ class ShotgunApp(App[None]):
29
29
  ]
30
30
  CSS_PATH = "styles.tcss"
31
31
 
32
- def __init__(self, no_update_check: bool = False) -> None:
32
+ def __init__(
33
+ self, no_update_check: bool = False, continue_session: bool = False
34
+ ) -> None:
33
35
  super().__init__()
34
36
  self.config_manager: ConfigManager = get_config_manager()
35
37
  self.no_update_check = no_update_check
38
+ self.continue_session = continue_session
36
39
  self.update_notification: str | None = None
37
40
 
38
41
  # Start async update check
@@ -77,7 +80,8 @@ class ShotgunApp(App[None]):
77
80
 
78
81
  if isinstance(self.screen, ChatScreen):
79
82
  return
80
- self.push_screen("chat")
83
+ # Pass continue_session flag to ChatScreen
84
+ self.push_screen(ChatScreen(continue_session=self.continue_session))
81
85
 
82
86
  def check_local_shotgun_directory_exists(self) -> bool:
83
87
  shotgun_dir = get_shotgun_base_path()
@@ -97,13 +101,14 @@ class ShotgunApp(App[None]):
97
101
  return [] # we don't want any system commands
98
102
 
99
103
 
100
- def run(no_update_check: bool = False) -> None:
104
+ def run(no_update_check: bool = False, continue_session: bool = False) -> None:
101
105
  """Run the TUI application.
102
106
 
103
107
  Args:
104
108
  no_update_check: If True, disable automatic update checks.
109
+ continue_session: If True, continue from previous conversation.
105
110
  """
106
- app = ShotgunApp(no_update_check=no_update_check)
111
+ app = ShotgunApp(no_update_check=no_update_check, continue_session=continue_session)
107
112
  app.run(inline_no_clear=True)
108
113
 
109
114
 
@@ -22,13 +22,18 @@ from textual.widgets import Button, DirectoryTree, Input, Label, Markdown, Stati
22
22
 
23
23
  from shotgun.agents.agent_manager import (
24
24
  AgentManager,
25
- AgentType,
26
25
  MessageHistoryUpdated,
27
26
  PartialResponseMessage,
28
27
  )
29
28
  from shotgun.agents.config import get_provider_model
29
+ from shotgun.agents.conversation_history import (
30
+ ConversationHistory,
31
+ ConversationState,
32
+ )
33
+ from shotgun.agents.conversation_manager import ConversationManager
30
34
  from shotgun.agents.models import (
31
35
  AgentDeps,
36
+ AgentType,
32
37
  FileOperationTracker,
33
38
  UserAnswer,
34
39
  UserQuestion,
@@ -36,12 +41,13 @@ from shotgun.agents.models import (
36
41
  from shotgun.codebase.core.manager import CodebaseAlreadyIndexedError
37
42
  from shotgun.sdk.codebase import CodebaseSDK
38
43
  from shotgun.sdk.exceptions import CodebaseNotFoundError, InvalidPathError
39
- from shotgun.sdk.services import get_artifact_service, get_codebase_service
44
+ from shotgun.sdk.services import get_codebase_service
40
45
  from shotgun.tui.commands import CommandHandler
41
46
  from shotgun.tui.screens.chat_screen.history import ChatHistory
42
47
 
43
48
  from ..components.prompt_input import PromptInput
44
49
  from ..components.spinner import Spinner
50
+ from ..utils.mode_progress import PlaceholderHints
45
51
  from .chat_screen.command_providers import (
46
52
  AgentModeProvider,
47
53
  CodebaseCommandProvider,
@@ -116,6 +122,7 @@ class ModeIndicator(Widget):
116
122
  """
117
123
  super().__init__()
118
124
  self.mode = mode
125
+ self.progress_checker = PlaceholderHints().progress_checker
119
126
 
120
127
  def render(self) -> str:
121
128
  """Render the mode indicator."""
@@ -137,7 +144,11 @@ class ModeIndicator(Widget):
137
144
  mode_title = mode_display.get(self.mode, self.mode.value.title())
138
145
  description = mode_description.get(self.mode, "")
139
146
 
140
- return f"[bold $text-accent]{mode_title} mode[/][$foreground-muted] ({description})[/]"
147
+ # Check if mode has content
148
+ has_content = self.progress_checker.has_mode_content(self.mode)
149
+ status_icon = " ✓" if has_content else ""
150
+
151
+ return f"[bold $text-accent]{mode_title}{status_icon} mode[/][$foreground-muted] ({description})[/]"
141
152
 
142
153
 
143
154
  class FilteredDirectoryTree(DirectoryTree):
@@ -174,9 +185,11 @@ class CodebaseIndexPromptScreen(ModalScreen[bool]):
174
185
 
175
186
  def compose(self) -> ComposeResult:
176
187
  with Container(id="index-prompt-dialog"):
177
- yield Label("Index your codebase?", id="index-prompt-title")
188
+ yield Label("Index this codebase?", id="index-prompt-title")
178
189
  yield Static(
179
- "We found project files but no index yet. Indexing enables smarter chat."
190
+ f"Would you like to index the codebase at:\n{Path.cwd()}\n\n"
191
+ "This is required for the agent to understand your code and answer "
192
+ "questions about it. Without indexing, the agent cannot analyze your codebase."
180
193
  )
181
194
  with Container(id="index-prompt-buttons"):
182
195
  yield Button(
@@ -306,24 +319,6 @@ class ChatScreen(Screen[None]):
306
319
 
307
320
  COMMANDS = {AgentModeProvider, ProviderSetupProvider, CodebaseCommandProvider}
308
321
 
309
- _PLACEHOLDER_BY_MODE: dict[AgentType, str] = {
310
- AgentType.RESEARCH: (
311
- "Ask for investigations, e.g. research strengths and weaknesses of PydanticAI vs its rivals"
312
- ),
313
- AgentType.PLAN: (
314
- "Describe a goal to plan, e.g. draft a rollout plan for launching our Slack automation"
315
- ),
316
- AgentType.TASKS: (
317
- "Request actionable work, e.g. break down tasks to wire OpenTelemetry into the API"
318
- ),
319
- AgentType.SPECIFY: (
320
- "Request detailed specifications, e.g. create a comprehensive spec for user authentication system"
321
- ),
322
- AgentType.EXPORT: (
323
- "Request export tasks, e.g. export research findings to Markdown or convert tasks to CSV"
324
- ),
325
- }
326
-
327
322
  value = reactive("")
328
323
  mode = reactive(AgentType.RESEARCH)
329
324
  history: PromptHistory = PromptHistory()
@@ -333,12 +328,11 @@ class ChatScreen(Screen[None]):
333
328
  indexing_job: reactive[CodebaseIndexSelection | None] = reactive(None)
334
329
  partial_message: reactive[ModelMessage | None] = reactive(None)
335
330
 
336
- def __init__(self) -> None:
331
+ def __init__(self, continue_session: bool = False) -> None:
337
332
  super().__init__()
338
333
  # Get the model configuration and services
339
334
  model_config = get_provider_model()
340
335
  codebase_service = get_codebase_service()
341
- artifact_service = get_artifact_service()
342
336
  self.codebase_sdk = CodebaseSDK()
343
337
 
344
338
  # Create shared deps without system_prompt_fn (agents provide their own)
@@ -350,18 +344,26 @@ class ChatScreen(Screen[None]):
350
344
 
351
345
  self.deps = AgentDeps(
352
346
  interactive_mode=True,
347
+ is_tui_context=True,
353
348
  llm_model=model_config,
354
349
  codebase_service=codebase_service,
355
- artifact_service=artifact_service,
356
350
  system_prompt_fn=_placeholder_system_prompt_fn,
357
351
  )
358
352
  self.agent_manager = AgentManager(deps=self.deps, initial_type=self.mode)
359
353
  self.command_handler = CommandHandler()
354
+ self.placeholder_hints = PlaceholderHints()
355
+ self.conversation_manager = ConversationManager()
356
+ self.continue_session = continue_session
360
357
 
361
358
  def on_mount(self) -> None:
362
359
  self.query_one(PromptInput).focus(scroll_visible=True)
363
360
  # Hide spinner initially
364
361
  self.query_one("#spinner").display = False
362
+
363
+ # Load conversation history if --continue flag was provided
364
+ if self.continue_session and self.conversation_manager.exists():
365
+ self._load_conversation()
366
+
365
367
  self.call_later(self.check_if_codebase_is_indexed)
366
368
  # Start the question listener worker to handle ask_user interactions
367
369
  self.call_later(self.add_question_listener)
@@ -407,7 +409,10 @@ class ChatScreen(Screen[None]):
407
409
  mode_indicator.refresh()
408
410
 
409
411
  prompt_input = self.query_one(PromptInput)
410
- prompt_input.placeholder = self._placeholder_for_mode(new_mode)
412
+ # Force new hint selection when mode changes
413
+ prompt_input.placeholder = self._placeholder_for_mode(
414
+ new_mode, force_new=True
415
+ )
411
416
  prompt_input.refresh()
412
417
 
413
418
  def watch_working(self, is_working: bool) -> None:
@@ -483,6 +488,10 @@ class ChatScreen(Screen[None]):
483
488
  if not chat_history.vertical_tail:
484
489
  return
485
490
  chat_history.vertical_tail.mount(Markdown(markdown))
491
+ # Scroll to bottom after mounting hint
492
+ chat_history.vertical_tail.call_after_refresh(
493
+ chat_history.vertical_tail.scroll_end, animate=False
494
+ )
486
495
 
487
496
  @on(PartialResponseMessage)
488
497
  def handle_partial_response(self, event: PartialResponseMessage) -> None:
@@ -503,6 +512,14 @@ class ChatScreen(Screen[None]):
503
512
  self._clear_partial_response()
504
513
  self.messages = event.messages
505
514
 
515
+ # Refresh placeholder and mode indicator in case artifacts were created
516
+ prompt_input = self.query_one(PromptInput)
517
+ prompt_input.placeholder = self._placeholder_for_mode(self.mode)
518
+ prompt_input.refresh()
519
+
520
+ mode_indicator = self.query_one(ModeIndicator)
521
+ mode_indicator.refresh()
522
+
506
523
  # If there are file operations, add a message showing the modified files
507
524
  if event.file_operations:
508
525
  chat_history = self.query_one(ChatHistory)
@@ -577,9 +594,17 @@ class ChatScreen(Screen[None]):
577
594
  prompt_input = self.query_one(PromptInput)
578
595
  prompt_input.clear()
579
596
 
580
- def _placeholder_for_mode(self, mode: AgentType) -> str:
581
- """Return the placeholder text appropriate for the current mode."""
582
- return self._PLACEHOLDER_BY_MODE.get(mode, "Type your message")
597
+ def _placeholder_for_mode(self, mode: AgentType, force_new: bool = False) -> str:
598
+ """Return the placeholder text appropriate for the current mode.
599
+
600
+ Args:
601
+ mode: The current agent mode.
602
+ force_new: If True, force selection of a new random hint.
603
+
604
+ Returns:
605
+ Dynamic placeholder hint based on mode and progress.
606
+ """
607
+ return self.placeholder_hints.get_placeholder_for_mode(mode)
583
608
 
584
609
  def index_codebase_command(self) -> None:
585
610
  start_path = Path.cwd()
@@ -677,9 +702,46 @@ class ChatScreen(Screen[None]):
677
702
  )
678
703
  self.working = False
679
704
 
705
+ # Save conversation after each interaction
706
+ self._save_conversation()
707
+
680
708
  prompt_input = self.query_one(PromptInput)
681
709
  prompt_input.focus()
682
710
 
711
+ def _save_conversation(self) -> None:
712
+ """Save the current conversation to persistent storage."""
713
+ # Get conversation state from agent manager
714
+ state = self.agent_manager.get_conversation_state()
715
+
716
+ # Create conversation history object
717
+ conversation = ConversationHistory(
718
+ last_agent_model=state.agent_type,
719
+ )
720
+ conversation.set_agent_messages(state.agent_messages)
721
+
722
+ # Save to file
723
+ self.conversation_manager.save(conversation)
724
+
725
+ def _load_conversation(self) -> None:
726
+ """Load conversation from persistent storage."""
727
+ conversation = self.conversation_manager.load()
728
+ if conversation is None:
729
+ return
730
+
731
+ # Restore agent state
732
+ agent_messages = conversation.get_agent_messages()
733
+
734
+ # Create ConversationState for restoration
735
+ state = ConversationState(
736
+ agent_messages=agent_messages,
737
+ agent_type=conversation.last_agent_model,
738
+ )
739
+
740
+ self.agent_manager.restore_conversation_state(state)
741
+
742
+ # Update the current mode
743
+ self.mode = AgentType(conversation.last_agent_model)
744
+
683
745
 
684
746
  def codebase_indexed_hint(codebase_name: str) -> str:
685
747
  return (
@@ -3,7 +3,7 @@ from typing import TYPE_CHECKING, cast
3
3
 
4
4
  from textual.command import DiscoveryHit, Hit, Provider
5
5
 
6
- from shotgun.agents.agent_manager import AgentType
6
+ from shotgun.agents.models import AgentType
7
7
  from shotgun.codebase.models import CodebaseGraph
8
8
 
9
9
  if TYPE_CHECKING:
@@ -187,6 +187,12 @@ class AgentResponseWidget(Widget):
187
187
  def _format_tool_call_part(self, part: ToolCallPart) -> str:
188
188
  if part.tool_name == "ask_user":
189
189
  return self._format_ask_user_part(part)
190
+ # write_file
191
+ if part.tool_name == "write_file" or part.tool_name == "append_file":
192
+ if isinstance(part.args, dict) and "filename" in part.args:
193
+ return f"{part.tool_name}({part.args['filename']})"
194
+ else:
195
+ return f"{part.tool_name}()"
190
196
  if part.tool_name == "write_artifact_section":
191
197
  if isinstance(part.args, dict) and "section_title" in part.args:
192
198
  return f"{part.tool_name}({part.args['section_title']})"
@@ -0,0 +1,5 @@
1
+ """TUI utilities package."""
2
+
3
+ from .mode_progress import ModeProgressChecker, PlaceholderHints
4
+
5
+ __all__ = ["ModeProgressChecker", "PlaceholderHints"]
@@ -0,0 +1,257 @@
1
+ """Utility module for checking mode progress in .shotgun directories."""
2
+
3
+ import random
4
+ from pathlib import Path
5
+
6
+ from shotgun.agents.models import AgentType
7
+ from shotgun.utils.file_system_utils import get_shotgun_base_path
8
+
9
+
10
+ class ModeProgressChecker:
11
+ """Checks progress across different agent modes based on file contents."""
12
+
13
+ # Minimum file size in characters to consider a mode as "started"
14
+ MIN_CONTENT_SIZE = 20
15
+
16
+ # Map agent types to their corresponding files (in workflow order)
17
+ MODE_FILES = {
18
+ AgentType.RESEARCH: "research.md",
19
+ AgentType.SPECIFY: "specification.md",
20
+ AgentType.PLAN: "plan.md",
21
+ AgentType.TASKS: "tasks.md",
22
+ AgentType.EXPORT: "exports/", # Export mode creates files in exports folder
23
+ }
24
+
25
+ def __init__(self, base_path: Path | None = None):
26
+ """Initialize the progress checker.
27
+
28
+ Args:
29
+ base_path: Base path for .shotgun directory. Defaults to current directory.
30
+ """
31
+ self.base_path = base_path or get_shotgun_base_path()
32
+
33
+ def has_mode_content(self, mode: AgentType) -> bool:
34
+ """Check if a mode has meaningful content.
35
+
36
+ Args:
37
+ mode: The agent mode to check.
38
+
39
+ Returns:
40
+ True if the mode has a file with >20 characters.
41
+ """
42
+ if mode not in self.MODE_FILES:
43
+ return False
44
+
45
+ file_or_dir = self.MODE_FILES[mode]
46
+
47
+ # Special handling for export mode (checks directory)
48
+ if mode == AgentType.EXPORT:
49
+ export_path = self.base_path / file_or_dir
50
+ if export_path.exists() and export_path.is_dir():
51
+ # Check if any files exist in exports directory
52
+ for item in export_path.glob("*"):
53
+ if item.is_file() and not item.name.startswith("."):
54
+ try:
55
+ content = item.read_text(encoding="utf-8")
56
+ if len(content.strip()) > self.MIN_CONTENT_SIZE:
57
+ return True
58
+ except (OSError, UnicodeDecodeError):
59
+ continue
60
+ return False
61
+
62
+ # Check single file for other modes
63
+ file_path = self.base_path / file_or_dir
64
+ if not file_path.exists() or not file_path.is_file():
65
+ return False
66
+
67
+ try:
68
+ content = file_path.read_text(encoding="utf-8")
69
+ # Check if file has meaningful content
70
+ return len(content.strip()) > self.MIN_CONTENT_SIZE
71
+ except (OSError, UnicodeDecodeError):
72
+ return False
73
+
74
+ def get_next_suggested_mode(self, current_mode: AgentType) -> AgentType | None:
75
+ """Get the next suggested mode based on current progress.
76
+
77
+ Args:
78
+ current_mode: The current agent mode.
79
+
80
+ Returns:
81
+ The next suggested mode, or None if no suggestion.
82
+ """
83
+ mode_order = [
84
+ AgentType.RESEARCH,
85
+ AgentType.SPECIFY,
86
+ AgentType.TASKS,
87
+ AgentType.EXPORT,
88
+ ]
89
+
90
+ try:
91
+ current_index = mode_order.index(current_mode)
92
+ except ValueError:
93
+ # Mode not in standard order (e.g., PLAN mode)
94
+ return None
95
+
96
+ # Check if current mode has content
97
+ if not self.has_mode_content(current_mode):
98
+ # Current mode is empty, no suggestion for next mode
99
+ return None
100
+
101
+ # Get next mode in sequence
102
+ if current_index < len(mode_order) - 1:
103
+ return mode_order[current_index + 1]
104
+
105
+ # Export mode cycles back to Research
106
+ return mode_order[0]
107
+
108
+
109
+ class PlaceholderHints:
110
+ """Manages dynamic placeholder hints for each mode based on progress."""
111
+
112
+ # Placeholder variations for each mode and state
113
+ HINTS = {
114
+ # Research mode
115
+ AgentType.RESEARCH: {
116
+ False: [
117
+ "Research a product or idea (SHIFT+TAB to cycle modes)",
118
+ "What would you like to explore? Start your research journey here (SHIFT+TAB to switch modes)",
119
+ "Dive into discovery mode - research anything that sparks curiosity (SHIFT+TAB for mode menu)",
120
+ "Ready to investigate? Feed me your burning questions (SHIFT+TAB to explore other modes)",
121
+ " 🔍 The research rabbit hole awaits! What shall we uncover? (SHIFT+TAB for mode carousel)",
122
+ ],
123
+ True: [
124
+ "Research complete! SHIFT+TAB to move to Specify mode",
125
+ "Great research! Time to specify (SHIFT+TAB to Specify mode)",
126
+ "Research done! Ready to create specifications (SHIFT+TAB to Specify)",
127
+ "Findings gathered! Move to specifications (SHIFT+TAB for Specify mode)",
128
+ " 🎯 Research complete! Advance to Specify mode (SHIFT+TAB)",
129
+ ],
130
+ },
131
+ # Specify mode
132
+ AgentType.SPECIFY: {
133
+ False: [
134
+ "Create detailed specifications and requirements (SHIFT+TAB to switch modes)",
135
+ "Define your project specifications here (SHIFT+TAB to navigate modes)",
136
+ "Time to get specific - write comprehensive specs (SHIFT+TAB for mode options)",
137
+ "Specification station: Document requirements and designs (SHIFT+TAB to change modes)",
138
+ " 📋 Spec-tacular time! Let's architect your ideas (SHIFT+TAB for mode magic)",
139
+ ],
140
+ True: [
141
+ "Specifications complete! SHIFT+TAB to create a Plan",
142
+ "Specs ready! Time to plan (SHIFT+TAB to Plan mode)",
143
+ "Requirements defined! Move to planning (SHIFT+TAB to Plan)",
144
+ "Specifications done! Create your roadmap (SHIFT+TAB for Plan mode)",
145
+ " 🚀 Specs complete! Advance to Plan mode (SHIFT+TAB)",
146
+ ],
147
+ },
148
+ # Tasks mode
149
+ AgentType.TASKS: {
150
+ False: [
151
+ "Break down your project into actionable tasks (SHIFT+TAB for modes)",
152
+ "Task creation time! Define your implementation steps (SHIFT+TAB to switch)",
153
+ "Ready to get tactical? Create your task list (SHIFT+TAB for mode options)",
154
+ "Task command center: Organize your work items (SHIFT+TAB to navigate)",
155
+ " ✅ Task mode activated! Break it down into bite-sized pieces (SHIFT+TAB)",
156
+ ],
157
+ True: [
158
+ "Tasks defined! Ready to export or cycle back (SHIFT+TAB)",
159
+ "Task list complete! Export your work (SHIFT+TAB to Export)",
160
+ "All tasks created! Time to export (SHIFT+TAB for Export mode)",
161
+ "Implementation plan ready! Export everything (SHIFT+TAB to Export)",
162
+ " 🎊 Tasks complete! Export your masterpiece (SHIFT+TAB)",
163
+ ],
164
+ },
165
+ # Export mode
166
+ AgentType.EXPORT: {
167
+ False: [
168
+ "Export your complete project documentation (SHIFT+TAB for modes)",
169
+ "Ready to package everything? Export time! (SHIFT+TAB to switch)",
170
+ "Export station: Generate deliverables (SHIFT+TAB for mode menu)",
171
+ "Time to share your work! Export documents (SHIFT+TAB to navigate)",
172
+ " 📦 Export mode! Package and share your creation (SHIFT+TAB)",
173
+ ],
174
+ True: [
175
+ "Exported! Start new research or continue refining (SHIFT+TAB)",
176
+ "Export complete! New cycle begins (SHIFT+TAB to Research)",
177
+ "All exported! Ready for another round (SHIFT+TAB for Research)",
178
+ "Documents exported! Start fresh (SHIFT+TAB to Research mode)",
179
+ " 🎉 Export complete! Begin a new adventure (SHIFT+TAB)",
180
+ ],
181
+ },
182
+ # Plan mode
183
+ AgentType.PLAN: {
184
+ False: [
185
+ "Create a strategic plan for your project (SHIFT+TAB for modes)",
186
+ "Planning phase: Map out your roadmap (SHIFT+TAB to switch)",
187
+ "Time to strategize! Create your project plan (SHIFT+TAB for options)",
188
+ "Plan your approach and milestones (SHIFT+TAB to navigate)",
189
+ " 🗺️ Plan mode! Chart your course to success (SHIFT+TAB)",
190
+ ],
191
+ True: [
192
+ "Plan complete! Move to Tasks mode (SHIFT+TAB)",
193
+ "Strategy ready! Time for tasks (SHIFT+TAB to Tasks mode)",
194
+ "Roadmap done! Create task list (SHIFT+TAB for Tasks)",
195
+ "Planning complete! Break into tasks (SHIFT+TAB to Tasks)",
196
+ " ⚡ Plan ready! Advance to Tasks mode (SHIFT+TAB)",
197
+ ],
198
+ },
199
+ }
200
+
201
+ def __init__(self, base_path: Path | None = None):
202
+ """Initialize placeholder hints with progress checker.
203
+
204
+ Args:
205
+ base_path: Base path for checking progress. Defaults to current directory.
206
+ """
207
+ self.progress_checker = ModeProgressChecker(base_path)
208
+ self._cached_hints: dict[tuple[AgentType, bool], str] = {}
209
+ self._hint_indices: dict[tuple[AgentType, bool], int] = {}
210
+
211
+ def get_hint(self, current_mode: AgentType, force_refresh: bool = False) -> str:
212
+ """Get a dynamic hint based on current mode and progress.
213
+
214
+ Args:
215
+ current_mode: The current agent mode.
216
+ force_refresh: Force recalculation of progress state.
217
+
218
+ Returns:
219
+ A contextual hint string for the placeholder.
220
+ """
221
+ # Default hint if mode not configured
222
+ if current_mode not in self.HINTS:
223
+ return f"Enter your {current_mode.value} mode prompt (SHIFT+TAB to switch modes)"
224
+
225
+ # Determine if mode has content
226
+ has_content = self.progress_checker.has_mode_content(current_mode)
227
+
228
+ # Get hint variations for this mode and state
229
+ hints_list = self.HINTS[current_mode][has_content]
230
+
231
+ # Cache key for this mode and state
232
+ cache_key = (current_mode, has_content)
233
+
234
+ # Force refresh or first time
235
+ if force_refresh or cache_key not in self._cached_hints:
236
+ # Initialize index for this cache key if not exists
237
+ if cache_key not in self._hint_indices:
238
+ self._hint_indices[cache_key] = random.randint(0, len(hints_list) - 1) # noqa: S311
239
+
240
+ # Get hint at current index
241
+ hint_index = self._hint_indices[cache_key]
242
+ self._cached_hints[cache_key] = hints_list[hint_index]
243
+
244
+ return self._cached_hints[cache_key]
245
+
246
+ def get_placeholder_for_mode(self, current_mode: AgentType) -> str:
247
+ """Get placeholder text for a given mode.
248
+
249
+ This is an alias for get_hint() to maintain compatibility.
250
+
251
+ Args:
252
+ current_mode: The current agent mode.
253
+
254
+ Returns:
255
+ A contextual hint string for the placeholder.
256
+ """
257
+ return self.get_hint(current_mode)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shotgun-sh
3
- Version: 0.1.0.dev20
3
+ Version: 0.1.0.dev23
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
@@ -16,12 +16,11 @@ Classifier: Intended Audience :: Developers
16
16
  Classifier: License :: OSI Approved :: MIT License
17
17
  Classifier: Operating System :: OS Independent
18
18
  Classifier: Programming Language :: Python :: 3
19
- Classifier: Programming Language :: Python :: 3.10
20
19
  Classifier: Programming Language :: Python :: 3.11
21
20
  Classifier: Programming Language :: Python :: 3.12
22
21
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
22
  Classifier: Topic :: Utilities
24
- Requires-Python: >=3.10
23
+ Requires-Python: >=3.11
25
24
  Requires-Dist: anthropic>=0.39.0
26
25
  Requires-Dist: google-generativeai>=0.8.5
27
26
  Requires-Dist: httpx>=0.27.0
@@ -177,7 +176,7 @@ The update command automatically detects and uses the appropriate method:
177
176
 
178
177
  ### Requirements
179
178
 
180
- - **Python 3.10+** (3.13 recommended)
179
+ - **Python 3.11+** (3.13 recommended)
181
180
  - **uv** - Fast Python package installer and resolver
182
181
  - **actionlint** (optional) - For GitHub Actions workflow validation
183
182
 
@@ -289,17 +288,17 @@ go install github.com/rhysd/actionlint/cmd/actionlint@latest
289
288
 
290
289
  ### Python Version Management
291
290
 
292
- The project supports **Python 3.10+**. The `.python-version` file specifies Python 3.10 to ensure development against the minimum supported version.
291
+ The project supports **Python 3.11+**. The `.python-version` file specifies Python 3.11 to ensure development against the minimum supported version.
293
292
 
294
293
  If using **pyenv**:
295
294
  ```bash
296
- pyenv install 3.10.16 # or latest 3.10.x
295
+ pyenv install 3.11
297
296
  ```
298
297
 
299
298
  If using **uv** (recommended):
300
299
  ```bash
301
- uv python install 3.10
302
- uv sync --python 3.10
300
+ uv python install 3.11
301
+ uv sync --python 3.11
303
302
  ```
304
303
 
305
304
  ### Commit Message Convention
@@ -350,7 +349,7 @@ uv run cz commit
350
349
 
351
350
  GitHub Actions automatically:
352
351
  - Runs on pull requests and pushes to main
353
- - Tests with Python 3.10
352
+ - Tests with Python 3.11
354
353
  - Validates code with ruff, ruff-format, and mypy
355
354
  - Ensures all checks pass before merge
356
355