shotgun-sh 0.2.23.dev1__py3-none-any.whl → 0.2.29.dev2__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.
- shotgun/agents/agent_manager.py +3 -3
- shotgun/agents/common.py +1 -1
- shotgun/agents/config/manager.py +36 -21
- shotgun/agents/config/models.py +30 -0
- shotgun/agents/config/provider.py +27 -14
- 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 +216 -0
- shotgun/agents/{history → conversation/history}/history_processors.py +267 -3
- shotgun/agents/{conversation_manager.py → conversation/manager.py} +1 -1
- shotgun/agents/{conversation_history.py → conversation/models.py} +8 -94
- shotgun/agents/tools/web_search/openai.py +1 -1
- shotgun/cli/clear.py +1 -1
- shotgun/cli/compact.py +5 -3
- shotgun/cli/context.py +1 -1
- shotgun/cli/spec/__init__.py +5 -0
- shotgun/cli/spec/backup.py +81 -0
- shotgun/cli/spec/commands.py +130 -0
- shotgun/cli/spec/models.py +30 -0
- shotgun/cli/spec/pull_service.py +165 -0
- shotgun/codebase/core/ingestor.py +153 -7
- shotgun/codebase/models.py +2 -0
- shotgun/exceptions.py +5 -3
- shotgun/main.py +2 -0
- shotgun/posthog_telemetry.py +1 -1
- shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +3 -3
- shotgun/prompts/agents/partials/interactive_mode.j2 +3 -3
- shotgun/prompts/agents/research.j2 +0 -3
- shotgun/prompts/history/chunk_summarization.j2 +34 -0
- shotgun/prompts/history/combine_summaries.j2 +53 -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 +291 -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 +39 -0
- shotgun/tui/containers.py +1 -1
- shotgun/tui/layout.py +5 -0
- shotgun/tui/screens/chat/chat_screen.py +212 -16
- shotgun/tui/screens/chat/codebase_index_prompt_screen.py +147 -19
- shotgun/tui/screens/chat_screen/command_providers.py +10 -0
- shotgun/tui/screens/chat_screen/history/chat_history.py +0 -36
- shotgun/tui/screens/confirmation_dialog.py +40 -0
- shotgun/tui/screens/model_picker.py +7 -1
- shotgun/tui/screens/onboarding.py +149 -0
- shotgun/tui/screens/pipx_migration.py +46 -0
- shotgun/tui/screens/provider_config.py +41 -0
- 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 +60 -6
- shotgun/tui/screens/spec_pull.py +286 -0
- shotgun/tui/screens/welcome.py +91 -0
- shotgun/tui/services/conversation_service.py +5 -2
- shotgun/tui/widgets/widget_coordinator.py +1 -1
- {shotgun_sh-0.2.23.dev1.dist-info → shotgun_sh-0.2.29.dev2.dist-info}/METADATA +1 -1
- {shotgun_sh-0.2.23.dev1.dist-info → shotgun_sh-0.2.29.dev2.dist-info}/RECORD +86 -59
- {shotgun_sh-0.2.23.dev1.dist-info → shotgun_sh-0.2.29.dev2.dist-info}/WHEEL +1 -1
- /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/anthropic.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.23.dev1.dist-info → shotgun_sh-0.2.29.dev2.dist-info}/entry_points.txt +0 -0
- {shotgun_sh-0.2.23.dev1.dist-info → shotgun_sh-0.2.29.dev2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
"""Dialog for selecting an existing spec or creating a new one."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
|
|
5
|
+
from textual import on, work
|
|
6
|
+
from textual.app import ComposeResult
|
|
7
|
+
from textual.containers import Container, Horizontal
|
|
8
|
+
from textual.events import Resize
|
|
9
|
+
from textual.screen import ModalScreen
|
|
10
|
+
from textual.widgets import Button, Label, ListItem, ListView, Static
|
|
11
|
+
|
|
12
|
+
from shotgun.logging_config import get_logger
|
|
13
|
+
from shotgun.shotgun_web.exceptions import (
|
|
14
|
+
ForbiddenError,
|
|
15
|
+
NotFoundError,
|
|
16
|
+
UnauthorizedError,
|
|
17
|
+
)
|
|
18
|
+
from shotgun.shotgun_web.models import SpecResponse, WorkspaceNotFoundError
|
|
19
|
+
from shotgun.shotgun_web.specs_client import SpecsClient
|
|
20
|
+
from shotgun.tui.layout import COMPACT_HEIGHT_THRESHOLD
|
|
21
|
+
from shotgun.tui.screens.shared_specs.models import (
|
|
22
|
+
ShareSpecsAction,
|
|
23
|
+
ShareSpecsResult,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
logger = get_logger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _relative_time(dt: datetime | None) -> str:
|
|
30
|
+
"""Convert datetime to relative time string (e.g., '2 days ago')."""
|
|
31
|
+
if dt is None:
|
|
32
|
+
return "unknown"
|
|
33
|
+
now = datetime.now(timezone.utc)
|
|
34
|
+
# Make sure dt is timezone-aware
|
|
35
|
+
if dt.tzinfo is None:
|
|
36
|
+
dt = dt.replace(tzinfo=timezone.utc)
|
|
37
|
+
|
|
38
|
+
diff = now - dt
|
|
39
|
+
seconds = diff.total_seconds()
|
|
40
|
+
|
|
41
|
+
if seconds < 60:
|
|
42
|
+
return "just now"
|
|
43
|
+
elif seconds < 3600:
|
|
44
|
+
minutes = int(seconds / 60)
|
|
45
|
+
return f"{minutes} min{'s' if minutes != 1 else ''} ago"
|
|
46
|
+
elif seconds < 86400:
|
|
47
|
+
hours = int(seconds / 3600)
|
|
48
|
+
return f"{hours} hour{'s' if hours != 1 else ''} ago"
|
|
49
|
+
elif seconds < 604800:
|
|
50
|
+
days = int(seconds / 86400)
|
|
51
|
+
return f"{days} day{'s' if days != 1 else ''} ago"
|
|
52
|
+
elif seconds < 2592000:
|
|
53
|
+
weeks = int(seconds / 604800)
|
|
54
|
+
return f"{weeks} week{'s' if weeks != 1 else ''} ago"
|
|
55
|
+
else:
|
|
56
|
+
months = int(seconds / 2592000)
|
|
57
|
+
return f"{months} month{'s' if months != 1 else ''} ago"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class ShareSpecsDialog(ModalScreen[ShareSpecsResult | None]):
|
|
61
|
+
"""Dialog for selecting an existing spec or creating a new one.
|
|
62
|
+
|
|
63
|
+
Shows a list of existing specs in the workspace with:
|
|
64
|
+
- Name (bold)
|
|
65
|
+
- Latest version label or "No versions"
|
|
66
|
+
- Created date (relative time)
|
|
67
|
+
- Visibility badge (Team/Public)
|
|
68
|
+
|
|
69
|
+
Plus a "Create new spec" option at the top.
|
|
70
|
+
|
|
71
|
+
Returns ShareSpecsResult or None if cancelled.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
DEFAULT_CSS = """
|
|
75
|
+
ShareSpecsDialog {
|
|
76
|
+
align: center middle;
|
|
77
|
+
background: rgba(0, 0, 0, 0.0);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
ShareSpecsDialog > #dialog-container {
|
|
81
|
+
width: 80%;
|
|
82
|
+
max-width: 90;
|
|
83
|
+
height: auto;
|
|
84
|
+
max-height: 80%;
|
|
85
|
+
border: wide $primary;
|
|
86
|
+
padding: 1 2;
|
|
87
|
+
layout: vertical;
|
|
88
|
+
background: $surface;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
#dialog-title {
|
|
92
|
+
text-style: bold;
|
|
93
|
+
color: $text-accent;
|
|
94
|
+
padding-bottom: 1;
|
|
95
|
+
text-align: center;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
#dialog-subtitle {
|
|
99
|
+
color: $text-muted;
|
|
100
|
+
padding-bottom: 1;
|
|
101
|
+
text-align: center;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
#spec-list {
|
|
105
|
+
height: auto;
|
|
106
|
+
max-height: 20;
|
|
107
|
+
border: solid $border;
|
|
108
|
+
padding: 0 1;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
#spec-list > ListItem {
|
|
112
|
+
padding: 1 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
#spec-list > ListItem.--highlight {
|
|
116
|
+
background: $primary-darken-2;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.spec-item-label {
|
|
120
|
+
width: 100%;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.spec-name {
|
|
124
|
+
text-style: bold;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.spec-meta {
|
|
128
|
+
color: $text-muted;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.visibility-badge-public {
|
|
132
|
+
color: $success;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.visibility-badge-team {
|
|
136
|
+
color: $warning;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
#loading-label {
|
|
140
|
+
text-align: center;
|
|
141
|
+
padding: 2;
|
|
142
|
+
color: $text-muted;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
#error-label {
|
|
146
|
+
text-align: center;
|
|
147
|
+
padding: 2;
|
|
148
|
+
color: $error;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
#dialog-buttons {
|
|
152
|
+
layout: horizontal;
|
|
153
|
+
align-horizontal: right;
|
|
154
|
+
height: auto;
|
|
155
|
+
padding-top: 1;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
#dialog-buttons Button {
|
|
159
|
+
margin-left: 1;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/* Compact styles for short terminals */
|
|
163
|
+
ShareSpecsDialog.compact #dialog-container {
|
|
164
|
+
padding: 0 2;
|
|
165
|
+
max-height: 98%;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
ShareSpecsDialog.compact #dialog-title {
|
|
169
|
+
padding-bottom: 0;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
ShareSpecsDialog.compact #dialog-subtitle {
|
|
173
|
+
padding-bottom: 0;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
ShareSpecsDialog.compact #spec-list {
|
|
177
|
+
max-height: 10;
|
|
178
|
+
}
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
BINDINGS = [
|
|
182
|
+
("escape", "cancel", "Cancel"),
|
|
183
|
+
]
|
|
184
|
+
|
|
185
|
+
def __init__(self) -> None:
|
|
186
|
+
"""Initialize the dialog."""
|
|
187
|
+
super().__init__()
|
|
188
|
+
self.workspace_id: str | None = None
|
|
189
|
+
self._specs: list[SpecResponse] = []
|
|
190
|
+
self._loading = True
|
|
191
|
+
self._error: str | None = None
|
|
192
|
+
|
|
193
|
+
def compose(self) -> ComposeResult:
|
|
194
|
+
"""Compose the dialog widgets."""
|
|
195
|
+
with Container(id="dialog-container"):
|
|
196
|
+
yield Label("Share Specs", id="dialog-title")
|
|
197
|
+
yield Static(
|
|
198
|
+
"Select an existing spec to add a new version, or create a new spec.",
|
|
199
|
+
id="dialog-subtitle",
|
|
200
|
+
)
|
|
201
|
+
yield Static("Loading specs...", id="loading-label")
|
|
202
|
+
yield Static("", id="error-label")
|
|
203
|
+
yield ListView(id="spec-list")
|
|
204
|
+
with Horizontal(id="dialog-buttons"):
|
|
205
|
+
yield Button("Cancel", id="cancel")
|
|
206
|
+
|
|
207
|
+
def on_mount(self) -> None:
|
|
208
|
+
"""Load specs when dialog is mounted."""
|
|
209
|
+
# Hide list and error initially
|
|
210
|
+
self.query_one("#spec-list", ListView).display = False
|
|
211
|
+
self.query_one("#error-label", Static).display = False
|
|
212
|
+
|
|
213
|
+
# Apply compact layout if starting in a short terminal
|
|
214
|
+
self._apply_compact_layout(self.app.size.height < COMPACT_HEIGHT_THRESHOLD)
|
|
215
|
+
|
|
216
|
+
# Load specs
|
|
217
|
+
self._load_specs()
|
|
218
|
+
|
|
219
|
+
@on(Resize)
|
|
220
|
+
def handle_resize(self, event: Resize) -> None:
|
|
221
|
+
"""Adjust layout based on terminal height."""
|
|
222
|
+
self._apply_compact_layout(event.size.height < COMPACT_HEIGHT_THRESHOLD)
|
|
223
|
+
|
|
224
|
+
def _apply_compact_layout(self, compact: bool) -> None:
|
|
225
|
+
"""Apply or remove compact layout class for short terminals."""
|
|
226
|
+
if compact:
|
|
227
|
+
self.add_class("compact")
|
|
228
|
+
else:
|
|
229
|
+
self.remove_class("compact")
|
|
230
|
+
|
|
231
|
+
def _update_loading_message(self, message: str) -> None:
|
|
232
|
+
"""Update the loading label message."""
|
|
233
|
+
self.query_one("#loading-label", Static).update(message)
|
|
234
|
+
|
|
235
|
+
@work
|
|
236
|
+
async def _load_specs(self) -> None:
|
|
237
|
+
"""Fetch workspace, check permissions, and load specs."""
|
|
238
|
+
try:
|
|
239
|
+
client = SpecsClient()
|
|
240
|
+
|
|
241
|
+
# Step 1: Get workspace
|
|
242
|
+
self._update_loading_message("Connecting to workspace...")
|
|
243
|
+
try:
|
|
244
|
+
self.workspace_id = await client.get_or_fetch_workspace_id()
|
|
245
|
+
except UnauthorizedError:
|
|
246
|
+
self._loading = False
|
|
247
|
+
self._error = "Not authenticated. Run 'shotgun auth' to login."
|
|
248
|
+
self._show_error()
|
|
249
|
+
return
|
|
250
|
+
except WorkspaceNotFoundError:
|
|
251
|
+
self._loading = False
|
|
252
|
+
self._error = "No workspaces available. Please create one at shotgun.sh"
|
|
253
|
+
self._show_error()
|
|
254
|
+
return
|
|
255
|
+
|
|
256
|
+
# Step 2: Check permissions
|
|
257
|
+
self._update_loading_message("Checking permissions...")
|
|
258
|
+
try:
|
|
259
|
+
permissions = await client.check_permissions(self.workspace_id)
|
|
260
|
+
if not permissions.can_create_specs:
|
|
261
|
+
self._loading = False
|
|
262
|
+
self._error = "You need editor access to share specs"
|
|
263
|
+
self._show_error()
|
|
264
|
+
return
|
|
265
|
+
except NotFoundError:
|
|
266
|
+
# Permissions endpoint not available yet - skip check
|
|
267
|
+
logger.debug("Permissions endpoint not available, skipping check")
|
|
268
|
+
except UnauthorizedError:
|
|
269
|
+
self._loading = False
|
|
270
|
+
self._error = "Not authenticated. Run 'shotgun auth' to login."
|
|
271
|
+
self._show_error()
|
|
272
|
+
return
|
|
273
|
+
except ForbiddenError:
|
|
274
|
+
self._loading = False
|
|
275
|
+
self._error = "You don't have access to this workspace"
|
|
276
|
+
self._show_error()
|
|
277
|
+
return
|
|
278
|
+
|
|
279
|
+
# Step 3: Load specs
|
|
280
|
+
self._update_loading_message("Loading specs...")
|
|
281
|
+
response = await client.list_specs(self.workspace_id)
|
|
282
|
+
self._specs = response.specs
|
|
283
|
+
self._loading = False
|
|
284
|
+
self._populate_list()
|
|
285
|
+
except Exception as e:
|
|
286
|
+
logger.exception(f"Failed to load specs: {type(e).__name__}: {e}")
|
|
287
|
+
self._loading = False
|
|
288
|
+
self._error = str(e) if str(e) else type(e).__name__
|
|
289
|
+
self._show_error()
|
|
290
|
+
|
|
291
|
+
def _populate_list(self) -> None:
|
|
292
|
+
"""Populate the ListView with specs."""
|
|
293
|
+
# Hide loading label
|
|
294
|
+
self.query_one("#loading-label", Static).display = False
|
|
295
|
+
|
|
296
|
+
# Show and populate list
|
|
297
|
+
list_view = self.query_one("#spec-list", ListView)
|
|
298
|
+
list_view.display = True
|
|
299
|
+
|
|
300
|
+
# Clear existing items
|
|
301
|
+
list_view.clear()
|
|
302
|
+
|
|
303
|
+
# Add "Create new spec" option first
|
|
304
|
+
create_label = Label(
|
|
305
|
+
"[bold]+ Create new spec[/]\nStart a fresh spec for your .shotgun/ files",
|
|
306
|
+
classes="spec-item-label",
|
|
307
|
+
)
|
|
308
|
+
list_view.append(ListItem(create_label, id="create-new"))
|
|
309
|
+
|
|
310
|
+
# Add existing specs
|
|
311
|
+
for spec in self._specs:
|
|
312
|
+
label = self._format_spec_label(spec)
|
|
313
|
+
list_view.append(ListItem(label, id=f"spec-{spec.id}"))
|
|
314
|
+
|
|
315
|
+
# Focus the list
|
|
316
|
+
list_view.focus()
|
|
317
|
+
|
|
318
|
+
def _format_spec_label(self, spec: SpecResponse) -> Label:
|
|
319
|
+
"""Format a spec as a label for display."""
|
|
320
|
+
# Name
|
|
321
|
+
name = spec.name
|
|
322
|
+
|
|
323
|
+
# Latest version info
|
|
324
|
+
if spec.latest_version and spec.latest_version.label:
|
|
325
|
+
version_info = spec.latest_version.label
|
|
326
|
+
elif spec.latest_version:
|
|
327
|
+
version_info = "Latest version"
|
|
328
|
+
else:
|
|
329
|
+
version_info = "No versions"
|
|
330
|
+
|
|
331
|
+
# Visibility badge
|
|
332
|
+
if spec.is_public:
|
|
333
|
+
visibility = "[green]Public[/]"
|
|
334
|
+
else:
|
|
335
|
+
visibility = "[yellow]Team[/]"
|
|
336
|
+
|
|
337
|
+
# Relative time
|
|
338
|
+
time_ago = _relative_time(spec.created_on)
|
|
339
|
+
|
|
340
|
+
# Format the label
|
|
341
|
+
text = f"[bold]{name}[/] · {visibility}\n{version_info} · {time_ago}"
|
|
342
|
+
return Label(text, classes="spec-item-label")
|
|
343
|
+
|
|
344
|
+
def _show_error(self) -> None:
|
|
345
|
+
"""Show error message."""
|
|
346
|
+
self.query_one("#loading-label", Static).display = False
|
|
347
|
+
error_label = self.query_one("#error-label", Static)
|
|
348
|
+
error_label.update(f"Failed to load specs: {self._error}")
|
|
349
|
+
error_label.display = True
|
|
350
|
+
|
|
351
|
+
@on(ListView.Selected)
|
|
352
|
+
def _on_spec_selected(self, event: ListView.Selected) -> None:
|
|
353
|
+
"""Handle spec selection."""
|
|
354
|
+
item = event.item
|
|
355
|
+
if item is None or item.id is None:
|
|
356
|
+
return
|
|
357
|
+
|
|
358
|
+
if item.id == "create-new":
|
|
359
|
+
self.dismiss(
|
|
360
|
+
ShareSpecsResult(
|
|
361
|
+
action=ShareSpecsAction.CREATE,
|
|
362
|
+
workspace_id=self.workspace_id,
|
|
363
|
+
)
|
|
364
|
+
)
|
|
365
|
+
elif item.id.startswith("spec-"):
|
|
366
|
+
spec_id = item.id.removeprefix("spec-")
|
|
367
|
+
# Find the spec to get the name
|
|
368
|
+
spec_name = None
|
|
369
|
+
for spec in self._specs:
|
|
370
|
+
if spec.id == spec_id:
|
|
371
|
+
spec_name = spec.name
|
|
372
|
+
break
|
|
373
|
+
self.dismiss(
|
|
374
|
+
ShareSpecsResult(
|
|
375
|
+
action=ShareSpecsAction.ADD_VERSION,
|
|
376
|
+
workspace_id=self.workspace_id,
|
|
377
|
+
spec_id=spec_id,
|
|
378
|
+
spec_name=spec_name,
|
|
379
|
+
)
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
@on(Button.Pressed, "#cancel")
|
|
383
|
+
def _on_cancel(self, event: Button.Pressed) -> None:
|
|
384
|
+
"""Handle cancel button."""
|
|
385
|
+
event.stop()
|
|
386
|
+
self.dismiss(None)
|
|
387
|
+
|
|
388
|
+
def action_cancel(self) -> None:
|
|
389
|
+
"""Handle escape key."""
|
|
390
|
+
self.dismiss(None)
|