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
|
@@ -113,93 +113,24 @@ class ModeProgressChecker:
|
|
|
113
113
|
|
|
114
114
|
|
|
115
115
|
class PlaceholderHints:
|
|
116
|
-
"""Manages dynamic placeholder hints for
|
|
116
|
+
"""Manages dynamic placeholder hints for the Router agent."""
|
|
117
117
|
|
|
118
|
-
# Placeholder variations for
|
|
118
|
+
# Placeholder variations for Router mode
|
|
119
119
|
HINTS = {
|
|
120
|
-
|
|
121
|
-
AgentType.RESEARCH: {
|
|
120
|
+
AgentType.ROUTER: {
|
|
122
121
|
False: [
|
|
123
|
-
"
|
|
124
|
-
"
|
|
125
|
-
"
|
|
126
|
-
"Ready to
|
|
127
|
-
"
|
|
122
|
+
"What would you like to work on? (SHIFT+TAB to toggle Planning/Drafting)",
|
|
123
|
+
"Ask me to research, plan, or implement anything (SHIFT+TAB toggles mode)",
|
|
124
|
+
"Describe your goal and I'll help break it down (SHIFT+TAB for mode toggle)",
|
|
125
|
+
"Ready to help with research, specs, plans, or tasks (SHIFT+TAB toggles mode)",
|
|
126
|
+
"Tell me what you need - I'll coordinate the work (SHIFT+TAB for Planning/Drafting)",
|
|
128
127
|
],
|
|
129
128
|
True: [
|
|
130
|
-
"
|
|
131
|
-
"
|
|
132
|
-
"
|
|
133
|
-
"
|
|
134
|
-
"
|
|
135
|
-
],
|
|
136
|
-
},
|
|
137
|
-
# Specify mode
|
|
138
|
-
AgentType.SPECIFY: {
|
|
139
|
-
False: [
|
|
140
|
-
"Create detailed specifications and requirements (SHIFT+TAB to switch modes)",
|
|
141
|
-
"Define your project specifications here (SHIFT+TAB to navigate modes)",
|
|
142
|
-
"Time to get specific - write comprehensive specs (SHIFT+TAB for mode options)",
|
|
143
|
-
"Specification station: Document requirements and designs (SHIFT+TAB to change modes)",
|
|
144
|
-
" 📋 Spec-tacular time! Let's architect your ideas (SHIFT+TAB for mode magic)",
|
|
145
|
-
],
|
|
146
|
-
True: [
|
|
147
|
-
"Specifications complete! SHIFT+TAB to create a Plan",
|
|
148
|
-
"Specs ready! Time to plan (SHIFT+TAB to Plan mode)",
|
|
149
|
-
"Requirements defined! Move to planning (SHIFT+TAB to Plan)",
|
|
150
|
-
"Specifications done! Create your roadmap (SHIFT+TAB for Plan mode)",
|
|
151
|
-
" 🚀 Specs complete! Advance to Plan mode (SHIFT+TAB)",
|
|
152
|
-
],
|
|
153
|
-
},
|
|
154
|
-
# Tasks mode
|
|
155
|
-
AgentType.TASKS: {
|
|
156
|
-
False: [
|
|
157
|
-
"Break down your project into actionable tasks (SHIFT+TAB for modes)",
|
|
158
|
-
"Task creation time! Define your implementation steps (SHIFT+TAB to switch)",
|
|
159
|
-
"Ready to get tactical? Create your task list (SHIFT+TAB for mode options)",
|
|
160
|
-
"Task command center: Organize your work items (SHIFT+TAB to navigate)",
|
|
161
|
-
" ✅ Task mode activated! Break it down into bite-sized pieces (SHIFT+TAB)",
|
|
162
|
-
],
|
|
163
|
-
True: [
|
|
164
|
-
"Tasks defined! Ready to export or cycle back (SHIFT+TAB)",
|
|
165
|
-
"Task list complete! Export your work (SHIFT+TAB to Export)",
|
|
166
|
-
"All tasks created! Time to export (SHIFT+TAB for Export mode)",
|
|
167
|
-
"Implementation plan ready! Export everything (SHIFT+TAB to Export)",
|
|
168
|
-
" 🎊 Tasks complete! Export your masterpiece (SHIFT+TAB)",
|
|
169
|
-
],
|
|
170
|
-
},
|
|
171
|
-
# Export mode
|
|
172
|
-
AgentType.EXPORT: {
|
|
173
|
-
False: [
|
|
174
|
-
"Export your complete project documentation (SHIFT+TAB for modes)",
|
|
175
|
-
"Ready to package everything? Export time! (SHIFT+TAB to switch)",
|
|
176
|
-
"Export station: Generate deliverables (SHIFT+TAB for mode menu)",
|
|
177
|
-
"Time to share your work! Export documents (SHIFT+TAB to navigate)",
|
|
178
|
-
" 📦 Export mode! Package and share your creation (SHIFT+TAB)",
|
|
179
|
-
],
|
|
180
|
-
True: [
|
|
181
|
-
"Exported! Start new research or continue refining (SHIFT+TAB)",
|
|
182
|
-
"Export complete! New cycle begins (SHIFT+TAB to Research)",
|
|
183
|
-
"All exported! Ready for another round (SHIFT+TAB for Research)",
|
|
184
|
-
"Documents exported! Start fresh (SHIFT+TAB to Research mode)",
|
|
185
|
-
" 🎉 Export complete! Begin a new adventure (SHIFT+TAB)",
|
|
186
|
-
],
|
|
187
|
-
},
|
|
188
|
-
# Plan mode
|
|
189
|
-
AgentType.PLAN: {
|
|
190
|
-
False: [
|
|
191
|
-
"Create a strategic plan for your project (SHIFT+TAB for modes)",
|
|
192
|
-
"Planning phase: Map out your roadmap (SHIFT+TAB to switch)",
|
|
193
|
-
"Time to strategize! Create your project plan (SHIFT+TAB for options)",
|
|
194
|
-
"Plan your approach and milestones (SHIFT+TAB to navigate)",
|
|
195
|
-
" 🗺️ Plan mode! Chart your course to success (SHIFT+TAB)",
|
|
196
|
-
],
|
|
197
|
-
True: [
|
|
198
|
-
"Plan complete! Move to Tasks mode (SHIFT+TAB)",
|
|
199
|
-
"Strategy ready! Time for tasks (SHIFT+TAB to Tasks mode)",
|
|
200
|
-
"Roadmap done! Create task list (SHIFT+TAB for Tasks)",
|
|
201
|
-
"Planning complete! Break into tasks (SHIFT+TAB to Tasks)",
|
|
202
|
-
" ⚡ Plan ready! Advance to Tasks mode (SHIFT+TAB)",
|
|
129
|
+
"Continue working or start something new (SHIFT+TAB toggles mode)",
|
|
130
|
+
"What's next? (SHIFT+TAB to toggle Planning/Drafting)",
|
|
131
|
+
"Ready for the next task (SHIFT+TAB toggles Planning/Drafting)",
|
|
132
|
+
"Let's keep going! (SHIFT+TAB to toggle mode)",
|
|
133
|
+
"What else can I help with? (SHIFT+TAB for mode toggle)",
|
|
203
134
|
],
|
|
204
135
|
},
|
|
205
136
|
}
|
|
@@ -224,19 +155,22 @@ class PlaceholderHints:
|
|
|
224
155
|
Returns:
|
|
225
156
|
A contextual hint string for the placeholder.
|
|
226
157
|
"""
|
|
158
|
+
# Always use Router hints since Router is the only user-facing agent
|
|
159
|
+
mode_key = AgentType.ROUTER
|
|
160
|
+
|
|
227
161
|
# Default hint if mode not configured
|
|
228
|
-
if
|
|
229
|
-
return
|
|
162
|
+
if mode_key not in self.HINTS:
|
|
163
|
+
return "Enter your prompt (SHIFT+TAB to toggle Planning/Drafting mode)"
|
|
230
164
|
|
|
231
165
|
# For placeholder text, we default to "no content" state (initial hints)
|
|
232
166
|
# This avoids async file system checks in the UI rendering path
|
|
233
167
|
has_content = False
|
|
234
168
|
|
|
235
169
|
# Get hint variations for this mode and state
|
|
236
|
-
hints_list = self.HINTS[
|
|
170
|
+
hints_list = self.HINTS[mode_key][has_content]
|
|
237
171
|
|
|
238
172
|
# Cache key for this mode and state
|
|
239
|
-
cache_key = (
|
|
173
|
+
cache_key = (mode_key, has_content)
|
|
240
174
|
|
|
241
175
|
# Force refresh or first time
|
|
242
176
|
if force_refresh or cache_key not in self._cached_hints:
|
shotgun/tui/widgets/__init__.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Widget utilities and coordinators for TUI."""
|
|
2
2
|
|
|
3
|
+
from shotgun.tui.widgets.plan_panel import PlanPanelWidget
|
|
3
4
|
from shotgun.tui.widgets.widget_coordinator import WidgetCoordinator
|
|
4
5
|
|
|
5
|
-
__all__ = ["WidgetCoordinator"]
|
|
6
|
+
__all__ = ["PlanPanelWidget", "WidgetCoordinator"]
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Plan approval widget for Planning mode.
|
|
2
|
+
|
|
3
|
+
This widget displays when a multi-step plan is created in Planning mode,
|
|
4
|
+
allowing the user to approve or reject the plan before execution begins.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from textual import events, on
|
|
8
|
+
from textual.app import ComposeResult
|
|
9
|
+
from textual.containers import Horizontal, VerticalScroll
|
|
10
|
+
from textual.css.query import NoMatches
|
|
11
|
+
from textual.widget import Widget
|
|
12
|
+
from textual.widgets import Button, Static
|
|
13
|
+
|
|
14
|
+
from shotgun.agents.router.models import ExecutionPlan
|
|
15
|
+
from shotgun.tui.screens.chat_screen.messages import (
|
|
16
|
+
PlanApproved,
|
|
17
|
+
PlanRejected,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class PlanApprovalWidget(Widget):
|
|
22
|
+
"""Widget for plan approval in Planning mode.
|
|
23
|
+
|
|
24
|
+
Displays the execution plan summary with goal and steps,
|
|
25
|
+
and provides action buttons for the user to approve or reject.
|
|
26
|
+
|
|
27
|
+
Attributes:
|
|
28
|
+
plan: The execution plan that needs user approval.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
DEFAULT_CSS = """
|
|
32
|
+
PlanApprovalWidget {
|
|
33
|
+
background: $secondary-background-darken-1;
|
|
34
|
+
height: auto;
|
|
35
|
+
max-height: 20;
|
|
36
|
+
margin: 0 1;
|
|
37
|
+
padding: 1;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
PlanApprovalWidget .approval-header {
|
|
41
|
+
height: auto;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
PlanApprovalWidget .plan-content {
|
|
45
|
+
height: auto;
|
|
46
|
+
max-height: 12;
|
|
47
|
+
margin: 1 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
PlanApprovalWidget .plan-goal {
|
|
51
|
+
color: $text;
|
|
52
|
+
margin-bottom: 1;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
PlanApprovalWidget .plan-steps-label {
|
|
56
|
+
color: $text-muted;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
PlanApprovalWidget .step-item {
|
|
60
|
+
color: $text;
|
|
61
|
+
margin-left: 2;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
PlanApprovalWidget .approval-buttons {
|
|
65
|
+
height: auto;
|
|
66
|
+
width: 100%;
|
|
67
|
+
dock: bottom;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
PlanApprovalWidget Button {
|
|
71
|
+
margin-right: 1;
|
|
72
|
+
min-width: 18;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
PlanApprovalWidget #btn-approve {
|
|
76
|
+
background: $success;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
PlanApprovalWidget #btn-reject {
|
|
80
|
+
background: $error;
|
|
81
|
+
}
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
def __init__(self, plan: ExecutionPlan) -> None:
|
|
85
|
+
"""Initialize the approval widget.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
plan: The execution plan that needs user approval.
|
|
89
|
+
"""
|
|
90
|
+
super().__init__()
|
|
91
|
+
self.plan = plan
|
|
92
|
+
|
|
93
|
+
def compose(self) -> ComposeResult:
|
|
94
|
+
"""Compose the approval widget layout."""
|
|
95
|
+
# Header with step count
|
|
96
|
+
yield Static(
|
|
97
|
+
f"[bold]📋 Plan created with {len(self.plan.steps)} steps[/]",
|
|
98
|
+
classes="approval-header",
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Scrollable content area for goal and steps
|
|
102
|
+
with VerticalScroll(classes="plan-content"):
|
|
103
|
+
# Goal
|
|
104
|
+
yield Static(
|
|
105
|
+
f"[dim]Goal:[/] {self.plan.goal}",
|
|
106
|
+
classes="plan-goal",
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Steps list
|
|
110
|
+
yield Static("[dim]Steps:[/]", classes="plan-steps-label")
|
|
111
|
+
for i, step in enumerate(self.plan.steps, 1):
|
|
112
|
+
yield Static(
|
|
113
|
+
f"{i}. {step.title}",
|
|
114
|
+
classes="step-item",
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Action buttons (always visible at bottom)
|
|
118
|
+
with Horizontal(classes="approval-buttons"):
|
|
119
|
+
yield Button("✓ Go Ahead", id="btn-approve")
|
|
120
|
+
yield Button("✗ No, Let Me Clarify", id="btn-reject")
|
|
121
|
+
|
|
122
|
+
def on_mount(self) -> None:
|
|
123
|
+
"""Auto-focus the Go Ahead button on mount."""
|
|
124
|
+
try:
|
|
125
|
+
approve_btn = self.query_one("#btn-approve", Button)
|
|
126
|
+
approve_btn.focus()
|
|
127
|
+
except NoMatches:
|
|
128
|
+
pass
|
|
129
|
+
|
|
130
|
+
@on(Button.Pressed, "#btn-approve")
|
|
131
|
+
def handle_approve(self) -> None:
|
|
132
|
+
"""Handle Go Ahead button press."""
|
|
133
|
+
self.post_message(PlanApproved())
|
|
134
|
+
|
|
135
|
+
@on(Button.Pressed, "#btn-reject")
|
|
136
|
+
def handle_reject(self) -> None:
|
|
137
|
+
"""Handle No, Let Me Clarify button press."""
|
|
138
|
+
self.post_message(PlanRejected())
|
|
139
|
+
|
|
140
|
+
def on_key(self, event: events.Key) -> None:
|
|
141
|
+
"""Handle keyboard shortcuts for approval actions.
|
|
142
|
+
|
|
143
|
+
Shortcuts:
|
|
144
|
+
Enter/Y: Approve plan (Go Ahead)
|
|
145
|
+
Escape/N: Reject plan (No, Let Me Clarify)
|
|
146
|
+
"""
|
|
147
|
+
if event.key in ("enter", "y", "Y"):
|
|
148
|
+
self.post_message(PlanApproved())
|
|
149
|
+
event.stop()
|
|
150
|
+
elif event.key in ("escape", "n", "N"):
|
|
151
|
+
self.post_message(PlanRejected())
|
|
152
|
+
event.stop()
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""Cascade confirmation widget for Planning mode.
|
|
2
|
+
|
|
3
|
+
This widget displays after a file with dependents is updated,
|
|
4
|
+
allowing the user to choose which dependent files should also be updated.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from textual import events, on
|
|
8
|
+
from textual.app import ComposeResult
|
|
9
|
+
from textual.containers import Horizontal
|
|
10
|
+
from textual.css.query import NoMatches
|
|
11
|
+
from textual.widget import Widget
|
|
12
|
+
from textual.widgets import Button, Static
|
|
13
|
+
|
|
14
|
+
from shotgun.agents.router.models import CascadeScope
|
|
15
|
+
from shotgun.tui.screens.chat_screen.messages import (
|
|
16
|
+
CascadeConfirmed,
|
|
17
|
+
CascadeDeclined,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
# File descriptions for cascade confirmation UI
|
|
21
|
+
FILE_DESCRIPTIONS: dict[str, str] = {
|
|
22
|
+
"specification.md": "may need updated requirements",
|
|
23
|
+
"plan.md": "may need new implementation steps",
|
|
24
|
+
"tasks.md": "may need new tasks",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class CascadeConfirmationWidget(Widget):
|
|
29
|
+
"""Widget for cascade confirmation in Planning mode.
|
|
30
|
+
|
|
31
|
+
Displays information about the updated file and its dependents,
|
|
32
|
+
providing action buttons for the user to choose the cascade scope.
|
|
33
|
+
|
|
34
|
+
Attributes:
|
|
35
|
+
updated_file: The file that was just updated.
|
|
36
|
+
dependent_files: List of files that depend on the updated file.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
DEFAULT_CSS = """
|
|
40
|
+
CascadeConfirmationWidget {
|
|
41
|
+
background: $secondary-background-darken-1;
|
|
42
|
+
height: auto;
|
|
43
|
+
margin: 1;
|
|
44
|
+
padding: 1;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
CascadeConfirmationWidget .cascade-header {
|
|
48
|
+
margin-bottom: 1;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
CascadeConfirmationWidget .cascade-info {
|
|
52
|
+
color: $text-muted;
|
|
53
|
+
margin-bottom: 1;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
CascadeConfirmationWidget .dependent-file {
|
|
57
|
+
color: $text;
|
|
58
|
+
margin-left: 2;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
CascadeConfirmationWidget .cascade-question {
|
|
62
|
+
margin-top: 1;
|
|
63
|
+
margin-bottom: 1;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
CascadeConfirmationWidget .cascade-buttons {
|
|
67
|
+
height: auto;
|
|
68
|
+
width: 100%;
|
|
69
|
+
margin-top: 1;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
CascadeConfirmationWidget Button {
|
|
73
|
+
margin-right: 1;
|
|
74
|
+
min-width: 14;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
CascadeConfirmationWidget #btn-update-all {
|
|
78
|
+
background: $success;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
CascadeConfirmationWidget #btn-plan-only {
|
|
82
|
+
background: $primary;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
CascadeConfirmationWidget #btn-tasks-only {
|
|
86
|
+
background: $primary;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
CascadeConfirmationWidget #btn-decline {
|
|
90
|
+
background: $error;
|
|
91
|
+
}
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
def __init__(self, updated_file: str, dependent_files: list[str]) -> None:
|
|
95
|
+
"""Initialize the cascade confirmation widget.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
updated_file: The file that was just updated.
|
|
99
|
+
dependent_files: List of files that depend on the updated file.
|
|
100
|
+
"""
|
|
101
|
+
super().__init__()
|
|
102
|
+
self.updated_file = updated_file
|
|
103
|
+
self.dependent_files = dependent_files
|
|
104
|
+
|
|
105
|
+
def compose(self) -> ComposeResult:
|
|
106
|
+
"""Compose the cascade confirmation widget layout."""
|
|
107
|
+
# Header showing updated file
|
|
108
|
+
file_name = self.updated_file.split("/")[-1]
|
|
109
|
+
yield Static(
|
|
110
|
+
f"[bold green]✅ Updated {file_name}[/]",
|
|
111
|
+
classes="cascade-header",
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# Show dependent files
|
|
115
|
+
if self.dependent_files:
|
|
116
|
+
yield Static(
|
|
117
|
+
"[dim]This affects dependent files:[/]",
|
|
118
|
+
classes="cascade-info",
|
|
119
|
+
)
|
|
120
|
+
for dep_file in self.dependent_files:
|
|
121
|
+
dep_name = dep_file.split("/")[-1]
|
|
122
|
+
description = FILE_DESCRIPTIONS.get(dep_name, "may need updates")
|
|
123
|
+
yield Static(
|
|
124
|
+
f"• {dep_name} - {description}",
|
|
125
|
+
classes="dependent-file",
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
yield Static(
|
|
129
|
+
"Should I update these to match?",
|
|
130
|
+
classes="cascade-question",
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# Action buttons based on dependent files
|
|
134
|
+
with Horizontal(classes="cascade-buttons"):
|
|
135
|
+
if self.dependent_files:
|
|
136
|
+
# Update all button
|
|
137
|
+
yield Button("Update all", id="btn-update-all")
|
|
138
|
+
|
|
139
|
+
# Selective buttons if multiple dependents
|
|
140
|
+
if "plan.md" in self.dependent_files and len(self.dependent_files) > 1:
|
|
141
|
+
yield Button("Just plan.md", id="btn-plan-only")
|
|
142
|
+
if "tasks.md" in self.dependent_files and len(self.dependent_files) > 1:
|
|
143
|
+
yield Button("Just tasks.md", id="btn-tasks-only")
|
|
144
|
+
|
|
145
|
+
# Always show decline button
|
|
146
|
+
yield Button("No, I'll handle it", id="btn-decline")
|
|
147
|
+
|
|
148
|
+
def on_mount(self) -> None:
|
|
149
|
+
"""Auto-focus the Update all button on mount."""
|
|
150
|
+
try:
|
|
151
|
+
update_btn = self.query_one("#btn-update-all", Button)
|
|
152
|
+
update_btn.focus()
|
|
153
|
+
except NoMatches:
|
|
154
|
+
try:
|
|
155
|
+
decline_btn = self.query_one("#btn-decline", Button)
|
|
156
|
+
decline_btn.focus()
|
|
157
|
+
except NoMatches:
|
|
158
|
+
pass # No buttons to focus
|
|
159
|
+
|
|
160
|
+
@on(Button.Pressed, "#btn-update-all")
|
|
161
|
+
def handle_update_all(self) -> None:
|
|
162
|
+
"""Handle Update all button press."""
|
|
163
|
+
self.post_message(CascadeConfirmed(CascadeScope.ALL))
|
|
164
|
+
|
|
165
|
+
@on(Button.Pressed, "#btn-plan-only")
|
|
166
|
+
def handle_plan_only(self) -> None:
|
|
167
|
+
"""Handle Just plan.md button press."""
|
|
168
|
+
self.post_message(CascadeConfirmed(CascadeScope.PLAN_ONLY))
|
|
169
|
+
|
|
170
|
+
@on(Button.Pressed, "#btn-tasks-only")
|
|
171
|
+
def handle_tasks_only(self) -> None:
|
|
172
|
+
"""Handle Just tasks.md button press."""
|
|
173
|
+
self.post_message(CascadeConfirmed(CascadeScope.TASKS_ONLY))
|
|
174
|
+
|
|
175
|
+
@on(Button.Pressed, "#btn-decline")
|
|
176
|
+
def handle_decline(self) -> None:
|
|
177
|
+
"""Handle No button press."""
|
|
178
|
+
self.post_message(CascadeDeclined())
|
|
179
|
+
|
|
180
|
+
def on_key(self, event: events.Key) -> None:
|
|
181
|
+
"""Handle keyboard shortcuts for cascade actions.
|
|
182
|
+
|
|
183
|
+
Shortcuts:
|
|
184
|
+
Enter/A: Update all dependent files
|
|
185
|
+
P: Just update plan.md (if available)
|
|
186
|
+
T: Just update tasks.md (if available)
|
|
187
|
+
N/Escape: No, I'll handle it
|
|
188
|
+
"""
|
|
189
|
+
if event.key in ("enter", "a", "A"):
|
|
190
|
+
if self.dependent_files:
|
|
191
|
+
self.post_message(CascadeConfirmed(CascadeScope.ALL))
|
|
192
|
+
event.stop()
|
|
193
|
+
elif event.key in ("p", "P"):
|
|
194
|
+
if "plan.md" in self.dependent_files and len(self.dependent_files) > 1:
|
|
195
|
+
self.post_message(CascadeConfirmed(CascadeScope.PLAN_ONLY))
|
|
196
|
+
event.stop()
|
|
197
|
+
elif event.key in ("t", "T"):
|
|
198
|
+
if "tasks.md" in self.dependent_files and len(self.dependent_files) > 1:
|
|
199
|
+
self.post_message(CascadeConfirmed(CascadeScope.TASKS_ONLY))
|
|
200
|
+
event.stop()
|
|
201
|
+
elif event.key in ("n", "N", "escape"):
|
|
202
|
+
self.post_message(CascadeDeclined())
|
|
203
|
+
event.stop()
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Plan panel widget for displaying the current execution plan.
|
|
2
|
+
|
|
3
|
+
This widget displays the current RouterDeps execution plan.
|
|
4
|
+
It auto-shows when a plan exists and can be closed with × button.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from textual import on
|
|
8
|
+
from textual.app import ComposeResult
|
|
9
|
+
from textual.containers import Horizontal
|
|
10
|
+
from textual.widget import Widget
|
|
11
|
+
from textual.widgets import Button, Static
|
|
12
|
+
|
|
13
|
+
from shotgun.agents.router.models import ExecutionPlan
|
|
14
|
+
from shotgun.tui.screens.chat_screen.messages import PlanPanelClosed
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PlanPanelWidget(Widget):
|
|
18
|
+
"""Widget for displaying the current execution plan.
|
|
19
|
+
|
|
20
|
+
Displays the plan goal and steps with status indicators.
|
|
21
|
+
Provides a close button to dismiss the panel.
|
|
22
|
+
|
|
23
|
+
Attributes:
|
|
24
|
+
plan: The execution plan to display.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
DEFAULT_CSS = """
|
|
28
|
+
PlanPanelWidget {
|
|
29
|
+
background: $secondary-background-darken-1;
|
|
30
|
+
height: auto;
|
|
31
|
+
max-height: 10;
|
|
32
|
+
margin: 0 1;
|
|
33
|
+
padding: 1;
|
|
34
|
+
border: solid $primary-darken-2;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
PlanPanelWidget .panel-header-row {
|
|
38
|
+
height: auto;
|
|
39
|
+
margin-bottom: 1;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
PlanPanelWidget .panel-header {
|
|
43
|
+
color: $text-accent;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
PlanPanelWidget #btn-close {
|
|
47
|
+
dock: right;
|
|
48
|
+
min-width: 3;
|
|
49
|
+
height: 1;
|
|
50
|
+
background: transparent;
|
|
51
|
+
border: none;
|
|
52
|
+
color: $text-muted;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
PlanPanelWidget #btn-close:hover {
|
|
56
|
+
color: $error;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
PlanPanelWidget .plan-goal {
|
|
60
|
+
color: $text;
|
|
61
|
+
margin-bottom: 1;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
PlanPanelWidget .step-item {
|
|
65
|
+
color: $text;
|
|
66
|
+
margin-left: 2;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
PlanPanelWidget .step-done {
|
|
70
|
+
color: $text-muted;
|
|
71
|
+
margin-left: 2;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
PlanPanelWidget .step-current {
|
|
75
|
+
color: $text-accent;
|
|
76
|
+
margin-left: 2;
|
|
77
|
+
}
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
def __init__(self, plan: ExecutionPlan) -> None:
|
|
81
|
+
"""Initialize the plan panel widget.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
plan: The execution plan to display.
|
|
85
|
+
"""
|
|
86
|
+
super().__init__()
|
|
87
|
+
self._plan = plan
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def plan(self) -> ExecutionPlan:
|
|
91
|
+
"""Get the current plan."""
|
|
92
|
+
return self._plan
|
|
93
|
+
|
|
94
|
+
def update_plan(self, plan: ExecutionPlan) -> None:
|
|
95
|
+
"""Update the displayed plan and refresh the widget.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
plan: The new plan to display.
|
|
99
|
+
"""
|
|
100
|
+
self._plan = plan
|
|
101
|
+
self.refresh(layout=True)
|
|
102
|
+
|
|
103
|
+
def compose(self) -> ComposeResult:
|
|
104
|
+
"""Compose the plan panel layout."""
|
|
105
|
+
# Header with close button
|
|
106
|
+
with Horizontal(classes="panel-header-row"):
|
|
107
|
+
yield Static("[bold]📋 Execution Plan[/]", classes="panel-header")
|
|
108
|
+
yield Button("×", id="btn-close")
|
|
109
|
+
|
|
110
|
+
# Goal
|
|
111
|
+
yield Static(f"[dim]Goal:[/] {self._plan.goal}", classes="plan-goal")
|
|
112
|
+
|
|
113
|
+
# Steps
|
|
114
|
+
for i, step in enumerate(self._plan.steps):
|
|
115
|
+
marker = "✅" if step.done else "⬜"
|
|
116
|
+
current = (
|
|
117
|
+
" ◀" if i == self._plan.current_step_index and not step.done else ""
|
|
118
|
+
)
|
|
119
|
+
css_class = (
|
|
120
|
+
"step-done"
|
|
121
|
+
if step.done
|
|
122
|
+
else ("step-current" if current else "step-item")
|
|
123
|
+
)
|
|
124
|
+
yield Static(f"{i + 1}. {marker} {step.title}{current}", classes=css_class)
|
|
125
|
+
|
|
126
|
+
@on(Button.Pressed, "#btn-close")
|
|
127
|
+
def handle_close(self) -> None:
|
|
128
|
+
"""Handle close button press."""
|
|
129
|
+
self.post_message(PlanPanelClosed())
|