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.
Files changed (159) hide show
  1. shotgun/agents/agent_manager.py +497 -30
  2. shotgun/agents/cancellation.py +103 -0
  3. shotgun/agents/common.py +90 -77
  4. shotgun/agents/config/README.md +0 -1
  5. shotgun/agents/config/manager.py +52 -8
  6. shotgun/agents/config/models.py +21 -27
  7. shotgun/agents/config/provider.py +44 -27
  8. shotgun/agents/conversation/history/file_content_deduplication.py +66 -43
  9. shotgun/agents/conversation/history/token_counting/base.py +51 -9
  10. shotgun/agents/export.py +12 -13
  11. shotgun/agents/file_read.py +176 -0
  12. shotgun/agents/messages.py +15 -3
  13. shotgun/agents/models.py +90 -2
  14. shotgun/agents/plan.py +12 -13
  15. shotgun/agents/research.py +13 -10
  16. shotgun/agents/router/__init__.py +47 -0
  17. shotgun/agents/router/models.py +384 -0
  18. shotgun/agents/router/router.py +185 -0
  19. shotgun/agents/router/tools/__init__.py +18 -0
  20. shotgun/agents/router/tools/delegation_tools.py +557 -0
  21. shotgun/agents/router/tools/plan_tools.py +403 -0
  22. shotgun/agents/runner.py +17 -2
  23. shotgun/agents/specify.py +12 -13
  24. shotgun/agents/tasks.py +12 -13
  25. shotgun/agents/tools/__init__.py +8 -0
  26. shotgun/agents/tools/codebase/directory_lister.py +27 -39
  27. shotgun/agents/tools/codebase/file_read.py +26 -35
  28. shotgun/agents/tools/codebase/query_graph.py +9 -0
  29. shotgun/agents/tools/codebase/retrieve_code.py +9 -0
  30. shotgun/agents/tools/file_management.py +81 -3
  31. shotgun/agents/tools/file_read_tools/__init__.py +7 -0
  32. shotgun/agents/tools/file_read_tools/multimodal_file_read.py +167 -0
  33. shotgun/agents/tools/markdown_tools/__init__.py +62 -0
  34. shotgun/agents/tools/markdown_tools/insert_section.py +148 -0
  35. shotgun/agents/tools/markdown_tools/models.py +86 -0
  36. shotgun/agents/tools/markdown_tools/remove_section.py +114 -0
  37. shotgun/agents/tools/markdown_tools/replace_section.py +119 -0
  38. shotgun/agents/tools/markdown_tools/utils.py +453 -0
  39. shotgun/agents/tools/registry.py +46 -6
  40. shotgun/agents/tools/web_search/__init__.py +1 -2
  41. shotgun/agents/tools/web_search/gemini.py +1 -3
  42. shotgun/agents/tools/web_search/openai.py +42 -23
  43. shotgun/attachments/__init__.py +41 -0
  44. shotgun/attachments/errors.py +60 -0
  45. shotgun/attachments/models.py +107 -0
  46. shotgun/attachments/parser.py +257 -0
  47. shotgun/attachments/processor.py +193 -0
  48. shotgun/build_constants.py +4 -7
  49. shotgun/cli/clear.py +2 -2
  50. shotgun/cli/codebase/commands.py +181 -65
  51. shotgun/cli/compact.py +2 -2
  52. shotgun/cli/context.py +2 -2
  53. shotgun/cli/error_handler.py +2 -2
  54. shotgun/cli/run.py +90 -0
  55. shotgun/cli/spec/backup.py +2 -1
  56. shotgun/codebase/__init__.py +2 -0
  57. shotgun/codebase/benchmarks/__init__.py +35 -0
  58. shotgun/codebase/benchmarks/benchmark_runner.py +309 -0
  59. shotgun/codebase/benchmarks/exporters.py +119 -0
  60. shotgun/codebase/benchmarks/formatters/__init__.py +49 -0
  61. shotgun/codebase/benchmarks/formatters/base.py +34 -0
  62. shotgun/codebase/benchmarks/formatters/json_formatter.py +106 -0
  63. shotgun/codebase/benchmarks/formatters/markdown.py +136 -0
  64. shotgun/codebase/benchmarks/models.py +129 -0
  65. shotgun/codebase/core/__init__.py +4 -0
  66. shotgun/codebase/core/call_resolution.py +91 -0
  67. shotgun/codebase/core/change_detector.py +11 -6
  68. shotgun/codebase/core/errors.py +159 -0
  69. shotgun/codebase/core/extractors/__init__.py +23 -0
  70. shotgun/codebase/core/extractors/base.py +138 -0
  71. shotgun/codebase/core/extractors/factory.py +63 -0
  72. shotgun/codebase/core/extractors/go/__init__.py +7 -0
  73. shotgun/codebase/core/extractors/go/extractor.py +122 -0
  74. shotgun/codebase/core/extractors/javascript/__init__.py +7 -0
  75. shotgun/codebase/core/extractors/javascript/extractor.py +132 -0
  76. shotgun/codebase/core/extractors/protocol.py +109 -0
  77. shotgun/codebase/core/extractors/python/__init__.py +7 -0
  78. shotgun/codebase/core/extractors/python/extractor.py +141 -0
  79. shotgun/codebase/core/extractors/rust/__init__.py +7 -0
  80. shotgun/codebase/core/extractors/rust/extractor.py +139 -0
  81. shotgun/codebase/core/extractors/types.py +15 -0
  82. shotgun/codebase/core/extractors/typescript/__init__.py +7 -0
  83. shotgun/codebase/core/extractors/typescript/extractor.py +92 -0
  84. shotgun/codebase/core/gitignore.py +252 -0
  85. shotgun/codebase/core/ingestor.py +644 -354
  86. shotgun/codebase/core/kuzu_compat.py +119 -0
  87. shotgun/codebase/core/language_config.py +239 -0
  88. shotgun/codebase/core/manager.py +256 -46
  89. shotgun/codebase/core/metrics_collector.py +310 -0
  90. shotgun/codebase/core/metrics_types.py +347 -0
  91. shotgun/codebase/core/parallel_executor.py +424 -0
  92. shotgun/codebase/core/work_distributor.py +254 -0
  93. shotgun/codebase/core/worker.py +768 -0
  94. shotgun/codebase/indexing_state.py +86 -0
  95. shotgun/codebase/models.py +94 -0
  96. shotgun/codebase/service.py +13 -0
  97. shotgun/exceptions.py +9 -9
  98. shotgun/main.py +3 -16
  99. shotgun/posthog_telemetry.py +165 -24
  100. shotgun/prompts/agents/export.j2 +2 -0
  101. shotgun/prompts/agents/file_read.j2 +48 -0
  102. shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +19 -52
  103. shotgun/prompts/agents/partials/content_formatting.j2 +12 -33
  104. shotgun/prompts/agents/partials/interactive_mode.j2 +9 -32
  105. shotgun/prompts/agents/partials/router_delegation_mode.j2 +35 -0
  106. shotgun/prompts/agents/plan.j2 +38 -12
  107. shotgun/prompts/agents/research.j2 +70 -31
  108. shotgun/prompts/agents/router.j2 +713 -0
  109. shotgun/prompts/agents/specify.j2 +53 -16
  110. shotgun/prompts/agents/state/codebase/codebase_graphs_available.j2 +14 -1
  111. shotgun/prompts/agents/state/system_state.j2 +24 -13
  112. shotgun/prompts/agents/tasks.j2 +72 -34
  113. shotgun/settings.py +49 -10
  114. shotgun/tui/app.py +154 -24
  115. shotgun/tui/commands/__init__.py +9 -1
  116. shotgun/tui/components/attachment_bar.py +87 -0
  117. shotgun/tui/components/mode_indicator.py +120 -25
  118. shotgun/tui/components/prompt_input.py +25 -28
  119. shotgun/tui/components/status_bar.py +14 -7
  120. shotgun/tui/dependencies.py +58 -8
  121. shotgun/tui/protocols.py +55 -0
  122. shotgun/tui/screens/chat/chat.tcss +24 -1
  123. shotgun/tui/screens/chat/chat_screen.py +1376 -213
  124. shotgun/tui/screens/chat/codebase_index_prompt_screen.py +8 -4
  125. shotgun/tui/screens/chat_screen/attachment_hint.py +40 -0
  126. shotgun/tui/screens/chat_screen/command_providers.py +0 -97
  127. shotgun/tui/screens/chat_screen/history/agent_response.py +7 -3
  128. shotgun/tui/screens/chat_screen/history/chat_history.py +58 -6
  129. shotgun/tui/screens/chat_screen/history/formatters.py +75 -15
  130. shotgun/tui/screens/chat_screen/history/partial_response.py +11 -1
  131. shotgun/tui/screens/chat_screen/history/user_question.py +25 -3
  132. shotgun/tui/screens/chat_screen/messages.py +219 -0
  133. shotgun/tui/screens/database_locked_dialog.py +219 -0
  134. shotgun/tui/screens/database_timeout_dialog.py +158 -0
  135. shotgun/tui/screens/kuzu_error_dialog.py +135 -0
  136. shotgun/tui/screens/model_picker.py +1 -3
  137. shotgun/tui/screens/models.py +11 -0
  138. shotgun/tui/state/processing_state.py +19 -0
  139. shotgun/tui/utils/mode_progress.py +20 -86
  140. shotgun/tui/widgets/__init__.py +2 -1
  141. shotgun/tui/widgets/approval_widget.py +152 -0
  142. shotgun/tui/widgets/cascade_confirmation_widget.py +203 -0
  143. shotgun/tui/widgets/plan_panel.py +129 -0
  144. shotgun/tui/widgets/step_checkpoint_widget.py +180 -0
  145. shotgun/tui/widgets/widget_coordinator.py +18 -0
  146. shotgun/utils/file_system_utils.py +4 -1
  147. {shotgun_sh-0.3.3.dev1.dist-info → shotgun_sh-0.6.2.dist-info}/METADATA +88 -35
  148. shotgun_sh-0.6.2.dist-info/RECORD +291 -0
  149. shotgun/cli/export.py +0 -81
  150. shotgun/cli/plan.py +0 -73
  151. shotgun/cli/research.py +0 -93
  152. shotgun/cli/specify.py +0 -70
  153. shotgun/cli/tasks.py +0 -78
  154. shotgun/sentry_telemetry.py +0 -232
  155. shotgun/tui/screens/onboarding.py +0 -580
  156. shotgun_sh-0.3.3.dev1.dist-info/RECORD +0 -229
  157. {shotgun_sh-0.3.3.dev1.dist-info → shotgun_sh-0.6.2.dist-info}/WHEEL +0 -0
  158. {shotgun_sh-0.3.3.dev1.dist-info → shotgun_sh-0.6.2.dist-info}/entry_points.txt +0 -0
  159. {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
- return Path.home() / ".shotgun-sh"
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: