shotgun-sh 0.2.29.dev2__py3-none-any.whl → 0.6.1.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.

Potentially problematic release.


This version of shotgun-sh might be problematic. Click here for more details.

Files changed (161) 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 +48 -45
  7. shotgun/agents/config/provider.py +44 -29
  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 +41 -0
  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/cli/clear.py +2 -2
  49. shotgun/cli/codebase/commands.py +181 -65
  50. shotgun/cli/compact.py +2 -2
  51. shotgun/cli/context.py +2 -2
  52. shotgun/cli/run.py +90 -0
  53. shotgun/cli/spec/backup.py +2 -1
  54. shotgun/cli/spec/commands.py +2 -0
  55. shotgun/cli/spec/models.py +18 -0
  56. shotgun/cli/spec/pull_service.py +122 -68
  57. shotgun/codebase/__init__.py +2 -0
  58. shotgun/codebase/benchmarks/__init__.py +35 -0
  59. shotgun/codebase/benchmarks/benchmark_runner.py +309 -0
  60. shotgun/codebase/benchmarks/exporters.py +119 -0
  61. shotgun/codebase/benchmarks/formatters/__init__.py +49 -0
  62. shotgun/codebase/benchmarks/formatters/base.py +34 -0
  63. shotgun/codebase/benchmarks/formatters/json_formatter.py +106 -0
  64. shotgun/codebase/benchmarks/formatters/markdown.py +136 -0
  65. shotgun/codebase/benchmarks/models.py +129 -0
  66. shotgun/codebase/core/__init__.py +4 -0
  67. shotgun/codebase/core/call_resolution.py +91 -0
  68. shotgun/codebase/core/change_detector.py +11 -6
  69. shotgun/codebase/core/errors.py +159 -0
  70. shotgun/codebase/core/extractors/__init__.py +23 -0
  71. shotgun/codebase/core/extractors/base.py +138 -0
  72. shotgun/codebase/core/extractors/factory.py +63 -0
  73. shotgun/codebase/core/extractors/go/__init__.py +7 -0
  74. shotgun/codebase/core/extractors/go/extractor.py +122 -0
  75. shotgun/codebase/core/extractors/javascript/__init__.py +7 -0
  76. shotgun/codebase/core/extractors/javascript/extractor.py +132 -0
  77. shotgun/codebase/core/extractors/protocol.py +109 -0
  78. shotgun/codebase/core/extractors/python/__init__.py +7 -0
  79. shotgun/codebase/core/extractors/python/extractor.py +141 -0
  80. shotgun/codebase/core/extractors/rust/__init__.py +7 -0
  81. shotgun/codebase/core/extractors/rust/extractor.py +139 -0
  82. shotgun/codebase/core/extractors/types.py +15 -0
  83. shotgun/codebase/core/extractors/typescript/__init__.py +7 -0
  84. shotgun/codebase/core/extractors/typescript/extractor.py +92 -0
  85. shotgun/codebase/core/gitignore.py +252 -0
  86. shotgun/codebase/core/ingestor.py +644 -354
  87. shotgun/codebase/core/kuzu_compat.py +119 -0
  88. shotgun/codebase/core/language_config.py +239 -0
  89. shotgun/codebase/core/manager.py +256 -46
  90. shotgun/codebase/core/metrics_collector.py +310 -0
  91. shotgun/codebase/core/metrics_types.py +347 -0
  92. shotgun/codebase/core/parallel_executor.py +424 -0
  93. shotgun/codebase/core/work_distributor.py +254 -0
  94. shotgun/codebase/core/worker.py +768 -0
  95. shotgun/codebase/indexing_state.py +86 -0
  96. shotgun/codebase/models.py +94 -0
  97. shotgun/codebase/service.py +13 -0
  98. shotgun/exceptions.py +1 -1
  99. shotgun/main.py +2 -10
  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 +20 -28
  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 +43 -1
  107. shotgun/prompts/agents/research.j2 +75 -20
  108. shotgun/prompts/agents/router.j2 +713 -0
  109. shotgun/prompts/agents/specify.j2 +94 -4
  110. shotgun/prompts/agents/state/codebase/codebase_graphs_available.j2 +14 -1
  111. shotgun/prompts/agents/state/system_state.j2 +24 -15
  112. shotgun/prompts/agents/tasks.j2 +77 -23
  113. shotgun/settings.py +44 -0
  114. shotgun/shotgun_web/shared_specs/upload_pipeline.py +38 -0
  115. shotgun/tui/app.py +90 -23
  116. shotgun/tui/commands/__init__.py +9 -1
  117. shotgun/tui/components/attachment_bar.py +87 -0
  118. shotgun/tui/components/mode_indicator.py +120 -25
  119. shotgun/tui/components/prompt_input.py +23 -28
  120. shotgun/tui/components/status_bar.py +5 -4
  121. shotgun/tui/dependencies.py +58 -8
  122. shotgun/tui/protocols.py +37 -0
  123. shotgun/tui/screens/chat/chat.tcss +24 -1
  124. shotgun/tui/screens/chat/chat_screen.py +1374 -211
  125. shotgun/tui/screens/chat/codebase_index_prompt_screen.py +8 -4
  126. shotgun/tui/screens/chat_screen/attachment_hint.py +40 -0
  127. shotgun/tui/screens/chat_screen/command_providers.py +0 -97
  128. shotgun/tui/screens/chat_screen/history/agent_response.py +7 -3
  129. shotgun/tui/screens/chat_screen/history/chat_history.py +49 -6
  130. shotgun/tui/screens/chat_screen/history/formatters.py +75 -15
  131. shotgun/tui/screens/chat_screen/history/partial_response.py +11 -1
  132. shotgun/tui/screens/chat_screen/history/user_question.py +25 -3
  133. shotgun/tui/screens/chat_screen/messages.py +219 -0
  134. shotgun/tui/screens/database_locked_dialog.py +219 -0
  135. shotgun/tui/screens/database_timeout_dialog.py +158 -0
  136. shotgun/tui/screens/kuzu_error_dialog.py +135 -0
  137. shotgun/tui/screens/model_picker.py +14 -9
  138. shotgun/tui/screens/models.py +11 -0
  139. shotgun/tui/screens/shotgun_auth.py +50 -0
  140. shotgun/tui/screens/spec_pull.py +2 -0
  141. shotgun/tui/state/processing_state.py +19 -0
  142. shotgun/tui/utils/mode_progress.py +20 -86
  143. shotgun/tui/widgets/__init__.py +2 -1
  144. shotgun/tui/widgets/approval_widget.py +152 -0
  145. shotgun/tui/widgets/cascade_confirmation_widget.py +203 -0
  146. shotgun/tui/widgets/plan_panel.py +129 -0
  147. shotgun/tui/widgets/step_checkpoint_widget.py +180 -0
  148. shotgun/tui/widgets/widget_coordinator.py +18 -0
  149. shotgun/utils/file_system_utils.py +4 -1
  150. {shotgun_sh-0.2.29.dev2.dist-info → shotgun_sh-0.6.1.dev1.dist-info}/METADATA +88 -34
  151. shotgun_sh-0.6.1.dev1.dist-info/RECORD +292 -0
  152. shotgun/cli/export.py +0 -81
  153. shotgun/cli/plan.py +0 -73
  154. shotgun/cli/research.py +0 -93
  155. shotgun/cli/specify.py +0 -70
  156. shotgun/cli/tasks.py +0 -78
  157. shotgun/tui/screens/onboarding.py +0 -580
  158. shotgun_sh-0.2.29.dev2.dist-info/RECORD +0 -229
  159. {shotgun_sh-0.2.29.dev2.dist-info → shotgun_sh-0.6.1.dev1.dist-info}/WHEEL +0 -0
  160. {shotgun_sh-0.2.29.dev2.dist-info → shotgun_sh-0.6.1.dev1.dist-info}/entry_points.txt +0 -0
  161. {shotgun_sh-0.2.29.dev2.dist-info → shotgun_sh-0.6.1.dev1.dist-info}/licenses/LICENSE +0 -0
@@ -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
+ """
@@ -0,0 +1,219 @@
1
+ """Dialog shown when the database is locked by another process."""
2
+
3
+ import webbrowser
4
+
5
+ import pyperclip # type: ignore[import-untyped]
6
+ from textual import on
7
+ from textual.app import ComposeResult
8
+ from textual.containers import Container, Horizontal
9
+ from textual.events import Resize
10
+ from textual.screen import ModalScreen
11
+ from textual.widgets import Button, Label, Static
12
+
13
+ from shotgun.exceptions import SHOTGUN_CONTACT_EMAIL
14
+ from shotgun.posthog_telemetry import track_event
15
+ from shotgun.tui.layout import COMPACT_HEIGHT_THRESHOLD
16
+ from shotgun.tui.screens.confirmation_dialog import ConfirmationDialog
17
+ from shotgun.tui.screens.models import LockedDialogAction
18
+
19
+ # Discord invite link for support
20
+ DISCORD_LINK = "https://discord.gg/5RmY6J2N7s"
21
+
22
+
23
+ class DatabaseLockedDialog(ModalScreen[LockedDialogAction]):
24
+ """Dialog shown when the database is locked by another process.
25
+
26
+ This modal informs the user that the database is locked, which could mean
27
+ another instance is running OR a previous instance shut down unsafely
28
+ without releasing the lock.
29
+
30
+ Returns:
31
+ LockedDialogAction.RETRY if user wants to retry after closing other instances
32
+ LockedDialogAction.DELETE if user wants to delete the locked database
33
+ LockedDialogAction.QUIT if user wants to quit the application
34
+ """
35
+
36
+ DEFAULT_CSS = """
37
+ DatabaseLockedDialog {
38
+ align: center middle;
39
+ background: rgba(0, 0, 0, 0.0);
40
+ }
41
+
42
+ DatabaseLockedDialog > #dialog-container {
43
+ width: 70%;
44
+ max-width: 80;
45
+ height: auto;
46
+ border: wide $warning;
47
+ padding: 1 2;
48
+ layout: vertical;
49
+ background: $surface;
50
+ }
51
+
52
+ #dialog-title {
53
+ text-style: bold;
54
+ color: $warning;
55
+ padding-bottom: 1;
56
+ }
57
+
58
+ #dialog-message {
59
+ padding-bottom: 1;
60
+ color: $text-muted;
61
+ }
62
+
63
+ #support-buttons {
64
+ layout: horizontal;
65
+ height: auto;
66
+ padding-bottom: 1;
67
+ }
68
+
69
+ #support-buttons Button {
70
+ margin-right: 1;
71
+ }
72
+
73
+ #dialog-buttons {
74
+ layout: horizontal;
75
+ align-horizontal: right;
76
+ height: auto;
77
+ }
78
+
79
+ #dialog-buttons Button {
80
+ margin-left: 1;
81
+ }
82
+
83
+ #delete-section {
84
+ layout: horizontal;
85
+ height: auto;
86
+ padding-top: 1;
87
+ border-top: solid $warning-darken-2;
88
+ }
89
+
90
+ #delete-section Static {
91
+ width: 1fr;
92
+ color: $text-muted;
93
+ }
94
+
95
+ #delete-section Button {
96
+ margin-left: 1;
97
+ }
98
+
99
+ /* Compact styles for short terminals */
100
+ #dialog-container.compact {
101
+ padding: 0 2;
102
+ max-height: 98%;
103
+ }
104
+
105
+ #dialog-title.compact {
106
+ padding-bottom: 0;
107
+ }
108
+
109
+ #dialog-message.compact {
110
+ padding-bottom: 0;
111
+ }
112
+ """
113
+
114
+ def compose(self) -> ComposeResult:
115
+ """Compose the dialog widgets."""
116
+ with Container(id="dialog-container"):
117
+ yield Label("Codebase Index Unavailable", id="dialog-title")
118
+ message = (
119
+ "Unable to access the codebase index because it is locked.\n\n"
120
+ "We can't determine if another shotgun instance is currently running "
121
+ "or if a previous instance shut down unsafely without releasing the lock.\n\n"
122
+ "To resolve this:\n"
123
+ "1. Close any other shotgun instances and click Retry\n"
124
+ "2. If no other instance is running, you can delete the index\n\n"
125
+ "Need help? Contact support:"
126
+ )
127
+ yield Static(message, id="dialog-message")
128
+ with Horizontal(id="support-buttons"):
129
+ yield Button(
130
+ f"Copy Support Email [{SHOTGUN_CONTACT_EMAIL}]", id="copy-email"
131
+ )
132
+ yield Button("Open Support Discord", id="open-discord")
133
+ with Container(id="dialog-buttons"):
134
+ yield Button("Retry", id="retry", variant="primary")
135
+ yield Button("Quit", id="cancel")
136
+ with Horizontal(id="delete-section"):
137
+ yield Static("Caution: Only delete if no other instance is running.")
138
+ yield Button("Delete Index", id="delete", variant="error")
139
+
140
+ def on_mount(self) -> None:
141
+ """Set up the dialog after mounting."""
142
+ # Track this event in PostHog
143
+ track_event("database_locked_dialog_shown", {})
144
+
145
+ # Focus retry button - user likely wants to retry after closing other instance
146
+ self.query_one("#retry", Button).focus()
147
+
148
+ # Apply compact layout if starting in a short terminal
149
+ self._apply_compact_layout(self.app.size.height < COMPACT_HEIGHT_THRESHOLD)
150
+
151
+ @on(Resize)
152
+ def handle_resize(self, event: Resize) -> None:
153
+ """Adjust layout based on terminal height."""
154
+ self._apply_compact_layout(event.size.height < COMPACT_HEIGHT_THRESHOLD)
155
+
156
+ def _apply_compact_layout(self, compact: bool) -> None:
157
+ """Apply or remove compact layout classes for short terminals."""
158
+ container = self.query_one("#dialog-container")
159
+ title = self.query_one("#dialog-title")
160
+ message = self.query_one("#dialog-message")
161
+
162
+ if compact:
163
+ container.add_class("compact")
164
+ title.add_class("compact")
165
+ message.add_class("compact")
166
+ else:
167
+ container.remove_class("compact")
168
+ title.remove_class("compact")
169
+ message.remove_class("compact")
170
+
171
+ @on(Button.Pressed, "#cancel")
172
+ def handle_cancel(self, event: Button.Pressed) -> None:
173
+ """Handle cancel button press."""
174
+ event.stop()
175
+ self.dismiss(LockedDialogAction.QUIT)
176
+
177
+ @on(Button.Pressed, "#retry")
178
+ def handle_retry(self, event: Button.Pressed) -> None:
179
+ """Handle retry button press."""
180
+ event.stop()
181
+ self.dismiss(LockedDialogAction.RETRY)
182
+
183
+ @on(Button.Pressed, "#delete")
184
+ async def handle_delete(self, event: Button.Pressed) -> None:
185
+ """Handle delete button press with confirmation."""
186
+ event.stop()
187
+ # Show confirmation dialog before proceeding
188
+ confirmed = await self.app.push_screen_wait(
189
+ ConfirmationDialog(
190
+ title="Delete Codebase Index?",
191
+ message=(
192
+ "Have you checked that no other shotgun instance is running?\n\n"
193
+ "Deleting while another instance is open could cause data loss. "
194
+ "You will need to re-index the codebase after deletion."
195
+ ),
196
+ confirm_label="Delete Index",
197
+ cancel_label="Cancel",
198
+ confirm_variant="error",
199
+ danger=True,
200
+ )
201
+ )
202
+ if confirmed:
203
+ track_event("database_locked_dialog_delete", {})
204
+ self.dismiss(LockedDialogAction.DELETE)
205
+
206
+ @on(Button.Pressed, "#copy-email")
207
+ def handle_copy_email(self, event: Button.Pressed) -> None:
208
+ """Copy support email to clipboard."""
209
+ event.stop()
210
+ pyperclip.copy(SHOTGUN_CONTACT_EMAIL)
211
+ track_event("database_locked_dialog_copy_email", {})
212
+ self.notify("Email copied to clipboard", severity="information")
213
+
214
+ @on(Button.Pressed, "#open-discord")
215
+ def handle_open_discord(self, event: Button.Pressed) -> None:
216
+ """Open Discord link in browser."""
217
+ event.stop()
218
+ webbrowser.open(DISCORD_LINK)
219
+ track_event("database_locked_dialog_open_discord", {})
@@ -0,0 +1,158 @@
1
+ """Dialog shown when database operation times out."""
2
+
3
+ from typing import Literal
4
+
5
+ from textual import on
6
+ from textual.app import ComposeResult
7
+ from textual.containers import Container
8
+ from textual.events import Resize
9
+ from textual.screen import ModalScreen
10
+ from textual.widgets import Button, Label, Static
11
+
12
+ from shotgun.tui.layout import COMPACT_HEIGHT_THRESHOLD
13
+
14
+ TimeoutAction = Literal["retry", "skip", "cancel"]
15
+
16
+
17
+ class DatabaseTimeoutDialog(ModalScreen[TimeoutAction]):
18
+ """Dialog shown when database operation takes longer than expected.
19
+
20
+ This modal informs the user that the database operation is taking longer
21
+ than expected (can happen with large codebases) and offers options to
22
+ wait longer, skip, or cancel.
23
+
24
+ Args:
25
+ codebase_name: Name of the codebase that timed out
26
+ timeout_seconds: The timeout that was exceeded
27
+
28
+ Returns:
29
+ "retry" - Wait longer (90s timeout)
30
+ "skip" - Skip this database and continue
31
+ "cancel" - Cancel the operation
32
+ """
33
+
34
+ DEFAULT_CSS = """
35
+ DatabaseTimeoutDialog {
36
+ align: center middle;
37
+ background: rgba(0, 0, 0, 0.0);
38
+ }
39
+
40
+ DatabaseTimeoutDialog > #dialog-container {
41
+ width: 60%;
42
+ max-width: 70;
43
+ height: auto;
44
+ border: wide $warning;
45
+ padding: 1 2;
46
+ layout: vertical;
47
+ background: $surface;
48
+ }
49
+
50
+ #dialog-title {
51
+ text-style: bold;
52
+ color: $warning;
53
+ padding-bottom: 1;
54
+ }
55
+
56
+ #dialog-message {
57
+ padding-bottom: 1;
58
+ color: $text-muted;
59
+ }
60
+
61
+ #dialog-buttons {
62
+ layout: horizontal;
63
+ align-horizontal: right;
64
+ height: auto;
65
+ }
66
+
67
+ #dialog-buttons Button {
68
+ margin-left: 1;
69
+ }
70
+
71
+ /* Compact styles for short terminals */
72
+ #dialog-container.compact {
73
+ padding: 0 2;
74
+ max-height: 98%;
75
+ }
76
+
77
+ #dialog-title.compact {
78
+ padding-bottom: 0;
79
+ }
80
+
81
+ #dialog-message.compact {
82
+ padding-bottom: 0;
83
+ }
84
+ """
85
+
86
+ def __init__(self, codebase_name: str = "", timeout_seconds: float = 10.0) -> None:
87
+ """Initialize the dialog.
88
+
89
+ Args:
90
+ codebase_name: Name of the codebase that timed out
91
+ timeout_seconds: The timeout that was exceeded
92
+ """
93
+ super().__init__()
94
+ self.codebase_name = codebase_name
95
+ self.timeout_seconds = timeout_seconds
96
+
97
+ def compose(self) -> ComposeResult:
98
+ """Compose the dialog widgets."""
99
+ with Container(id="dialog-container"):
100
+ yield Label("Database Taking Longer Than Expected", id="dialog-title")
101
+ message = (
102
+ f"The database operation exceeded {self.timeout_seconds:.0f} seconds.\n\n"
103
+ "This can happen with large codebases. "
104
+ "Would you like to wait longer (90 seconds)?"
105
+ )
106
+ if self.codebase_name:
107
+ message = f"Codebase: {self.codebase_name}\n\n" + message
108
+ yield Static(message, id="dialog-message")
109
+ with Container(id="dialog-buttons"):
110
+ yield Button("Wait Longer", id="retry", variant="primary")
111
+ yield Button("Skip", id="skip")
112
+ yield Button("Cancel", id="cancel")
113
+
114
+ def on_mount(self) -> None:
115
+ """Set up the dialog after mounting."""
116
+ # Focus "Wait Longer" button - most likely what user wants
117
+ self.query_one("#retry", Button).focus()
118
+
119
+ # Apply compact layout if starting in a short terminal
120
+ self._apply_compact_layout(self.app.size.height < COMPACT_HEIGHT_THRESHOLD)
121
+
122
+ @on(Resize)
123
+ def handle_resize(self, event: Resize) -> None:
124
+ """Adjust layout based on terminal height."""
125
+ self._apply_compact_layout(event.size.height < COMPACT_HEIGHT_THRESHOLD)
126
+
127
+ def _apply_compact_layout(self, compact: bool) -> None:
128
+ """Apply or remove compact layout classes for short terminals."""
129
+ container = self.query_one("#dialog-container")
130
+ title = self.query_one("#dialog-title")
131
+ message = self.query_one("#dialog-message")
132
+
133
+ if compact:
134
+ container.add_class("compact")
135
+ title.add_class("compact")
136
+ message.add_class("compact")
137
+ else:
138
+ container.remove_class("compact")
139
+ title.remove_class("compact")
140
+ message.remove_class("compact")
141
+
142
+ @on(Button.Pressed, "#cancel")
143
+ def handle_cancel(self, event: Button.Pressed) -> None:
144
+ """Handle cancel button press."""
145
+ event.stop()
146
+ self.dismiss("cancel")
147
+
148
+ @on(Button.Pressed, "#skip")
149
+ def handle_skip(self, event: Button.Pressed) -> None:
150
+ """Handle skip button press."""
151
+ event.stop()
152
+ self.dismiss("skip")
153
+
154
+ @on(Button.Pressed, "#retry")
155
+ def handle_retry(self, event: Button.Pressed) -> None:
156
+ """Handle retry button press."""
157
+ event.stop()
158
+ self.dismiss("retry")