shotgun-sh 0.3.3.dev1__py3-none-any.whl → 0.6.2__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 +497 -30
- shotgun/agents/cancellation.py +103 -0
- shotgun/agents/common.py +90 -77
- shotgun/agents/config/README.md +0 -1
- shotgun/agents/config/manager.py +52 -8
- shotgun/agents/config/models.py +21 -27
- shotgun/agents/config/provider.py +44 -27
- shotgun/agents/conversation/history/file_content_deduplication.py +66 -43
- shotgun/agents/conversation/history/token_counting/base.py +51 -9
- shotgun/agents/export.py +12 -13
- shotgun/agents/file_read.py +176 -0
- shotgun/agents/messages.py +15 -3
- shotgun/agents/models.py +90 -2
- shotgun/agents/plan.py +12 -13
- shotgun/agents/research.py +13 -10
- shotgun/agents/router/__init__.py +47 -0
- shotgun/agents/router/models.py +384 -0
- shotgun/agents/router/router.py +185 -0
- shotgun/agents/router/tools/__init__.py +18 -0
- shotgun/agents/router/tools/delegation_tools.py +557 -0
- shotgun/agents/router/tools/plan_tools.py +403 -0
- shotgun/agents/runner.py +17 -2
- shotgun/agents/specify.py +12 -13
- shotgun/agents/tasks.py +12 -13
- shotgun/agents/tools/__init__.py +8 -0
- shotgun/agents/tools/codebase/directory_lister.py +27 -39
- shotgun/agents/tools/codebase/file_read.py +26 -35
- shotgun/agents/tools/codebase/query_graph.py +9 -0
- shotgun/agents/tools/codebase/retrieve_code.py +9 -0
- shotgun/agents/tools/file_management.py +81 -3
- shotgun/agents/tools/file_read_tools/__init__.py +7 -0
- shotgun/agents/tools/file_read_tools/multimodal_file_read.py +167 -0
- shotgun/agents/tools/markdown_tools/__init__.py +62 -0
- shotgun/agents/tools/markdown_tools/insert_section.py +148 -0
- shotgun/agents/tools/markdown_tools/models.py +86 -0
- shotgun/agents/tools/markdown_tools/remove_section.py +114 -0
- shotgun/agents/tools/markdown_tools/replace_section.py +119 -0
- shotgun/agents/tools/markdown_tools/utils.py +453 -0
- shotgun/agents/tools/registry.py +46 -6
- shotgun/agents/tools/web_search/__init__.py +1 -2
- shotgun/agents/tools/web_search/gemini.py +1 -3
- shotgun/agents/tools/web_search/openai.py +42 -23
- shotgun/attachments/__init__.py +41 -0
- shotgun/attachments/errors.py +60 -0
- shotgun/attachments/models.py +107 -0
- shotgun/attachments/parser.py +257 -0
- shotgun/attachments/processor.py +193 -0
- shotgun/build_constants.py +4 -7
- shotgun/cli/clear.py +2 -2
- shotgun/cli/codebase/commands.py +181 -65
- shotgun/cli/compact.py +2 -2
- shotgun/cli/context.py +2 -2
- shotgun/cli/error_handler.py +2 -2
- shotgun/cli/run.py +90 -0
- shotgun/cli/spec/backup.py +2 -1
- shotgun/codebase/__init__.py +2 -0
- shotgun/codebase/benchmarks/__init__.py +35 -0
- shotgun/codebase/benchmarks/benchmark_runner.py +309 -0
- shotgun/codebase/benchmarks/exporters.py +119 -0
- shotgun/codebase/benchmarks/formatters/__init__.py +49 -0
- shotgun/codebase/benchmarks/formatters/base.py +34 -0
- shotgun/codebase/benchmarks/formatters/json_formatter.py +106 -0
- shotgun/codebase/benchmarks/formatters/markdown.py +136 -0
- shotgun/codebase/benchmarks/models.py +129 -0
- shotgun/codebase/core/__init__.py +4 -0
- shotgun/codebase/core/call_resolution.py +91 -0
- shotgun/codebase/core/change_detector.py +11 -6
- shotgun/codebase/core/errors.py +159 -0
- shotgun/codebase/core/extractors/__init__.py +23 -0
- shotgun/codebase/core/extractors/base.py +138 -0
- shotgun/codebase/core/extractors/factory.py +63 -0
- shotgun/codebase/core/extractors/go/__init__.py +7 -0
- shotgun/codebase/core/extractors/go/extractor.py +122 -0
- shotgun/codebase/core/extractors/javascript/__init__.py +7 -0
- shotgun/codebase/core/extractors/javascript/extractor.py +132 -0
- shotgun/codebase/core/extractors/protocol.py +109 -0
- shotgun/codebase/core/extractors/python/__init__.py +7 -0
- shotgun/codebase/core/extractors/python/extractor.py +141 -0
- shotgun/codebase/core/extractors/rust/__init__.py +7 -0
- shotgun/codebase/core/extractors/rust/extractor.py +139 -0
- shotgun/codebase/core/extractors/types.py +15 -0
- shotgun/codebase/core/extractors/typescript/__init__.py +7 -0
- shotgun/codebase/core/extractors/typescript/extractor.py +92 -0
- shotgun/codebase/core/gitignore.py +252 -0
- shotgun/codebase/core/ingestor.py +644 -354
- shotgun/codebase/core/kuzu_compat.py +119 -0
- shotgun/codebase/core/language_config.py +239 -0
- shotgun/codebase/core/manager.py +256 -46
- shotgun/codebase/core/metrics_collector.py +310 -0
- shotgun/codebase/core/metrics_types.py +347 -0
- shotgun/codebase/core/parallel_executor.py +424 -0
- shotgun/codebase/core/work_distributor.py +254 -0
- shotgun/codebase/core/worker.py +768 -0
- shotgun/codebase/indexing_state.py +86 -0
- shotgun/codebase/models.py +94 -0
- shotgun/codebase/service.py +13 -0
- shotgun/exceptions.py +9 -9
- shotgun/main.py +3 -16
- shotgun/posthog_telemetry.py +165 -24
- shotgun/prompts/agents/export.j2 +2 -0
- shotgun/prompts/agents/file_read.j2 +48 -0
- shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +19 -52
- shotgun/prompts/agents/partials/content_formatting.j2 +12 -33
- shotgun/prompts/agents/partials/interactive_mode.j2 +9 -32
- shotgun/prompts/agents/partials/router_delegation_mode.j2 +35 -0
- shotgun/prompts/agents/plan.j2 +38 -12
- shotgun/prompts/agents/research.j2 +70 -31
- shotgun/prompts/agents/router.j2 +713 -0
- shotgun/prompts/agents/specify.j2 +53 -16
- shotgun/prompts/agents/state/codebase/codebase_graphs_available.j2 +14 -1
- shotgun/prompts/agents/state/system_state.j2 +24 -13
- shotgun/prompts/agents/tasks.j2 +72 -34
- shotgun/settings.py +49 -10
- shotgun/tui/app.py +154 -24
- shotgun/tui/commands/__init__.py +9 -1
- shotgun/tui/components/attachment_bar.py +87 -0
- shotgun/tui/components/mode_indicator.py +120 -25
- shotgun/tui/components/prompt_input.py +25 -28
- shotgun/tui/components/status_bar.py +14 -7
- shotgun/tui/dependencies.py +58 -8
- shotgun/tui/protocols.py +55 -0
- shotgun/tui/screens/chat/chat.tcss +24 -1
- shotgun/tui/screens/chat/chat_screen.py +1376 -213
- shotgun/tui/screens/chat/codebase_index_prompt_screen.py +8 -4
- shotgun/tui/screens/chat_screen/attachment_hint.py +40 -0
- shotgun/tui/screens/chat_screen/command_providers.py +0 -97
- shotgun/tui/screens/chat_screen/history/agent_response.py +7 -3
- shotgun/tui/screens/chat_screen/history/chat_history.py +58 -6
- shotgun/tui/screens/chat_screen/history/formatters.py +75 -15
- shotgun/tui/screens/chat_screen/history/partial_response.py +11 -1
- shotgun/tui/screens/chat_screen/history/user_question.py +25 -3
- shotgun/tui/screens/chat_screen/messages.py +219 -0
- shotgun/tui/screens/database_locked_dialog.py +219 -0
- shotgun/tui/screens/database_timeout_dialog.py +158 -0
- shotgun/tui/screens/kuzu_error_dialog.py +135 -0
- shotgun/tui/screens/model_picker.py +1 -3
- shotgun/tui/screens/models.py +11 -0
- shotgun/tui/state/processing_state.py +19 -0
- 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/tui/widgets/widget_coordinator.py +18 -0
- shotgun/utils/file_system_utils.py +4 -1
- {shotgun_sh-0.3.3.dev1.dist-info → shotgun_sh-0.6.2.dist-info}/METADATA +88 -35
- shotgun_sh-0.6.2.dist-info/RECORD +291 -0
- shotgun/cli/export.py +0 -81
- shotgun/cli/plan.py +0 -73
- shotgun/cli/research.py +0 -93
- shotgun/cli/specify.py +0 -70
- shotgun/cli/tasks.py +0 -78
- shotgun/sentry_telemetry.py +0 -232
- shotgun/tui/screens/onboarding.py +0 -580
- shotgun_sh-0.3.3.dev1.dist-info/RECORD +0 -229
- {shotgun_sh-0.3.3.dev1.dist-info → shotgun_sh-0.6.2.dist-info}/WHEEL +0 -0
- {shotgun_sh-0.3.3.dev1.dist-info → shotgun_sh-0.6.2.dist-info}/entry_points.txt +0 -0
- {shotgun_sh-0.3.3.dev1.dist-info → shotgun_sh-0.6.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -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())
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""Step checkpoint widget for Planning mode.
|
|
2
|
+
|
|
3
|
+
This widget displays after each step completes in Planning mode,
|
|
4
|
+
allowing the user to continue, modify the plan, or stop execution.
|
|
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 ExecutionStep
|
|
15
|
+
from shotgun.tui.screens.chat_screen.messages import (
|
|
16
|
+
CheckpointContinue,
|
|
17
|
+
CheckpointModify,
|
|
18
|
+
CheckpointStop,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class StepCheckpointWidget(Widget):
|
|
23
|
+
"""Widget for step completion checkpoints in Planning mode.
|
|
24
|
+
|
|
25
|
+
Displays information about the completed step and provides
|
|
26
|
+
action buttons for the user to choose how to proceed.
|
|
27
|
+
|
|
28
|
+
Attributes:
|
|
29
|
+
step: The step that was just completed.
|
|
30
|
+
next_step: The next step to execute, or None if this was the last step.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
DEFAULT_CSS = """
|
|
34
|
+
StepCheckpointWidget {
|
|
35
|
+
background: $secondary-background-darken-1;
|
|
36
|
+
height: auto;
|
|
37
|
+
margin: 1;
|
|
38
|
+
padding: 1;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
StepCheckpointWidget .checkpoint-header {
|
|
42
|
+
margin-bottom: 1;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
StepCheckpointWidget .next-step-preview {
|
|
46
|
+
color: $text-muted;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
StepCheckpointWidget .checkpoint-buttons {
|
|
50
|
+
height: auto;
|
|
51
|
+
width: 100%;
|
|
52
|
+
margin-top: 1;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
StepCheckpointWidget Button {
|
|
56
|
+
margin-right: 1;
|
|
57
|
+
min-width: 14;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
StepCheckpointWidget #btn-continue {
|
|
61
|
+
background: $success;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
StepCheckpointWidget #btn-modify {
|
|
65
|
+
background: $warning;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
StepCheckpointWidget #btn-stop {
|
|
69
|
+
background: $error;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
StepCheckpointWidget #btn-done {
|
|
73
|
+
background: $success;
|
|
74
|
+
}
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
def __init__(self, step: ExecutionStep, next_step: ExecutionStep | None) -> None:
|
|
78
|
+
"""Initialize the checkpoint widget.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
step: The step that was just completed.
|
|
82
|
+
next_step: The next step to execute, or None if last step.
|
|
83
|
+
"""
|
|
84
|
+
super().__init__()
|
|
85
|
+
self.step = step
|
|
86
|
+
self.next_step = next_step
|
|
87
|
+
|
|
88
|
+
def compose(self) -> ComposeResult:
|
|
89
|
+
"""Compose the checkpoint widget layout."""
|
|
90
|
+
if self.next_step:
|
|
91
|
+
# Mid-plan checkpoint: show step completed with next step preview
|
|
92
|
+
yield Static(
|
|
93
|
+
f"[bold green]✅ Step completed:[/] {self.step.title}",
|
|
94
|
+
classes="checkpoint-header",
|
|
95
|
+
)
|
|
96
|
+
yield Static(
|
|
97
|
+
f"[dim]Next:[/] {self.next_step.title}",
|
|
98
|
+
classes="next-step-preview",
|
|
99
|
+
)
|
|
100
|
+
with Horizontal(classes="checkpoint-buttons"):
|
|
101
|
+
yield Button("Continue", id="btn-continue")
|
|
102
|
+
yield Button("Modify plan", id="btn-modify")
|
|
103
|
+
yield Button("Stop here", id="btn-stop")
|
|
104
|
+
else:
|
|
105
|
+
# Plan completed: show completion message with Done button only
|
|
106
|
+
yield Static(
|
|
107
|
+
"[bold green]✅ Plan completed![/]",
|
|
108
|
+
classes="checkpoint-header",
|
|
109
|
+
)
|
|
110
|
+
yield Static(
|
|
111
|
+
f"[dim]Final step:[/] {self.step.title}",
|
|
112
|
+
classes="next-step-preview",
|
|
113
|
+
)
|
|
114
|
+
with Horizontal(classes="checkpoint-buttons"):
|
|
115
|
+
yield Button("Done", id="btn-done")
|
|
116
|
+
|
|
117
|
+
def on_mount(self) -> None:
|
|
118
|
+
"""Auto-focus the appropriate button on mount."""
|
|
119
|
+
# Auto-focus Continue button if available, otherwise Done, then Modify
|
|
120
|
+
try:
|
|
121
|
+
continue_btn = self.query_one("#btn-continue", Button)
|
|
122
|
+
continue_btn.focus()
|
|
123
|
+
except NoMatches:
|
|
124
|
+
try:
|
|
125
|
+
done_btn = self.query_one("#btn-done", Button)
|
|
126
|
+
done_btn.focus()
|
|
127
|
+
except NoMatches:
|
|
128
|
+
try:
|
|
129
|
+
modify_btn = self.query_one("#btn-modify", Button)
|
|
130
|
+
modify_btn.focus()
|
|
131
|
+
except NoMatches:
|
|
132
|
+
pass # No buttons to focus
|
|
133
|
+
|
|
134
|
+
@on(Button.Pressed, "#btn-continue")
|
|
135
|
+
def handle_continue(self) -> None:
|
|
136
|
+
"""Handle Continue button press."""
|
|
137
|
+
self.post_message(CheckpointContinue())
|
|
138
|
+
|
|
139
|
+
@on(Button.Pressed, "#btn-modify")
|
|
140
|
+
def handle_modify(self) -> None:
|
|
141
|
+
"""Handle Modify plan button press."""
|
|
142
|
+
self.post_message(CheckpointModify())
|
|
143
|
+
|
|
144
|
+
@on(Button.Pressed, "#btn-stop")
|
|
145
|
+
def handle_stop(self) -> None:
|
|
146
|
+
"""Handle Stop here button press."""
|
|
147
|
+
self.post_message(CheckpointStop())
|
|
148
|
+
|
|
149
|
+
@on(Button.Pressed, "#btn-done")
|
|
150
|
+
def handle_done(self) -> None:
|
|
151
|
+
"""Handle Done button press (plan completed)."""
|
|
152
|
+
self.post_message(CheckpointStop())
|
|
153
|
+
|
|
154
|
+
def on_key(self, event: events.Key) -> None:
|
|
155
|
+
"""Handle keyboard shortcuts for checkpoint actions.
|
|
156
|
+
|
|
157
|
+
Shortcuts:
|
|
158
|
+
Enter/C: Continue to next step (if available), or Done (if plan complete)
|
|
159
|
+
M: Modify the plan (only if not complete)
|
|
160
|
+
S/Escape: Stop execution (only if not complete)
|
|
161
|
+
"""
|
|
162
|
+
if event.key in ("enter", "c", "C"):
|
|
163
|
+
if self.next_step:
|
|
164
|
+
self.post_message(CheckpointContinue())
|
|
165
|
+
else:
|
|
166
|
+
# Plan complete - Enter dismisses
|
|
167
|
+
self.post_message(CheckpointStop())
|
|
168
|
+
event.stop()
|
|
169
|
+
elif event.key in ("m", "M"):
|
|
170
|
+
if self.next_step:
|
|
171
|
+
self.post_message(CheckpointModify())
|
|
172
|
+
event.stop()
|
|
173
|
+
elif event.key in ("s", "S", "escape"):
|
|
174
|
+
if self.next_step:
|
|
175
|
+
self.post_message(CheckpointStop())
|
|
176
|
+
event.stop()
|
|
177
|
+
else:
|
|
178
|
+
# Plan complete - Escape also dismisses
|
|
179
|
+
self.post_message(CheckpointStop())
|
|
180
|
+
event.stop()
|
|
@@ -24,6 +24,7 @@ from shotgun.tui.screens.chat_screen.history.chat_history import ChatHistory
|
|
|
24
24
|
|
|
25
25
|
if TYPE_CHECKING:
|
|
26
26
|
from shotgun.agents.context_analyzer.models import ContextAnalysis
|
|
27
|
+
from shotgun.attachments import FileAttachment
|
|
27
28
|
from shotgun.tui.screens.chat import ChatScreen
|
|
28
29
|
from shotgun.tui.screens.chat_screen.hint_message import HintMessage
|
|
29
30
|
|
|
@@ -261,3 +262,20 @@ class WidgetCoordinator:
|
|
|
261
262
|
context_indicator.set_streaming(streaming)
|
|
262
263
|
except Exception as e:
|
|
263
264
|
logger.exception(f"Failed to set context streaming: {e}")
|
|
265
|
+
|
|
266
|
+
def update_attachment_bar(self, attachment: "FileAttachment | None") -> None:
|
|
267
|
+
"""Update the attachment bar with pending attachment.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
attachment: FileAttachment to display, or None to hide bar.
|
|
271
|
+
"""
|
|
272
|
+
if not self.screen.is_mounted:
|
|
273
|
+
return
|
|
274
|
+
|
|
275
|
+
try:
|
|
276
|
+
from shotgun.tui.components.attachment_bar import AttachmentBar
|
|
277
|
+
|
|
278
|
+
attachment_bar = self.screen.query_one(AttachmentBar)
|
|
279
|
+
attachment_bar.update_attachment(attachment)
|
|
280
|
+
except Exception as e:
|
|
281
|
+
logger.exception(f"Failed to update attachment bar: {e}")
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""File system utility functions."""
|
|
2
2
|
|
|
3
|
+
import os
|
|
3
4
|
from pathlib import Path
|
|
4
5
|
|
|
5
6
|
import aiofiles
|
|
@@ -24,7 +25,9 @@ def get_shotgun_home() -> Path:
|
|
|
24
25
|
if custom_home := settings.dev.home:
|
|
25
26
|
return Path(custom_home)
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
# Use os.path.join for explicit path separator handling on Windows
|
|
29
|
+
# This avoids potential edge cases with pathlib's / operator
|
|
30
|
+
return Path(os.path.join(os.path.expanduser("~"), ".shotgun-sh"))
|
|
28
31
|
|
|
29
32
|
|
|
30
33
|
def ensure_shotgun_directory_exists() -> Path:
|