shotgun-sh 0.3.3.dev1__py3-none-any.whl → 0.4.0.dev1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- shotgun/agents/agent_manager.py +191 -23
- shotgun/agents/common.py +78 -77
- shotgun/agents/config/manager.py +42 -1
- shotgun/agents/config/models.py +16 -0
- shotgun/agents/conversation/history/file_content_deduplication.py +66 -43
- shotgun/agents/export.py +12 -13
- shotgun/agents/models.py +66 -1
- shotgun/agents/plan.py +12 -13
- shotgun/agents/research.py +13 -10
- shotgun/agents/router/__init__.py +47 -0
- shotgun/agents/router/models.py +376 -0
- shotgun/agents/router/router.py +185 -0
- shotgun/agents/router/tools/__init__.py +18 -0
- shotgun/agents/router/tools/delegation_tools.py +503 -0
- shotgun/agents/router/tools/plan_tools.py +322 -0
- shotgun/agents/specify.py +12 -13
- shotgun/agents/tasks.py +12 -13
- shotgun/agents/tools/file_management.py +49 -1
- shotgun/agents/tools/registry.py +2 -0
- shotgun/agents/tools/web_search/__init__.py +1 -2
- shotgun/agents/tools/web_search/gemini.py +1 -3
- shotgun/codebase/core/change_detector.py +1 -1
- shotgun/codebase/core/ingestor.py +1 -1
- shotgun/codebase/core/manager.py +1 -1
- shotgun/prompts/agents/export.j2 +2 -0
- shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +5 -10
- shotgun/prompts/agents/partials/router_delegation_mode.j2 +36 -0
- shotgun/prompts/agents/plan.j2 +24 -12
- shotgun/prompts/agents/research.j2 +70 -31
- shotgun/prompts/agents/router.j2 +440 -0
- shotgun/prompts/agents/specify.j2 +39 -16
- shotgun/prompts/agents/state/system_state.j2 +15 -6
- shotgun/prompts/agents/tasks.j2 +58 -34
- shotgun/tui/app.py +5 -6
- shotgun/tui/components/mode_indicator.py +120 -25
- shotgun/tui/components/status_bar.py +2 -2
- shotgun/tui/dependencies.py +64 -9
- shotgun/tui/protocols.py +37 -0
- shotgun/tui/screens/chat/chat.tcss +9 -1
- shotgun/tui/screens/chat/chat_screen.py +643 -11
- shotgun/tui/screens/chat_screen/command_providers.py +0 -87
- shotgun/tui/screens/chat_screen/history/agent_response.py +7 -3
- shotgun/tui/screens/chat_screen/history/chat_history.py +12 -0
- shotgun/tui/screens/chat_screen/history/formatters.py +53 -15
- shotgun/tui/screens/chat_screen/history/partial_response.py +11 -1
- shotgun/tui/screens/chat_screen/messages.py +219 -0
- shotgun/tui/screens/onboarding.py +30 -26
- shotgun/tui/utils/mode_progress.py +20 -86
- shotgun/tui/widgets/__init__.py +2 -1
- shotgun/tui/widgets/approval_widget.py +152 -0
- shotgun/tui/widgets/cascade_confirmation_widget.py +203 -0
- shotgun/tui/widgets/plan_panel.py +129 -0
- shotgun/tui/widgets/step_checkpoint_widget.py +180 -0
- {shotgun_sh-0.3.3.dev1.dist-info → shotgun_sh-0.4.0.dev1.dist-info}/METADATA +3 -3
- {shotgun_sh-0.3.3.dev1.dist-info → shotgun_sh-0.4.0.dev1.dist-info}/RECORD +58 -45
- {shotgun_sh-0.3.3.dev1.dist-info → shotgun_sh-0.4.0.dev1.dist-info}/WHEEL +0 -0
- {shotgun_sh-0.3.3.dev1.dist-info → shotgun_sh-0.4.0.dev1.dist-info}/entry_points.txt +0 -0
- {shotgun_sh-0.3.3.dev1.dist-info → shotgun_sh-0.4.0.dev1.dist-info}/licenses/LICENSE +0 -0
|
@@ -3,7 +3,6 @@ from typing import TYPE_CHECKING, cast
|
|
|
3
3
|
|
|
4
4
|
from textual.command import DiscoveryHit, Hit, Provider
|
|
5
5
|
|
|
6
|
-
from shotgun.agents.models import AgentType
|
|
7
6
|
from shotgun.codebase.models import CodebaseGraph
|
|
8
7
|
from shotgun.tui.screens.chat_screen.hint_message import HintMessage
|
|
9
8
|
from shotgun.tui.screens.model_picker import ModelPickerScreen
|
|
@@ -13,92 +12,6 @@ if TYPE_CHECKING:
|
|
|
13
12
|
from shotgun.tui.screens.chat import ChatScreen
|
|
14
13
|
|
|
15
14
|
|
|
16
|
-
class AgentModeProvider(Provider):
|
|
17
|
-
"""Command provider for agent mode switching."""
|
|
18
|
-
|
|
19
|
-
@property
|
|
20
|
-
def chat_screen(self) -> "ChatScreen":
|
|
21
|
-
from shotgun.tui.screens.chat import ChatScreen
|
|
22
|
-
|
|
23
|
-
return cast(ChatScreen, self.screen)
|
|
24
|
-
|
|
25
|
-
def set_mode(self, mode: AgentType) -> None:
|
|
26
|
-
"""Switch to research mode."""
|
|
27
|
-
self.chat_screen.mode = mode
|
|
28
|
-
|
|
29
|
-
async def discover(self) -> AsyncGenerator[DiscoveryHit, None]:
|
|
30
|
-
"""Provide default mode switching commands when palette opens."""
|
|
31
|
-
yield DiscoveryHit(
|
|
32
|
-
"Switch to Research Mode",
|
|
33
|
-
lambda: self.set_mode(AgentType.RESEARCH),
|
|
34
|
-
help="🔬 Research topics with web search and synthesize findings",
|
|
35
|
-
)
|
|
36
|
-
yield DiscoveryHit(
|
|
37
|
-
"Switch to Specify Mode",
|
|
38
|
-
lambda: self.set_mode(AgentType.SPECIFY),
|
|
39
|
-
help="📝 Create detailed specifications and requirements documents",
|
|
40
|
-
)
|
|
41
|
-
yield DiscoveryHit(
|
|
42
|
-
"Switch to Plan Mode",
|
|
43
|
-
lambda: self.set_mode(AgentType.PLAN),
|
|
44
|
-
help="📋 Create comprehensive, actionable plans with milestones",
|
|
45
|
-
)
|
|
46
|
-
yield DiscoveryHit(
|
|
47
|
-
"Switch to Tasks Mode",
|
|
48
|
-
lambda: self.set_mode(AgentType.TASKS),
|
|
49
|
-
help="✅ Generate specific, actionable tasks from research and plans",
|
|
50
|
-
)
|
|
51
|
-
yield DiscoveryHit(
|
|
52
|
-
"Switch to Export Mode",
|
|
53
|
-
lambda: self.set_mode(AgentType.EXPORT),
|
|
54
|
-
help="📤 Export artifacts and findings to various formats",
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
-
async def search(self, query: str) -> AsyncGenerator[Hit, None]:
|
|
58
|
-
"""Search for mode commands."""
|
|
59
|
-
matcher = self.matcher(query)
|
|
60
|
-
|
|
61
|
-
commands = [
|
|
62
|
-
(
|
|
63
|
-
"Switch to Research Mode",
|
|
64
|
-
"🔬 Research topics with web search and synthesize findings",
|
|
65
|
-
lambda: self.set_mode(AgentType.RESEARCH),
|
|
66
|
-
AgentType.RESEARCH,
|
|
67
|
-
),
|
|
68
|
-
(
|
|
69
|
-
"Switch to Specify Mode",
|
|
70
|
-
"📝 Create detailed specifications and requirements documents",
|
|
71
|
-
lambda: self.set_mode(AgentType.SPECIFY),
|
|
72
|
-
AgentType.SPECIFY,
|
|
73
|
-
),
|
|
74
|
-
(
|
|
75
|
-
"Switch to Plan Mode",
|
|
76
|
-
"📋 Create comprehensive, actionable plans with milestones",
|
|
77
|
-
lambda: self.set_mode(AgentType.PLAN),
|
|
78
|
-
AgentType.PLAN,
|
|
79
|
-
),
|
|
80
|
-
(
|
|
81
|
-
"Switch to Tasks Mode",
|
|
82
|
-
"✅ Generate specific, actionable tasks from research and plans",
|
|
83
|
-
lambda: self.set_mode(AgentType.TASKS),
|
|
84
|
-
AgentType.TASKS,
|
|
85
|
-
),
|
|
86
|
-
(
|
|
87
|
-
"Switch to Export Mode",
|
|
88
|
-
"📤 Export artifacts and findings to various formats",
|
|
89
|
-
lambda: self.set_mode(AgentType.EXPORT),
|
|
90
|
-
AgentType.EXPORT,
|
|
91
|
-
),
|
|
92
|
-
]
|
|
93
|
-
|
|
94
|
-
for title, help_text, callback, mode in commands:
|
|
95
|
-
if self.chat_screen.mode == mode:
|
|
96
|
-
continue
|
|
97
|
-
score = matcher.match(title)
|
|
98
|
-
if score > 0:
|
|
99
|
-
yield Hit(score, matcher.highlight(title), callback, help=help_text)
|
|
100
|
-
|
|
101
|
-
|
|
102
15
|
class UsageProvider(Provider):
|
|
103
16
|
"""Command provider for agent mode switching."""
|
|
104
17
|
|
|
@@ -18,9 +18,10 @@ from .formatters import ToolFormatter
|
|
|
18
18
|
class AgentResponseWidget(Widget):
|
|
19
19
|
"""Widget that displays agent responses in the chat history."""
|
|
20
20
|
|
|
21
|
-
def __init__(self, item: ModelResponse | None) -> None:
|
|
21
|
+
def __init__(self, item: ModelResponse | None, is_sub_agent: bool = False) -> None:
|
|
22
22
|
super().__init__()
|
|
23
23
|
self.item = item
|
|
24
|
+
self.is_sub_agent = is_sub_agent
|
|
24
25
|
|
|
25
26
|
def compose(self) -> ComposeResult:
|
|
26
27
|
self.display = self.item is not None
|
|
@@ -35,11 +36,14 @@ class AgentResponseWidget(Widget):
|
|
|
35
36
|
if self.item is None:
|
|
36
37
|
return ""
|
|
37
38
|
|
|
39
|
+
# Use different prefix for sub-agent responses
|
|
40
|
+
prefix = "**⏺** " if not self.is_sub_agent else " **↳** "
|
|
41
|
+
|
|
38
42
|
for idx, part in enumerate(self.item.parts):
|
|
39
43
|
if isinstance(part, TextPart):
|
|
40
|
-
# Only show the
|
|
44
|
+
# Only show the prefix if there's actual content
|
|
41
45
|
if part.content and part.content.strip():
|
|
42
|
-
acc += f"
|
|
46
|
+
acc += f"{prefix}{part.content}\n\n"
|
|
43
47
|
elif isinstance(part, ToolCallPart):
|
|
44
48
|
parts_str = ToolFormatter.format_tool_call_part(part)
|
|
45
49
|
if parts_str: # Only add if there's actual content
|
|
@@ -8,10 +8,12 @@ from pydantic_ai.messages import (
|
|
|
8
8
|
ModelResponse,
|
|
9
9
|
UserPromptPart,
|
|
10
10
|
)
|
|
11
|
+
from textual import events
|
|
11
12
|
from textual.app import ComposeResult
|
|
12
13
|
from textual.reactive import reactive
|
|
13
14
|
from textual.widget import Widget
|
|
14
15
|
|
|
16
|
+
from shotgun.tui.components.prompt_input import PromptInput
|
|
15
17
|
from shotgun.tui.components.vertical_tail import VerticalTail
|
|
16
18
|
from shotgun.tui.screens.chat_screen.hint_message import HintMessage, HintMessageWidget
|
|
17
19
|
|
|
@@ -113,3 +115,13 @@ class ChatHistory(Widget):
|
|
|
113
115
|
|
|
114
116
|
# Scroll to bottom to show newly added messages
|
|
115
117
|
self.vertical_tail.scroll_end(animate=False)
|
|
118
|
+
|
|
119
|
+
def on_click(self, event: events.Click) -> None:
|
|
120
|
+
"""Focus the prompt input when clicking on the history area."""
|
|
121
|
+
# Only handle clicks that weren't already handled by a child widget
|
|
122
|
+
if event.button == 1: # Left click
|
|
123
|
+
results = self.screen.query(PromptInput)
|
|
124
|
+
if results:
|
|
125
|
+
prompt_input = results.first()
|
|
126
|
+
if prompt_input.display:
|
|
127
|
+
prompt_input.focus()
|
|
@@ -29,6 +29,53 @@ class ToolFormatter:
|
|
|
29
29
|
return {}
|
|
30
30
|
return args if isinstance(args, dict) else {}
|
|
31
31
|
|
|
32
|
+
@classmethod
|
|
33
|
+
def _extract_key_arg(
|
|
34
|
+
cls,
|
|
35
|
+
args: dict[str, object],
|
|
36
|
+
key_arg: str,
|
|
37
|
+
tool_name: str | None = None,
|
|
38
|
+
) -> str | None:
|
|
39
|
+
"""Extract key argument value, handling nested args and special cases.
|
|
40
|
+
|
|
41
|
+
Supports:
|
|
42
|
+
- Direct key access: key_arg="query" -> args["query"]
|
|
43
|
+
- Nested access: key_arg="task" -> args["input"]["task"] (for Pydantic model inputs)
|
|
44
|
+
- Special handling for codebase_shell
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
args: Parsed tool arguments dict
|
|
48
|
+
key_arg: The key argument to extract
|
|
49
|
+
tool_name: Optional tool name for special handling
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
The extracted value as a string, or None if not found
|
|
53
|
+
"""
|
|
54
|
+
if not args or not isinstance(args, dict):
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
# Special handling for codebase_shell which needs command + args
|
|
58
|
+
if tool_name == "codebase_shell" and "command" in args:
|
|
59
|
+
command = args.get("command", "")
|
|
60
|
+
cmd_args = args.get("args", [])
|
|
61
|
+
if isinstance(cmd_args, list):
|
|
62
|
+
args_str = " ".join(str(arg) for arg in cmd_args)
|
|
63
|
+
else:
|
|
64
|
+
args_str = ""
|
|
65
|
+
return f"{command} {args_str}".strip()
|
|
66
|
+
|
|
67
|
+
# Direct key access
|
|
68
|
+
if key_arg in args:
|
|
69
|
+
return str(args[key_arg])
|
|
70
|
+
|
|
71
|
+
# Try nested access through "input" (for Pydantic model inputs)
|
|
72
|
+
if "input" in args and isinstance(args["input"], dict):
|
|
73
|
+
input_dict = args["input"]
|
|
74
|
+
if key_arg in input_dict:
|
|
75
|
+
return str(input_dict[key_arg])
|
|
76
|
+
|
|
77
|
+
return None
|
|
78
|
+
|
|
32
79
|
@classmethod
|
|
33
80
|
def format_tool_call_part(cls, part: ToolCallPart) -> str:
|
|
34
81
|
"""Format a tool call part using the tool display registry."""
|
|
@@ -44,19 +91,10 @@ class ToolFormatter:
|
|
|
44
91
|
args = cls.parse_args(part.args)
|
|
45
92
|
|
|
46
93
|
# Get the key argument value
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
cmd_args = args.get("args", [])
|
|
52
|
-
if isinstance(cmd_args, list):
|
|
53
|
-
args_str = " ".join(str(arg) for arg in cmd_args)
|
|
54
|
-
else:
|
|
55
|
-
args_str = ""
|
|
56
|
-
key_value = f"{command} {args_str}".strip()
|
|
57
|
-
else:
|
|
58
|
-
key_value = str(args[display_config.key_arg])
|
|
59
|
-
|
|
94
|
+
key_value = cls._extract_key_arg(
|
|
95
|
+
args, display_config.key_arg, part.tool_name
|
|
96
|
+
)
|
|
97
|
+
if key_value:
|
|
60
98
|
# Format: "display_text: key_value"
|
|
61
99
|
return f"{display_config.display_text}: {cls.truncate(key_value)}"
|
|
62
100
|
else:
|
|
@@ -95,8 +133,8 @@ class ToolFormatter:
|
|
|
95
133
|
|
|
96
134
|
args = cls.parse_args(part.args)
|
|
97
135
|
# Get the key argument value
|
|
98
|
-
|
|
99
|
-
|
|
136
|
+
key_value = cls._extract_key_arg(args, display_config.key_arg)
|
|
137
|
+
if key_value:
|
|
100
138
|
# Format: "display_text: key_value"
|
|
101
139
|
return f"{display_config.display_text}: {cls.truncate(key_value)}"
|
|
102
140
|
else:
|
|
@@ -5,6 +5,8 @@ from textual.app import ComposeResult
|
|
|
5
5
|
from textual.reactive import reactive
|
|
6
6
|
from textual.widget import Widget
|
|
7
7
|
|
|
8
|
+
from shotgun.tui.protocols import ActiveSubAgentProvider
|
|
9
|
+
|
|
8
10
|
from .agent_response import AgentResponseWidget
|
|
9
11
|
from .user_question import UserQuestionWidget
|
|
10
12
|
|
|
@@ -27,11 +29,19 @@ class PartialResponseWidget(Widget): # TODO: doesn't work lol
|
|
|
27
29
|
super().__init__()
|
|
28
30
|
self.item = item
|
|
29
31
|
|
|
32
|
+
def _is_sub_agent_active(self) -> bool:
|
|
33
|
+
"""Check if a sub-agent is currently active."""
|
|
34
|
+
if isinstance(self.screen, ActiveSubAgentProvider):
|
|
35
|
+
return self.screen.active_sub_agent is not None
|
|
36
|
+
return False
|
|
37
|
+
|
|
30
38
|
def compose(self) -> ComposeResult:
|
|
31
39
|
if self.item is None:
|
|
32
40
|
pass
|
|
33
41
|
elif self.item.kind == "response":
|
|
34
|
-
yield AgentResponseWidget(
|
|
42
|
+
yield AgentResponseWidget(
|
|
43
|
+
self.item, is_sub_agent=self._is_sub_agent_active()
|
|
44
|
+
)
|
|
35
45
|
elif self.item.kind == "request":
|
|
36
46
|
yield UserQuestionWidget(self.item)
|
|
37
47
|
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""Message types for ChatScreen communication.
|
|
2
|
+
|
|
3
|
+
This module defines Textual message types used for communication
|
|
4
|
+
between widgets and the ChatScreen, particularly for step checkpoints
|
|
5
|
+
and cascade confirmation in the Router's Planning mode.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from textual.message import Message
|
|
9
|
+
|
|
10
|
+
from shotgun.agents.models import AgentType
|
|
11
|
+
from shotgun.agents.router.models import CascadeScope, ExecutionPlan, ExecutionStep
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
# Step checkpoint messages (Stage 4)
|
|
15
|
+
"StepCompleted",
|
|
16
|
+
"CheckpointContinue",
|
|
17
|
+
"CheckpointModify",
|
|
18
|
+
"CheckpointStop",
|
|
19
|
+
# Cascade confirmation messages (Stage 5)
|
|
20
|
+
"CascadeConfirmationRequired",
|
|
21
|
+
"CascadeConfirmed",
|
|
22
|
+
"CascadeDeclined",
|
|
23
|
+
# Plan approval messages (Stage 7)
|
|
24
|
+
"PlanApprovalRequired",
|
|
25
|
+
"PlanApproved",
|
|
26
|
+
"PlanRejected",
|
|
27
|
+
# Sub-agent lifecycle messages (Stage 8)
|
|
28
|
+
"SubAgentStarted",
|
|
29
|
+
"SubAgentCompleted",
|
|
30
|
+
# Plan panel messages (Stage 11)
|
|
31
|
+
"PlanUpdated",
|
|
32
|
+
"PlanPanelClosed",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class StepCompleted(Message):
|
|
37
|
+
"""Posted when a plan step completes in Planning mode.
|
|
38
|
+
|
|
39
|
+
This message triggers the checkpoint UI to appear, allowing the user
|
|
40
|
+
to choose whether to continue, modify the plan, or stop execution.
|
|
41
|
+
|
|
42
|
+
Attributes:
|
|
43
|
+
step: The step that was just completed.
|
|
44
|
+
next_step: The next step to execute, or None if this was the last step.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(self, step: ExecutionStep, next_step: ExecutionStep | None) -> None:
|
|
48
|
+
super().__init__()
|
|
49
|
+
self.step = step
|
|
50
|
+
self.next_step = next_step
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class CheckpointContinue(Message):
|
|
54
|
+
"""Posted when user chooses to continue to next step.
|
|
55
|
+
|
|
56
|
+
This message indicates the user wants to proceed with the next
|
|
57
|
+
step in the execution plan.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class CheckpointModify(Message):
|
|
62
|
+
"""Posted when user wants to modify the plan.
|
|
63
|
+
|
|
64
|
+
This message indicates the user wants to return to the prompt input
|
|
65
|
+
to make adjustments to the plan before continuing.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class CheckpointStop(Message):
|
|
70
|
+
"""Posted when user wants to stop execution.
|
|
71
|
+
|
|
72
|
+
This message indicates the user wants to halt execution while
|
|
73
|
+
keeping the remaining steps in the plan as pending.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# =============================================================================
|
|
78
|
+
# Cascade Confirmation Messages (Stage 5)
|
|
79
|
+
# =============================================================================
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class CascadeConfirmationRequired(Message):
|
|
83
|
+
"""Posted when a file with dependents was modified and needs cascade confirmation.
|
|
84
|
+
|
|
85
|
+
In Planning mode, after modifying a file like specification.md that has
|
|
86
|
+
dependent files (plan.md, tasks.md), this message triggers the cascade
|
|
87
|
+
confirmation UI to appear.
|
|
88
|
+
|
|
89
|
+
Attributes:
|
|
90
|
+
updated_file: The file that was just updated (e.g., "specification.md").
|
|
91
|
+
dependent_files: List of files that depend on the updated file.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
def __init__(self, updated_file: str, dependent_files: list[str]) -> None:
|
|
95
|
+
super().__init__()
|
|
96
|
+
self.updated_file = updated_file
|
|
97
|
+
self.dependent_files = dependent_files
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class CascadeConfirmed(Message):
|
|
101
|
+
"""Posted when user confirms cascade update.
|
|
102
|
+
|
|
103
|
+
This message indicates the user wants to proceed with updating
|
|
104
|
+
dependent files based on the selected scope.
|
|
105
|
+
|
|
106
|
+
Attributes:
|
|
107
|
+
scope: The scope of files to update (ALL, PLAN_ONLY, TASKS_ONLY, NONE).
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
def __init__(self, scope: CascadeScope) -> None:
|
|
111
|
+
super().__init__()
|
|
112
|
+
self.scope = scope
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class CascadeDeclined(Message):
|
|
116
|
+
"""Posted when user declines cascade update.
|
|
117
|
+
|
|
118
|
+
This message indicates the user does not want to update dependent
|
|
119
|
+
files and will handle them manually.
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# =============================================================================
|
|
124
|
+
# Plan Approval Messages (Stage 7)
|
|
125
|
+
# =============================================================================
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class PlanApprovalRequired(Message):
|
|
129
|
+
"""Posted when a multi-step plan is created and needs user approval.
|
|
130
|
+
|
|
131
|
+
In Planning mode, after the router creates a plan with multiple steps,
|
|
132
|
+
this message triggers the approval UI to appear.
|
|
133
|
+
|
|
134
|
+
Attributes:
|
|
135
|
+
plan: The execution plan that needs user approval.
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
def __init__(self, plan: ExecutionPlan) -> None:
|
|
139
|
+
super().__init__()
|
|
140
|
+
self.plan = plan
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class PlanApproved(Message):
|
|
144
|
+
"""Posted when user approves the plan.
|
|
145
|
+
|
|
146
|
+
This message indicates the user wants to proceed with executing
|
|
147
|
+
the plan ("Go Ahead").
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class PlanRejected(Message):
|
|
152
|
+
"""Posted when user rejects the plan to clarify/modify.
|
|
153
|
+
|
|
154
|
+
This message indicates the user wants to return to the prompt input
|
|
155
|
+
to modify or clarify the request ("No, Let Me Clarify").
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# =============================================================================
|
|
160
|
+
# Sub-Agent Lifecycle Messages (Stage 8)
|
|
161
|
+
# =============================================================================
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class SubAgentStarted(Message):
|
|
165
|
+
"""Posted when router starts delegating to a sub-agent.
|
|
166
|
+
|
|
167
|
+
This message triggers the mode indicator to show the active sub-agent
|
|
168
|
+
in the format "📋 Planning → Research".
|
|
169
|
+
|
|
170
|
+
Attributes:
|
|
171
|
+
agent_type: The type of sub-agent that started executing.
|
|
172
|
+
"""
|
|
173
|
+
|
|
174
|
+
def __init__(self, agent_type: AgentType) -> None:
|
|
175
|
+
super().__init__()
|
|
176
|
+
self.agent_type = agent_type
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class SubAgentCompleted(Message):
|
|
180
|
+
"""Posted when sub-agent delegation completes.
|
|
181
|
+
|
|
182
|
+
This message triggers the mode indicator to clear the sub-agent
|
|
183
|
+
display and return to showing just the mode.
|
|
184
|
+
|
|
185
|
+
Attributes:
|
|
186
|
+
agent_type: The type of sub-agent that completed.
|
|
187
|
+
"""
|
|
188
|
+
|
|
189
|
+
def __init__(self, agent_type: AgentType) -> None:
|
|
190
|
+
super().__init__()
|
|
191
|
+
self.agent_type = agent_type
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# =============================================================================
|
|
195
|
+
# Plan Panel Messages (Stage 11)
|
|
196
|
+
# =============================================================================
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class PlanUpdated(Message):
|
|
200
|
+
"""Posted when the current plan changes.
|
|
201
|
+
|
|
202
|
+
This message triggers the plan panel to auto-show/hide based on
|
|
203
|
+
whether a plan exists.
|
|
204
|
+
|
|
205
|
+
Attributes:
|
|
206
|
+
plan: The updated execution plan, or None if plan was cleared.
|
|
207
|
+
"""
|
|
208
|
+
|
|
209
|
+
def __init__(self, plan: ExecutionPlan | None) -> None:
|
|
210
|
+
super().__init__()
|
|
211
|
+
self.plan = plan
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class PlanPanelClosed(Message):
|
|
215
|
+
"""Posted when user closes the plan panel with × button.
|
|
216
|
+
|
|
217
|
+
This message indicates the user wants to dismiss the plan panel
|
|
218
|
+
temporarily. The panel will reopen when the plan changes.
|
|
219
|
+
"""
|
|
@@ -427,40 +427,45 @@ Here are some helpful resources to get you up to speed with Shotgun:
|
|
|
427
427
|
"""
|
|
428
428
|
|
|
429
429
|
def _page_2_modes(self) -> str:
|
|
430
|
-
"""Page 2: Explanation of the
|
|
430
|
+
"""Page 2: Explanation of the Router and its modes."""
|
|
431
431
|
return """
|
|
432
|
-
## Understanding Shotgun's
|
|
432
|
+
## Understanding Shotgun's Router
|
|
433
433
|
|
|
434
|
-
Shotgun
|
|
434
|
+
Shotgun uses an intelligent **Router** that orchestrates your workflow automatically. Just describe what you need, and the Router will coordinate research, specifications, planning, and tasks for you.
|
|
435
435
|
|
|
436
|
-
###
|
|
437
|
-
Research topics with web search and synthesize findings. Perfect for gathering information and exploring new concepts.
|
|
436
|
+
### Two Operating Modes
|
|
438
437
|
|
|
439
|
-
|
|
438
|
+
The Router operates in two modes, which you can toggle with `Shift+Tab`:
|
|
440
439
|
|
|
441
|
-
###
|
|
442
|
-
|
|
440
|
+
### 📋 Planning Mode (Default)
|
|
441
|
+
- **Incremental execution**: Does one step at a time
|
|
442
|
+
- **Asks clarifying questions** before complex tasks
|
|
443
|
+
- **Shows plan for approval** before executing multi-step work
|
|
444
|
+
- **Confirms before cascading** changes to dependent files
|
|
443
445
|
|
|
444
|
-
|
|
446
|
+
Best for: Complex tasks, learning the workflow, staying in control
|
|
445
447
|
|
|
446
|
-
###
|
|
447
|
-
|
|
448
|
+
### ✍️ Drafting Mode
|
|
449
|
+
- **Auto-executes** plans without stopping for approval
|
|
450
|
+
- **Makes reasonable assumptions** instead of asking questions
|
|
451
|
+
- **Updates all dependent files** automatically
|
|
448
452
|
|
|
449
|
-
|
|
453
|
+
Best for: Routine tasks, experienced users, speed-focused work
|
|
450
454
|
|
|
451
|
-
|
|
452
|
-
Generate specific, actionable tasks from research and plans. Best for getting concrete next steps and action items.
|
|
453
|
-
|
|
454
|
-
**Writes to:** `.shotgun/tasks.md`
|
|
455
|
+
---
|
|
455
456
|
|
|
456
|
-
###
|
|
457
|
-
Export artifacts and findings to various formats. Creates documentation like Claude.md (AI instructions), Agent.md (agent specs), PRDs, and other deliverables. Can write to any file in `.shotgun/` except the mode-specific files above.
|
|
457
|
+
### Files Created
|
|
458
458
|
|
|
459
|
-
|
|
459
|
+
The Router manages these files in `.shotgun/`:
|
|
460
|
+
- `research.md` - Research findings
|
|
461
|
+
- `specification.md` - Detailed specifications
|
|
462
|
+
- `plan.md` - Implementation plans
|
|
463
|
+
- `tasks.md` - Actionable task lists
|
|
464
|
+
- `exports/` - Documentation exports
|
|
460
465
|
|
|
461
466
|
---
|
|
462
467
|
|
|
463
|
-
**Tip:**
|
|
468
|
+
**Tip:** Press `Shift+Tab` to toggle between Planning and Drafting modes!
|
|
464
469
|
"""
|
|
465
470
|
|
|
466
471
|
def _page_3_prompts(self) -> str:
|
|
@@ -485,12 +490,11 @@ Provide relevant context about what you're trying to accomplish:
|
|
|
485
490
|
|
|
486
491
|
> "I'm working on the payment flow. I need to add support for refunds."
|
|
487
492
|
|
|
488
|
-
### 4.
|
|
489
|
-
|
|
490
|
-
-
|
|
491
|
-
-
|
|
492
|
-
-
|
|
493
|
-
- Use **Tasks** for actionable next steps
|
|
493
|
+
### 4. Let the Router Guide You
|
|
494
|
+
The Router will automatically coordinate the right workflow:
|
|
495
|
+
- Describe what you want to accomplish
|
|
496
|
+
- In Planning mode, it will ask clarifying questions first
|
|
497
|
+
- In Drafting mode, it will execute immediately
|
|
494
498
|
|
|
495
499
|
---
|
|
496
500
|
|