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.
- shotgun/agents/agent_manager.py +100 -16
- shotgun/agents/common.py +142 -28
- shotgun/agents/conversation_history.py +56 -0
- shotgun/agents/conversation_manager.py +105 -0
- shotgun/agents/export.py +5 -2
- shotgun/agents/models.py +21 -7
- shotgun/agents/plan.py +2 -1
- shotgun/agents/research.py +2 -1
- shotgun/agents/specify.py +2 -1
- shotgun/agents/tasks.py +5 -2
- shotgun/agents/tools/codebase/codebase_shell.py +2 -2
- shotgun/agents/tools/codebase/directory_lister.py +1 -1
- shotgun/agents/tools/codebase/file_read.py +1 -1
- shotgun/agents/tools/codebase/query_graph.py +1 -1
- shotgun/agents/tools/codebase/retrieve_code.py +1 -1
- shotgun/agents/tools/file_management.py +67 -2
- shotgun/main.py +9 -1
- shotgun/prompts/agents/export.j2 +14 -11
- shotgun/prompts/agents/partials/codebase_understanding.j2 +9 -0
- shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +6 -9
- shotgun/prompts/agents/plan.j2 +9 -13
- shotgun/prompts/agents/research.j2 +11 -14
- shotgun/prompts/agents/specify.j2 +9 -12
- shotgun/prompts/agents/state/codebase/codebase_graphs_available.j2 +5 -1
- shotgun/prompts/agents/state/system_state.j2 +27 -5
- shotgun/prompts/agents/tasks.j2 +12 -12
- shotgun/sdk/models.py +1 -1
- shotgun/sdk/services.py +0 -14
- shotgun/tui/app.py +9 -4
- shotgun/tui/screens/chat.py +92 -30
- shotgun/tui/screens/chat_screen/command_providers.py +1 -1
- shotgun/tui/screens/chat_screen/history.py +6 -0
- shotgun/tui/utils/__init__.py +5 -0
- shotgun/tui/utils/mode_progress.py +257 -0
- {shotgun_sh-0.1.0.dev20.dist-info → shotgun_sh-0.1.0.dev23.dist-info}/METADATA +8 -9
- {shotgun_sh-0.1.0.dev20.dist-info → shotgun_sh-0.1.0.dev23.dist-info}/RECORD +39 -56
- shotgun/agents/artifact_state.py +0 -58
- shotgun/agents/tools/artifact_management.py +0 -481
- shotgun/artifacts/__init__.py +0 -17
- shotgun/artifacts/exceptions.py +0 -89
- shotgun/artifacts/manager.py +0 -530
- shotgun/artifacts/models.py +0 -334
- shotgun/artifacts/service.py +0 -463
- shotgun/artifacts/templates/__init__.py +0 -10
- shotgun/artifacts/templates/loader.py +0 -252
- shotgun/artifacts/templates/models.py +0 -136
- shotgun/artifacts/templates/plan/delivery_and_release_plan.yaml +0 -66
- shotgun/artifacts/templates/research/market_research.yaml +0 -585
- shotgun/artifacts/templates/research/sdk_comparison.yaml +0 -257
- shotgun/artifacts/templates/specify/prd.yaml +0 -331
- shotgun/artifacts/templates/specify/product_spec.yaml +0 -301
- shotgun/artifacts/utils.py +0 -76
- shotgun/prompts/agents/partials/artifact_system.j2 +0 -32
- shotgun/prompts/agents/state/artifact_templates_available.j2 +0 -20
- shotgun/prompts/agents/state/existing_artifacts_available.j2 +0 -25
- shotgun/sdk/artifact_models.py +0 -186
- shotgun/sdk/artifacts.py +0 -448
- {shotgun_sh-0.1.0.dev20.dist-info → shotgun_sh-0.1.0.dev23.dist-info}/WHEEL +0 -0
- {shotgun_sh-0.1.0.dev20.dist-info → shotgun_sh-0.1.0.dev23.dist-info}/entry_points.txt +0 -0
- {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__(
|
|
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
|
-
|
|
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
|
|
shotgun/tui/screens/chat.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
188
|
+
yield Label("Index this codebase?", id="index-prompt-title")
|
|
178
189
|
yield Static(
|
|
179
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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,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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
295
|
+
pyenv install 3.11
|
|
297
296
|
```
|
|
298
297
|
|
|
299
298
|
If using **uv** (recommended):
|
|
300
299
|
```bash
|
|
301
|
-
uv python install 3.
|
|
302
|
-
uv sync --python 3.
|
|
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.
|
|
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
|
|