shotgun-sh 0.2.17__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 +219 -37
- shotgun/agents/common.py +79 -78
- shotgun/agents/config/README.md +89 -0
- shotgun/agents/config/__init__.py +10 -1
- shotgun/agents/config/manager.py +364 -53
- shotgun/agents/config/models.py +101 -21
- shotgun/agents/config/provider.py +51 -13
- shotgun/agents/config/streaming_test.py +119 -0
- shotgun/agents/context_analyzer/analyzer.py +6 -2
- shotgun/agents/conversation/__init__.py +18 -0
- shotgun/agents/conversation/filters.py +164 -0
- shotgun/agents/conversation/history/chunking.py +278 -0
- shotgun/agents/{history → conversation/history}/compaction.py +27 -1
- shotgun/agents/{history → conversation/history}/constants.py +5 -0
- shotgun/agents/conversation/history/file_content_deduplication.py +239 -0
- shotgun/agents/{history → conversation/history}/history_processors.py +267 -3
- shotgun/agents/{history → conversation/history}/token_counting/anthropic.py +8 -0
- shotgun/agents/{conversation_manager.py → conversation/manager.py} +1 -1
- shotgun/agents/{conversation_history.py → conversation/models.py} +8 -94
- shotgun/agents/error/__init__.py +11 -0
- shotgun/agents/error/models.py +19 -0
- 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/runner.py +230 -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/agents/tools/web_search/openai.py +1 -1
- shotgun/build_constants.py +2 -2
- shotgun/cli/clear.py +1 -1
- shotgun/cli/compact.py +5 -3
- shotgun/cli/context.py +44 -1
- shotgun/cli/error_handler.py +24 -0
- shotgun/cli/export.py +34 -34
- shotgun/cli/plan.py +34 -34
- shotgun/cli/research.py +17 -9
- shotgun/cli/spec/__init__.py +5 -0
- shotgun/cli/spec/backup.py +81 -0
- shotgun/cli/spec/commands.py +132 -0
- shotgun/cli/spec/models.py +48 -0
- shotgun/cli/spec/pull_service.py +219 -0
- shotgun/cli/specify.py +20 -19
- shotgun/cli/tasks.py +34 -34
- shotgun/codebase/core/change_detector.py +1 -1
- shotgun/codebase/core/ingestor.py +154 -8
- shotgun/codebase/core/manager.py +1 -1
- shotgun/codebase/models.py +2 -0
- shotgun/exceptions.py +325 -0
- shotgun/llm_proxy/__init__.py +17 -0
- shotgun/llm_proxy/client.py +215 -0
- shotgun/llm_proxy/models.py +137 -0
- shotgun/logging_config.py +42 -0
- shotgun/main.py +4 -0
- shotgun/posthog_telemetry.py +1 -1
- shotgun/prompts/agents/export.j2 +2 -0
- shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +23 -3
- shotgun/prompts/agents/partials/interactive_mode.j2 +3 -3
- shotgun/prompts/agents/partials/router_delegation_mode.j2 +36 -0
- shotgun/prompts/agents/plan.j2 +29 -1
- shotgun/prompts/agents/research.j2 +75 -23
- shotgun/prompts/agents/router.j2 +440 -0
- shotgun/prompts/agents/specify.j2 +80 -4
- shotgun/prompts/agents/state/system_state.j2 +15 -8
- shotgun/prompts/agents/tasks.j2 +63 -23
- shotgun/prompts/history/chunk_summarization.j2 +34 -0
- shotgun/prompts/history/combine_summaries.j2 +53 -0
- shotgun/sdk/codebase.py +14 -3
- shotgun/settings.py +5 -0
- shotgun/shotgun_web/__init__.py +67 -1
- shotgun/shotgun_web/client.py +42 -1
- shotgun/shotgun_web/constants.py +46 -0
- shotgun/shotgun_web/exceptions.py +29 -0
- shotgun/shotgun_web/models.py +390 -0
- shotgun/shotgun_web/shared_specs/__init__.py +32 -0
- shotgun/shotgun_web/shared_specs/file_scanner.py +175 -0
- shotgun/shotgun_web/shared_specs/hasher.py +83 -0
- shotgun/shotgun_web/shared_specs/models.py +71 -0
- shotgun/shotgun_web/shared_specs/upload_pipeline.py +329 -0
- shotgun/shotgun_web/shared_specs/utils.py +34 -0
- shotgun/shotgun_web/specs_client.py +703 -0
- shotgun/shotgun_web/supabase_client.py +31 -0
- shotgun/tui/app.py +78 -15
- shotgun/tui/components/mode_indicator.py +120 -25
- shotgun/tui/components/status_bar.py +2 -2
- shotgun/tui/containers.py +1 -1
- shotgun/tui/dependencies.py +64 -9
- shotgun/tui/layout.py +5 -0
- shotgun/tui/protocols.py +37 -0
- shotgun/tui/screens/chat/chat.tcss +9 -1
- shotgun/tui/screens/chat/chat_screen.py +1015 -106
- shotgun/tui/screens/chat/codebase_index_prompt_screen.py +196 -17
- shotgun/tui/screens/chat_screen/command_providers.py +13 -89
- shotgun/tui/screens/chat_screen/hint_message.py +76 -1
- 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/confirmation_dialog.py +40 -0
- shotgun/tui/screens/directory_setup.py +45 -41
- shotgun/tui/screens/feedback.py +10 -3
- shotgun/tui/screens/github_issue.py +11 -2
- shotgun/tui/screens/model_picker.py +28 -8
- shotgun/tui/screens/onboarding.py +179 -26
- shotgun/tui/screens/pipx_migration.py +58 -6
- shotgun/tui/screens/provider_config.py +66 -8
- shotgun/tui/screens/shared_specs/__init__.py +21 -0
- shotgun/tui/screens/shared_specs/create_spec_dialog.py +273 -0
- shotgun/tui/screens/shared_specs/models.py +56 -0
- shotgun/tui/screens/shared_specs/share_specs_dialog.py +390 -0
- shotgun/tui/screens/shared_specs/upload_progress_screen.py +452 -0
- shotgun/tui/screens/shotgun_auth.py +110 -16
- shotgun/tui/screens/spec_pull.py +288 -0
- shotgun/tui/screens/welcome.py +123 -0
- shotgun/tui/services/conversation_service.py +5 -2
- 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 +1 -1
- {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.4.0.dev1.dist-info}/METADATA +11 -4
- shotgun_sh-0.4.0.dev1.dist-info/RECORD +242 -0
- {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.4.0.dev1.dist-info}/WHEEL +1 -1
- shotgun_sh-0.2.17.dist-info/RECORD +0 -194
- /shotgun/agents/{history → conversation/history}/__init__.py +0 -0
- /shotgun/agents/{history → conversation/history}/context_extraction.py +0 -0
- /shotgun/agents/{history → conversation/history}/history_building.py +0 -0
- /shotgun/agents/{history → conversation/history}/message_utils.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_counting/__init__.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_counting/base.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_counting/openai.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_counting/sentencepiece_counter.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_counting/tokenizer_cache.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_counting/utils.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_estimation.py +0 -0
- {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.4.0.dev1.dist-info}/entry_points.txt +0 -0
- {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.4.0.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
|
+
"""
|
|
@@ -5,9 +5,12 @@ from typing import Literal
|
|
|
5
5
|
from textual import on
|
|
6
6
|
from textual.app import ComposeResult
|
|
7
7
|
from textual.containers import Container
|
|
8
|
+
from textual.events import Resize
|
|
8
9
|
from textual.screen import ModalScreen
|
|
9
10
|
from textual.widgets import Button, Label, Static
|
|
10
11
|
|
|
12
|
+
from shotgun.tui.layout import COMPACT_HEIGHT_THRESHOLD
|
|
13
|
+
|
|
11
14
|
ButtonVariant = Literal["default", "primary", "success", "warning", "error"]
|
|
12
15
|
|
|
13
16
|
|
|
@@ -87,6 +90,20 @@ class ConfirmationDialog(ModalScreen[bool]):
|
|
|
87
90
|
#dialog-buttons Button {
|
|
88
91
|
margin-left: 1;
|
|
89
92
|
}
|
|
93
|
+
|
|
94
|
+
/* Compact styles for short terminals */
|
|
95
|
+
#dialog-container.compact {
|
|
96
|
+
padding: 0 2;
|
|
97
|
+
max-height: 98%;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
#dialog-title.compact {
|
|
101
|
+
padding-bottom: 0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
#dialog-message.compact {
|
|
105
|
+
padding-bottom: 0;
|
|
106
|
+
}
|
|
90
107
|
"""
|
|
91
108
|
|
|
92
109
|
def __init__(
|
|
@@ -138,6 +155,29 @@ class ConfirmationDialog(ModalScreen[bool]):
|
|
|
138
155
|
# Focus cancel button by default for safety
|
|
139
156
|
self.query_one("#cancel", Button).focus()
|
|
140
157
|
|
|
158
|
+
# Apply compact layout if starting in a short terminal
|
|
159
|
+
self._apply_compact_layout(self.app.size.height < COMPACT_HEIGHT_THRESHOLD)
|
|
160
|
+
|
|
161
|
+
@on(Resize)
|
|
162
|
+
def handle_resize(self, event: Resize) -> None:
|
|
163
|
+
"""Adjust layout based on terminal height."""
|
|
164
|
+
self._apply_compact_layout(event.size.height < COMPACT_HEIGHT_THRESHOLD)
|
|
165
|
+
|
|
166
|
+
def _apply_compact_layout(self, compact: bool) -> None:
|
|
167
|
+
"""Apply or remove compact layout classes for short terminals."""
|
|
168
|
+
container = self.query_one("#dialog-container")
|
|
169
|
+
title = self.query_one("#dialog-title")
|
|
170
|
+
message = self.query_one("#dialog-message")
|
|
171
|
+
|
|
172
|
+
if compact:
|
|
173
|
+
container.add_class("compact")
|
|
174
|
+
title.add_class("compact")
|
|
175
|
+
message.add_class("compact")
|
|
176
|
+
else:
|
|
177
|
+
container.remove_class("compact")
|
|
178
|
+
title.remove_class("compact")
|
|
179
|
+
message.remove_class("compact")
|
|
180
|
+
|
|
141
181
|
@on(Button.Pressed, "#cancel")
|
|
142
182
|
def handle_cancel(self, event: Button.Pressed) -> None:
|
|
143
183
|
"""Handle cancel button press."""
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Screen for
|
|
1
|
+
"""Screen for displaying .shotgun directory creation errors."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -8,13 +8,20 @@ from textual import on
|
|
|
8
8
|
from textual.app import ComposeResult
|
|
9
9
|
from textual.containers import Horizontal, Vertical
|
|
10
10
|
from textual.screen import Screen
|
|
11
|
-
from textual.widgets import Button, Static
|
|
12
|
-
|
|
13
|
-
from shotgun.utils.file_system_utils import ensure_shotgun_directory_exists
|
|
11
|
+
from textual.widgets import Button, Label, Static
|
|
14
12
|
|
|
15
13
|
|
|
16
14
|
class DirectorySetupScreen(Screen[None]):
|
|
17
|
-
"""
|
|
15
|
+
"""Display an error when .shotgun directory creation fails."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, error_message: str) -> None:
|
|
18
|
+
"""Initialize the error screen.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
error_message: The error message to display to the user.
|
|
22
|
+
"""
|
|
23
|
+
super().__init__()
|
|
24
|
+
self.error_message = error_message
|
|
18
25
|
|
|
19
26
|
CSS = """
|
|
20
27
|
DirectorySetupScreen {
|
|
@@ -56,58 +63,55 @@ class DirectorySetupScreen(Screen[None]):
|
|
|
56
63
|
#directory-actions > * {
|
|
57
64
|
margin-right: 2;
|
|
58
65
|
}
|
|
66
|
+
|
|
67
|
+
#directory-status {
|
|
68
|
+
height: auto;
|
|
69
|
+
padding: 0 1;
|
|
70
|
+
min-height: 1;
|
|
71
|
+
color: $error;
|
|
72
|
+
text-align: center;
|
|
73
|
+
}
|
|
59
74
|
"""
|
|
60
75
|
|
|
61
76
|
BINDINGS = [
|
|
62
|
-
("enter", "
|
|
77
|
+
("enter", "retry", "Retry"),
|
|
63
78
|
("escape", "cancel", "Exit"),
|
|
64
79
|
]
|
|
65
80
|
|
|
66
81
|
def compose(self) -> ComposeResult:
|
|
67
82
|
with Vertical(id="titlebox"):
|
|
68
|
-
yield Static(
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
yield Static(
|
|
72
|
-
|
|
73
|
-
yield
|
|
74
|
-
|
|
83
|
+
yield Static(
|
|
84
|
+
"Failed to create .shotgun directory", id="directory-setup-title"
|
|
85
|
+
)
|
|
86
|
+
yield Static("Shotgun was unable to create the .shotgun directory in:\n")
|
|
87
|
+
yield Static(f"[$foreground-muted]({Path.cwd().resolve()})[/]\n")
|
|
88
|
+
yield Static(f"[bold red]Error:[/] {self.error_message}\n")
|
|
89
|
+
yield Static(
|
|
90
|
+
"This directory is required for storing workspace data. "
|
|
91
|
+
"Please check permissions and try again."
|
|
75
92
|
)
|
|
76
|
-
|
|
93
|
+
yield Label("", id="directory-status")
|
|
94
|
+
with Horizontal(id="directory-actions"):
|
|
95
|
+
yield Button("Retry \\[ENTER]", variant="primary", id="retry")
|
|
96
|
+
yield Button("Exit \\[ESC]", variant="default", id="exit")
|
|
77
97
|
|
|
78
98
|
def on_mount(self) -> None:
|
|
79
|
-
self.set_focus(self.query_one("#
|
|
99
|
+
self.set_focus(self.query_one("#retry", Button))
|
|
80
100
|
|
|
81
|
-
def
|
|
82
|
-
|
|
101
|
+
def action_retry(self) -> None:
|
|
102
|
+
"""Retry by dismissing the screen, which will trigger refresh_startup_screen."""
|
|
103
|
+
self.dismiss()
|
|
83
104
|
|
|
84
105
|
def action_cancel(self) -> None:
|
|
85
|
-
|
|
106
|
+
"""Exit the application."""
|
|
107
|
+
self.app.exit()
|
|
86
108
|
|
|
87
|
-
@on(Button.Pressed, "#
|
|
88
|
-
def
|
|
89
|
-
|
|
109
|
+
@on(Button.Pressed, "#retry")
|
|
110
|
+
def _on_retry_pressed(self) -> None:
|
|
111
|
+
"""Retry by dismissing the screen."""
|
|
112
|
+
self.dismiss()
|
|
90
113
|
|
|
91
114
|
@on(Button.Pressed, "#exit")
|
|
92
115
|
def _on_exit_pressed(self) -> None:
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def _initialize_directory(self) -> None:
|
|
96
|
-
try:
|
|
97
|
-
path = ensure_shotgun_directory_exists()
|
|
98
|
-
except Exception as exc: # pragma: no cover - defensive; textual path
|
|
99
|
-
self.notify(f"Failed to initialize directory: {exc}", severity="error")
|
|
100
|
-
return
|
|
101
|
-
|
|
102
|
-
# Double-check a directory now exists; guard against unexpected filesystem state.
|
|
103
|
-
if not path.is_dir():
|
|
104
|
-
self.notify(
|
|
105
|
-
"Unable to initialize .shotgun directory due to filesystem conflict.",
|
|
106
|
-
severity="error",
|
|
107
|
-
)
|
|
108
|
-
return
|
|
109
|
-
|
|
110
|
-
self.dismiss()
|
|
111
|
-
|
|
112
|
-
def _exit_application(self) -> None:
|
|
116
|
+
"""Exit the application."""
|
|
113
117
|
self.app.exit()
|
shotgun/tui/screens/feedback.py
CHANGED
|
@@ -76,6 +76,13 @@ class FeedbackScreen(Screen[Feedback | None]):
|
|
|
76
76
|
#feedback-type-list {
|
|
77
77
|
padding: 1;
|
|
78
78
|
}
|
|
79
|
+
|
|
80
|
+
#feedback-status {
|
|
81
|
+
height: auto;
|
|
82
|
+
padding: 0 1;
|
|
83
|
+
min-height: 1;
|
|
84
|
+
color: $error;
|
|
85
|
+
}
|
|
79
86
|
"""
|
|
80
87
|
|
|
81
88
|
BINDINGS = [
|
|
@@ -96,6 +103,7 @@ class FeedbackScreen(Screen[Feedback | None]):
|
|
|
96
103
|
"",
|
|
97
104
|
id="feedback-description",
|
|
98
105
|
)
|
|
106
|
+
yield Label("", id="feedback-status")
|
|
99
107
|
with Horizontal(id="feedback-actions"):
|
|
100
108
|
yield Button("Submit", variant="primary", id="submit")
|
|
101
109
|
yield Button("Cancel \\[ESC]", id="cancel")
|
|
@@ -176,9 +184,8 @@ class FeedbackScreen(Screen[Feedback | None]):
|
|
|
176
184
|
description = text_area.text.strip()
|
|
177
185
|
|
|
178
186
|
if not description:
|
|
179
|
-
self.
|
|
180
|
-
|
|
181
|
-
)
|
|
187
|
+
status_label = self.query_one("#feedback-status", Label)
|
|
188
|
+
status_label.update("❌ Please enter a description before submitting.")
|
|
182
189
|
return
|
|
183
190
|
|
|
184
191
|
app = cast("ShotgunApp", self.app)
|
|
@@ -6,7 +6,7 @@ from textual import on
|
|
|
6
6
|
from textual.app import ComposeResult
|
|
7
7
|
from textual.containers import Container, Vertical
|
|
8
8
|
from textual.screen import ModalScreen
|
|
9
|
-
from textual.widgets import Button, Markdown, Static
|
|
9
|
+
from textual.widgets import Button, Label, Markdown, Static
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class GitHubIssueScreen(ModalScreen[None]):
|
|
@@ -47,6 +47,13 @@ class GitHubIssueScreen(ModalScreen[None]):
|
|
|
47
47
|
margin: 1 1;
|
|
48
48
|
min-width: 20;
|
|
49
49
|
}
|
|
50
|
+
|
|
51
|
+
#issue-status {
|
|
52
|
+
height: auto;
|
|
53
|
+
padding: 1;
|
|
54
|
+
min-height: 1;
|
|
55
|
+
text-align: center;
|
|
56
|
+
}
|
|
50
57
|
"""
|
|
51
58
|
|
|
52
59
|
BINDINGS = [
|
|
@@ -85,6 +92,7 @@ We review all issues and will respond as soon as possible!
|
|
|
85
92
|
id="issue-markdown",
|
|
86
93
|
)
|
|
87
94
|
with Vertical(id="issue-buttons"):
|
|
95
|
+
yield Label("", id="issue-status")
|
|
88
96
|
yield Button(
|
|
89
97
|
"🐙 Open GitHub Issues", id="github-button", variant="primary"
|
|
90
98
|
)
|
|
@@ -94,7 +102,8 @@ We review all issues and will respond as soon as possible!
|
|
|
94
102
|
def handle_github(self) -> None:
|
|
95
103
|
"""Open GitHub issues page in browser."""
|
|
96
104
|
webbrowser.open("https://github.com/shotgun-sh/shotgun/issues")
|
|
97
|
-
self.
|
|
105
|
+
status_label = self.query_one("#issue-status", Label)
|
|
106
|
+
status_label.update("✓ Opening GitHub Issues in your browser...")
|
|
98
107
|
|
|
99
108
|
@on(Button.Pressed, "#close-button")
|
|
100
109
|
def handle_close(self) -> None:
|
|
@@ -72,6 +72,11 @@ class ModelPickerScreen(Screen[ModelConfigUpdated | None]):
|
|
|
72
72
|
padding: 1 0;
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
|
+
#model-picker-status {
|
|
76
|
+
height: auto;
|
|
77
|
+
padding: 0 1;
|
|
78
|
+
color: $error;
|
|
79
|
+
}
|
|
75
80
|
#model-actions {
|
|
76
81
|
padding: 1;
|
|
77
82
|
}
|
|
@@ -84,7 +89,7 @@ class ModelPickerScreen(Screen[ModelConfigUpdated | None]):
|
|
|
84
89
|
("escape", "done", "Back"),
|
|
85
90
|
]
|
|
86
91
|
|
|
87
|
-
selected_model: reactive[ModelName] = reactive(ModelName.
|
|
92
|
+
selected_model: reactive[ModelName] = reactive(ModelName.GPT_5_1)
|
|
88
93
|
|
|
89
94
|
def compose(self) -> ComposeResult:
|
|
90
95
|
with Vertical(id="titlebox"):
|
|
@@ -94,6 +99,7 @@ class ModelPickerScreen(Screen[ModelConfigUpdated | None]):
|
|
|
94
99
|
id="model-picker-summary",
|
|
95
100
|
)
|
|
96
101
|
yield ListView(id="model-list")
|
|
102
|
+
yield Label("", id="model-picker-status")
|
|
97
103
|
with Horizontal(id="model-actions"):
|
|
98
104
|
yield Button("Select \\[ENTER]", variant="primary", id="select")
|
|
99
105
|
yield Button("Done \\[ESC]", id="done")
|
|
@@ -287,6 +293,12 @@ class ModelPickerScreen(Screen[ModelConfigUpdated | None]):
|
|
|
287
293
|
)
|
|
288
294
|
return has_key
|
|
289
295
|
|
|
296
|
+
def _format_tokens(self, tokens: int) -> str:
|
|
297
|
+
"""Format token count for display (K for thousands, M for millions)."""
|
|
298
|
+
if tokens >= 1_000_000:
|
|
299
|
+
return f"{tokens / 1_000_000:.1f}M"
|
|
300
|
+
return f"{tokens // 1000}K"
|
|
301
|
+
|
|
290
302
|
def _model_label(self, model_name: ModelName, is_current: bool) -> str:
|
|
291
303
|
"""Generate label for model with specs and current indicator."""
|
|
292
304
|
if model_name not in MODEL_SPECS:
|
|
@@ -296,13 +308,13 @@ class ModelPickerScreen(Screen[ModelConfigUpdated | None]):
|
|
|
296
308
|
display_name = self._model_display_name(model_name)
|
|
297
309
|
|
|
298
310
|
# Format context/output tokens in readable format
|
|
299
|
-
|
|
300
|
-
|
|
311
|
+
input_fmt = self._format_tokens(spec.max_input_tokens)
|
|
312
|
+
output_fmt = self._format_tokens(spec.max_output_tokens)
|
|
301
313
|
|
|
302
|
-
label = f"{display_name} · {
|
|
314
|
+
label = f"{display_name} · {input_fmt} context · {output_fmt} output"
|
|
303
315
|
|
|
304
316
|
# Add cost indicator for expensive models
|
|
305
|
-
if model_name == ModelName.
|
|
317
|
+
if model_name == ModelName.CLAUDE_OPUS_4_5:
|
|
306
318
|
label += " · Expensive"
|
|
307
319
|
|
|
308
320
|
if is_current:
|
|
@@ -313,10 +325,17 @@ class ModelPickerScreen(Screen[ModelConfigUpdated | None]):
|
|
|
313
325
|
def _model_display_name(self, model_name: ModelName) -> str:
|
|
314
326
|
"""Get human-readable model name."""
|
|
315
327
|
names = {
|
|
316
|
-
ModelName.
|
|
317
|
-
ModelName.
|
|
328
|
+
ModelName.GPT_5_1: "GPT-5.1 (OpenAI)",
|
|
329
|
+
ModelName.GPT_5_1_CODEX: "GPT-5.1 Codex (OpenAI)",
|
|
330
|
+
ModelName.GPT_5_1_CODEX_MINI: "GPT-5.1 Codex Mini (OpenAI)",
|
|
331
|
+
ModelName.CLAUDE_OPUS_4_5: "Claude Opus 4.5 (Anthropic)",
|
|
332
|
+
ModelName.CLAUDE_SONNET_4: "Claude Sonnet 4 (Anthropic)",
|
|
318
333
|
ModelName.CLAUDE_SONNET_4_5: "Claude Sonnet 4.5 (Anthropic)",
|
|
334
|
+
ModelName.CLAUDE_HAIKU_4_5: "Claude Haiku 4.5 (Anthropic)",
|
|
319
335
|
ModelName.GEMINI_2_5_PRO: "Gemini 2.5 Pro (Google)",
|
|
336
|
+
ModelName.GEMINI_2_5_FLASH: "Gemini 2.5 Flash (Google)",
|
|
337
|
+
ModelName.GEMINI_2_5_FLASH_LITE: "Gemini 2.5 Flash Lite (Google)",
|
|
338
|
+
ModelName.GEMINI_3_PRO_PREVIEW: "Gemini 3 Pro Preview (Google)",
|
|
320
339
|
}
|
|
321
340
|
return names.get(model_name, model_name.value)
|
|
322
341
|
|
|
@@ -349,4 +368,5 @@ class ModelPickerScreen(Screen[ModelConfigUpdated | None]):
|
|
|
349
368
|
)
|
|
350
369
|
)
|
|
351
370
|
except Exception as exc: # pragma: no cover - defensive; textual path
|
|
352
|
-
self.
|
|
371
|
+
status_label = self.query_one("#model-picker-status", Label)
|
|
372
|
+
status_label.update(f"❌ Failed to select model: {exc}")
|