siftcode 0.3.0__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.
- siftcode/__init__.py +3 -0
- siftcode/__main__.py +11 -0
- siftcode/_version.py +59 -0
- siftcode/cli.py +531 -0
- siftcode/core/__init__.py +1 -0
- siftcode/core/analyzer.py +575 -0
- siftcode/core/cache.py +300 -0
- siftcode/core/config.py +258 -0
- siftcode/core/llm.py +295 -0
- siftcode/core/manual.py +208 -0
- siftcode/core/prompts.py +215 -0
- siftcode/data/__init__.py +0 -0
- siftcode/data/siftcode-logo.png +0 -0
- siftcode/gui/__init__.py +9 -0
- siftcode/gui/analyzer_view.py +560 -0
- siftcode/gui/app.py +252 -0
- siftcode/gui/guide_window.py +88 -0
- siftcode/gui/project_list.py +160 -0
- siftcode/gui/settings_view.py +430 -0
- siftcode/gui/widgets/__init__.py +1 -0
- siftcode/gui/widgets/exclusion_list.py +169 -0
- siftcode/gui/widgets/log_view.py +108 -0
- siftcode/gui/widgets/markdown_view.py +205 -0
- siftcode/gui/widgets/project_row.py +141 -0
- siftcode/gui/window.py +446 -0
- siftcode-0.3.0.dist-info/METADATA +413 -0
- siftcode-0.3.0.dist-info/RECORD +31 -0
- siftcode-0.3.0.dist-info/WHEEL +5 -0
- siftcode-0.3.0.dist-info/entry_points.txt +5 -0
- siftcode-0.3.0.dist-info/licenses/LICENSE +674 -0
- siftcode-0.3.0.dist-info/top_level.txt +1 -0
siftcode/gui/window.py
ADDED
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Main application window for Siftcode.
|
|
3
|
+
|
|
4
|
+
Phase 4: Welcome / Projects / Settings / Analyzer views via a Gtk.Stack.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import gi
|
|
13
|
+
|
|
14
|
+
gi.require_version("Gtk", "4.0")
|
|
15
|
+
gi.require_version("Adw", "1")
|
|
16
|
+
|
|
17
|
+
from gi.repository import Adw, Gio, Gtk # noqa: E402
|
|
18
|
+
|
|
19
|
+
from siftcode.core.config import ( # noqa: E402
|
|
20
|
+
PROJECTS_DIR,
|
|
21
|
+
ProjectConfig,
|
|
22
|
+
delete_project,
|
|
23
|
+
list_projects,
|
|
24
|
+
save_project,
|
|
25
|
+
)
|
|
26
|
+
from siftcode.gui.analyzer_view import AnalyzerView # noqa: E402
|
|
27
|
+
from siftcode.gui.project_list import ProjectListView # noqa: E402
|
|
28
|
+
from siftcode.gui.settings_view import SettingsView # noqa: E402
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class MainWindow(Adw.ApplicationWindow):
|
|
35
|
+
"""The main window of Siftcode."""
|
|
36
|
+
|
|
37
|
+
def __init__(self, **kwargs) -> None:
|
|
38
|
+
super().__init__(**kwargs)
|
|
39
|
+
|
|
40
|
+
self.set_title("Siftcode")
|
|
41
|
+
self.set_default_size(900, 640)
|
|
42
|
+
self.set_size_request(600, 400)
|
|
43
|
+
|
|
44
|
+
# Active row reference for context menu actions
|
|
45
|
+
self._active_row = None
|
|
46
|
+
|
|
47
|
+
# --- Window-level actions for the row context menu ------------
|
|
48
|
+
action_group = Gio.SimpleActionGroup()
|
|
49
|
+
|
|
50
|
+
edit_row_action = Gio.SimpleAction.new("edit-row", None)
|
|
51
|
+
edit_row_action.connect("activate", self._on_edit_row_action)
|
|
52
|
+
action_group.add_action(edit_row_action)
|
|
53
|
+
|
|
54
|
+
open_guide_action = Gio.SimpleAction.new("open-latest-guide", None)
|
|
55
|
+
open_guide_action.connect("activate", self._on_open_latest_guide)
|
|
56
|
+
action_group.add_action(open_guide_action)
|
|
57
|
+
|
|
58
|
+
open_folder_action = Gio.SimpleAction.new("open-output-folder", None)
|
|
59
|
+
open_folder_action.connect("activate", self._on_open_output_folder)
|
|
60
|
+
action_group.add_action(open_folder_action)
|
|
61
|
+
|
|
62
|
+
delete_row_action = Gio.SimpleAction.new("delete-row", None)
|
|
63
|
+
delete_row_action.connect("activate", self._on_delete_row_action)
|
|
64
|
+
action_group.add_action(delete_row_action)
|
|
65
|
+
|
|
66
|
+
self.insert_action_group("window", action_group)
|
|
67
|
+
|
|
68
|
+
# Root layout
|
|
69
|
+
toolbar_view = Adw.ToolbarView()
|
|
70
|
+
|
|
71
|
+
# Main header bar (shown on Welcome / Projects views)
|
|
72
|
+
self.main_header = Adw.HeaderBar()
|
|
73
|
+
self.main_header.set_title_widget(
|
|
74
|
+
Adw.WindowTitle(title="Siftcode", subtitle="")
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
self.add_button = Gtk.Button.new_from_icon_name("list-add-symbolic")
|
|
78
|
+
self.add_button.set_tooltip_text("Add a project")
|
|
79
|
+
self.add_button.connect("clicked", self._on_add_clicked)
|
|
80
|
+
self.main_header.pack_start(self.add_button)
|
|
81
|
+
|
|
82
|
+
menu_button = Gtk.MenuButton()
|
|
83
|
+
menu_button.set_icon_name("open-menu-symbolic")
|
|
84
|
+
menu_button.set_tooltip_text("Main menu")
|
|
85
|
+
|
|
86
|
+
menu = Gio.Menu()
|
|
87
|
+
menu.append("About Siftcode", "app.about")
|
|
88
|
+
menu.append("Quit", "app.quit")
|
|
89
|
+
menu_button.set_menu_model(menu)
|
|
90
|
+
|
|
91
|
+
self.main_header.pack_end(menu_button)
|
|
92
|
+
|
|
93
|
+
toolbar_view.add_top_bar(self.main_header)
|
|
94
|
+
|
|
95
|
+
# Content: stack of views
|
|
96
|
+
self.stack = Gtk.Stack()
|
|
97
|
+
self.stack.set_transition_type(Gtk.StackTransitionType.CROSSFADE)
|
|
98
|
+
|
|
99
|
+
# Welcome
|
|
100
|
+
self.stack.add_named(self._build_welcome_view(), "welcome")
|
|
101
|
+
|
|
102
|
+
# Project list
|
|
103
|
+
self.project_list = ProjectListView()
|
|
104
|
+
self.project_list.set_on_project_activated(self._on_project_activated)
|
|
105
|
+
self.stack.add_named(self.project_list, "projects")
|
|
106
|
+
|
|
107
|
+
# Analyzer view (created before settings, since settings hands off to it)
|
|
108
|
+
self.analyzer_view = AnalyzerView()
|
|
109
|
+
self.analyzer_view.set_on_back(self._on_analyzer_back)
|
|
110
|
+
self.stack.add_named(self.analyzer_view, "analyzer")
|
|
111
|
+
|
|
112
|
+
# Settings view
|
|
113
|
+
self.settings_view = SettingsView()
|
|
114
|
+
self.settings_view.set_on_saved(self._on_settings_saved)
|
|
115
|
+
self.settings_view.set_on_cancelled(self._on_settings_cancelled)
|
|
116
|
+
self.settings_view.set_on_deleted(self._on_settings_deleted)
|
|
117
|
+
self.settings_view.set_on_analyze(self._on_start_analysis) # ← UUSI
|
|
118
|
+
self.stack.add_named(self.settings_view, "settings")
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
has_projects = len(list_projects()) > 0
|
|
122
|
+
except Exception:
|
|
123
|
+
has_projects = False
|
|
124
|
+
|
|
125
|
+
self.stack.set_visible_child_name("projects" if has_projects else "welcome")
|
|
126
|
+
|
|
127
|
+
toolbar_view.set_content(self.stack)
|
|
128
|
+
|
|
129
|
+
self.set_content(toolbar_view)
|
|
130
|
+
|
|
131
|
+
# ------------------------------------------------------------------
|
|
132
|
+
# Welcome view
|
|
133
|
+
# ------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
def _build_welcome_view(self) -> Gtk.Widget:
|
|
136
|
+
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
|
|
137
|
+
box.set_margin_top(48)
|
|
138
|
+
box.set_margin_bottom(48)
|
|
139
|
+
box.set_margin_start(48)
|
|
140
|
+
box.set_margin_end(48)
|
|
141
|
+
box.set_valign(Gtk.Align.CENTER)
|
|
142
|
+
box.set_halign(Gtk.Align.CENTER)
|
|
143
|
+
|
|
144
|
+
icon = Gtk.Image.new_from_icon_name("system-search-symbolic")
|
|
145
|
+
icon.set_pixel_size(96)
|
|
146
|
+
icon.add_css_class("welcome-icon")
|
|
147
|
+
box.append(icon)
|
|
148
|
+
|
|
149
|
+
title = Gtk.Label(label="Siftcode")
|
|
150
|
+
title.add_css_class("welcome-title")
|
|
151
|
+
box.append(title)
|
|
152
|
+
|
|
153
|
+
subtitle = Gtk.Label(
|
|
154
|
+
label="Local LLM-powered code analysis and developer guide generator",
|
|
155
|
+
wrap=True,
|
|
156
|
+
justify=Gtk.Justification.CENTER,
|
|
157
|
+
)
|
|
158
|
+
subtitle.add_css_class("welcome-subtitle")
|
|
159
|
+
box.append(subtitle)
|
|
160
|
+
|
|
161
|
+
box.append(Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=24))
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
project_count = len(list_projects())
|
|
165
|
+
except Exception:
|
|
166
|
+
project_count = 0
|
|
167
|
+
|
|
168
|
+
stats = self._build_stats_row(
|
|
169
|
+
("Saved projects", str(project_count)),
|
|
170
|
+
("Config dir", str(PROJECTS_DIR).replace(str(Path.home()), "~")),
|
|
171
|
+
)
|
|
172
|
+
box.append(stats)
|
|
173
|
+
|
|
174
|
+
return box
|
|
175
|
+
|
|
176
|
+
def _build_stats_row(self, *pairs: tuple[str, str]) -> Gtk.Widget:
|
|
177
|
+
row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=48)
|
|
178
|
+
row.set_halign(Gtk.Align.CENTER)
|
|
179
|
+
row.set_margin_top(24)
|
|
180
|
+
|
|
181
|
+
for label, value in pairs:
|
|
182
|
+
col = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4)
|
|
183
|
+
col.set_halign(Gtk.Align.CENTER)
|
|
184
|
+
|
|
185
|
+
value_label = Gtk.Label(label=value)
|
|
186
|
+
value_label.add_css_class("stat-value")
|
|
187
|
+
col.append(value_label)
|
|
188
|
+
|
|
189
|
+
name_label = Gtk.Label(label=label)
|
|
190
|
+
name_label.add_css_class("stat-label")
|
|
191
|
+
col.append(name_label)
|
|
192
|
+
|
|
193
|
+
row.append(col)
|
|
194
|
+
|
|
195
|
+
return row
|
|
196
|
+
|
|
197
|
+
# ------------------------------------------------------------------
|
|
198
|
+
# Row context actions
|
|
199
|
+
# ------------------------------------------------------------------
|
|
200
|
+
|
|
201
|
+
def _on_edit_row_action(self, _action, _param) -> None:
|
|
202
|
+
row = self._active_row
|
|
203
|
+
if row is None:
|
|
204
|
+
return
|
|
205
|
+
config = getattr(row, "config", None)
|
|
206
|
+
if config is not None:
|
|
207
|
+
self._on_project_activated(config)
|
|
208
|
+
|
|
209
|
+
def _project_slug(self, name: str) -> str:
|
|
210
|
+
"""Convert a project name to the same slug used in output folders."""
|
|
211
|
+
slug = "".join(
|
|
212
|
+
c if c.isalnum() or c in "-_" else "-"
|
|
213
|
+
for c in name.lower().replace(" ", "-")
|
|
214
|
+
)
|
|
215
|
+
while "--" in slug:
|
|
216
|
+
slug = slug.replace("--", "-")
|
|
217
|
+
return slug.strip("-") or "project"
|
|
218
|
+
|
|
219
|
+
def _find_latest_output(self, config: ProjectConfig) -> "Path | None":
|
|
220
|
+
"""Return the most recent output folder for the given project."""
|
|
221
|
+
from pathlib import Path
|
|
222
|
+
|
|
223
|
+
output_base = Path.home() / "siftcode-output"
|
|
224
|
+
if not output_base.exists():
|
|
225
|
+
return None
|
|
226
|
+
|
|
227
|
+
slug = self._project_slug(config.name)
|
|
228
|
+
|
|
229
|
+
# Match both old (slug_timestamp) and new (timestamp_slug) formats
|
|
230
|
+
candidates = [
|
|
231
|
+
d for d in output_base.iterdir()
|
|
232
|
+
if d.is_dir() and (d.name.endswith(f"_{slug}") or d.name.startswith(f"{slug}_"))
|
|
233
|
+
]
|
|
234
|
+
if not candidates:
|
|
235
|
+
return None
|
|
236
|
+
|
|
237
|
+
# Sort by name (timestamp prefix in new format, suffix in old)
|
|
238
|
+
candidates.sort(key=lambda d: d.name, reverse=True)
|
|
239
|
+
return candidates[0]
|
|
240
|
+
|
|
241
|
+
def _on_open_latest_guide(self, _action, _param) -> None:
|
|
242
|
+
"""Open the latest guide for the active row's project."""
|
|
243
|
+
row = self._active_row
|
|
244
|
+
if row is None:
|
|
245
|
+
return
|
|
246
|
+
config = getattr(row, "config", None)
|
|
247
|
+
if config is None:
|
|
248
|
+
return
|
|
249
|
+
|
|
250
|
+
latest = self._find_latest_output(config)
|
|
251
|
+
if latest is None:
|
|
252
|
+
return
|
|
253
|
+
|
|
254
|
+
guide_path = latest / "guide.md"
|
|
255
|
+
if not guide_path.exists():
|
|
256
|
+
return
|
|
257
|
+
|
|
258
|
+
from siftcode.gui.guide_window import GuideWindow
|
|
259
|
+
|
|
260
|
+
window = GuideWindow(self, f"Guide — {config.name}", guide_path)
|
|
261
|
+
window.present()
|
|
262
|
+
|
|
263
|
+
def _on_open_output_folder(self, _action, _param) -> None:
|
|
264
|
+
"""Open the latest output folder for the active row's project."""
|
|
265
|
+
import subprocess
|
|
266
|
+
|
|
267
|
+
row = self._active_row
|
|
268
|
+
if row is None:
|
|
269
|
+
return
|
|
270
|
+
config = getattr(row, "config", None)
|
|
271
|
+
if config is None:
|
|
272
|
+
return
|
|
273
|
+
|
|
274
|
+
latest = self._find_latest_output(config)
|
|
275
|
+
if latest is None:
|
|
276
|
+
return
|
|
277
|
+
|
|
278
|
+
try:
|
|
279
|
+
subprocess.Popen(
|
|
280
|
+
["xdg-open", str(latest)],
|
|
281
|
+
stdout=subprocess.DEVNULL,
|
|
282
|
+
stderr=subprocess.DEVNULL,
|
|
283
|
+
)
|
|
284
|
+
except Exception:
|
|
285
|
+
pass
|
|
286
|
+
|
|
287
|
+
def _on_delete_row_action(self, _action, _param) -> None:
|
|
288
|
+
row = self._active_row
|
|
289
|
+
if row is None:
|
|
290
|
+
return
|
|
291
|
+
config = getattr(row, "config", None)
|
|
292
|
+
if config is None:
|
|
293
|
+
return
|
|
294
|
+
|
|
295
|
+
dialog = Adw.MessageDialog.new(
|
|
296
|
+
self,
|
|
297
|
+
"Delete project?",
|
|
298
|
+
f"Are you sure you want to delete '{config.name}'?\n\n"
|
|
299
|
+
f"This removes the saved configuration, not the project files.",
|
|
300
|
+
)
|
|
301
|
+
dialog.add_response("cancel", "Cancel")
|
|
302
|
+
dialog.add_response("delete", "Delete")
|
|
303
|
+
dialog.set_response_appearance(
|
|
304
|
+
"delete", Adw.ResponseAppearance.DESTRUCTIVE
|
|
305
|
+
)
|
|
306
|
+
dialog.set_default_response("cancel")
|
|
307
|
+
dialog.set_close_response("cancel")
|
|
308
|
+
dialog.connect("response", self._on_delete_row_response, config)
|
|
309
|
+
dialog.present()
|
|
310
|
+
|
|
311
|
+
def _on_delete_row_response(self, _dialog, response_id: str, config) -> None:
|
|
312
|
+
if response_id != "delete":
|
|
313
|
+
return
|
|
314
|
+
|
|
315
|
+
if delete_project(config.name):
|
|
316
|
+
logger.info(f"Deleted project: {config.name}")
|
|
317
|
+
self.project_list.refresh()
|
|
318
|
+
else:
|
|
319
|
+
error = Adw.MessageDialog.new(
|
|
320
|
+
self,
|
|
321
|
+
"Could not delete",
|
|
322
|
+
f"Project '{config.name}' was not found on disk.",
|
|
323
|
+
)
|
|
324
|
+
error.add_response("ok", "OK")
|
|
325
|
+
error.present()
|
|
326
|
+
|
|
327
|
+
# ------------------------------------------------------------------
|
|
328
|
+
# View callbacks
|
|
329
|
+
# ------------------------------------------------------------------
|
|
330
|
+
|
|
331
|
+
def _on_project_activated(self, config: ProjectConfig) -> None:
|
|
332
|
+
logger.info(f"Project activated: {config.name}")
|
|
333
|
+
# Hide the main header — SettingsView has its own
|
|
334
|
+
self.main_header.set_visible(False)
|
|
335
|
+
self.settings_view.load_config(config)
|
|
336
|
+
self.stack.set_visible_child_name("settings")
|
|
337
|
+
|
|
338
|
+
def _on_settings_saved(self, config: ProjectConfig) -> None:
|
|
339
|
+
logger.info(f"Settings saved for: {config.name}")
|
|
340
|
+
# Restore the main header
|
|
341
|
+
self.main_header.set_visible(True)
|
|
342
|
+
self.project_list.refresh()
|
|
343
|
+
self.stack.set_visible_child_name("projects")
|
|
344
|
+
|
|
345
|
+
def _on_settings_cancelled(self) -> None:
|
|
346
|
+
# Restore the main header
|
|
347
|
+
self.main_header.set_visible(True)
|
|
348
|
+
self.stack.set_visible_child_name("projects")
|
|
349
|
+
|
|
350
|
+
def _on_settings_deleted(self, config: ProjectConfig) -> None:
|
|
351
|
+
logger.info(f"Settings view deleted: {config.name}")
|
|
352
|
+
# Restore the main header
|
|
353
|
+
self.main_header.set_visible(True)
|
|
354
|
+
self.project_list.refresh()
|
|
355
|
+
self.stack.set_visible_child_name("projects")
|
|
356
|
+
|
|
357
|
+
def _on_analyzer_back(self) -> None:
|
|
358
|
+
"""Called when the Back button in the analyzer is pressed."""
|
|
359
|
+
# Refresh the project list so last_run updates are visible
|
|
360
|
+
self.project_list.refresh()
|
|
361
|
+
# Return to the project list
|
|
362
|
+
self.main_header.set_visible(True)
|
|
363
|
+
self.stack.set_visible_child_name("projects")
|
|
364
|
+
|
|
365
|
+
def _on_start_analysis(self, config: ProjectConfig) -> None:
|
|
366
|
+
"""Called when the user clicks Analyze in settings."""
|
|
367
|
+
logger.info(f"Starting analysis for: {config.name}")
|
|
368
|
+
self.main_header.set_visible(False)
|
|
369
|
+
self.analyzer_view.load_config(config)
|
|
370
|
+
self.stack.set_visible_child_name("analyzer")
|
|
371
|
+
|
|
372
|
+
# ------------------------------------------------------------------
|
|
373
|
+
# Add project
|
|
374
|
+
# ------------------------------------------------------------------
|
|
375
|
+
|
|
376
|
+
def _on_add_clicked(self, _button) -> None:
|
|
377
|
+
dialog = Gtk.FileDialog.new()
|
|
378
|
+
dialog.set_title("Select a project directory")
|
|
379
|
+
dialog.select_folder(self, None, self._on_folder_selected)
|
|
380
|
+
|
|
381
|
+
def _on_folder_selected(self, dialog, result) -> None:
|
|
382
|
+
try:
|
|
383
|
+
folder = dialog.select_folder_finish(result)
|
|
384
|
+
except Exception as e:
|
|
385
|
+
logger.debug(f"Folder selection cancelled or failed: {e}")
|
|
386
|
+
return
|
|
387
|
+
|
|
388
|
+
if folder is None:
|
|
389
|
+
return
|
|
390
|
+
|
|
391
|
+
path = Path(folder.get_path())
|
|
392
|
+
logger.info(f"Adding project: {path}")
|
|
393
|
+
self._ask_for_project_name(path)
|
|
394
|
+
|
|
395
|
+
def _ask_for_project_name(self, path: Path) -> None:
|
|
396
|
+
entry = Gtk.Entry()
|
|
397
|
+
entry.set_text(path.name)
|
|
398
|
+
entry.set_activates_default(True)
|
|
399
|
+
|
|
400
|
+
dialog = Adw.MessageDialog.new(
|
|
401
|
+
self,
|
|
402
|
+
"Name this project",
|
|
403
|
+
f"Directory: {path}",
|
|
404
|
+
)
|
|
405
|
+
dialog.set_extra_child(entry)
|
|
406
|
+
dialog.add_response("cancel", "Cancel")
|
|
407
|
+
dialog.add_response("add", "Add")
|
|
408
|
+
dialog.set_response_appearance("add", Adw.ResponseAppearance.SUGGESTED)
|
|
409
|
+
dialog.set_default_response("add")
|
|
410
|
+
dialog.set_close_response("cancel")
|
|
411
|
+
|
|
412
|
+
dialog.connect("response", self._on_name_response, path, entry)
|
|
413
|
+
dialog.present()
|
|
414
|
+
|
|
415
|
+
def _on_name_response(
|
|
416
|
+
self,
|
|
417
|
+
_dialog,
|
|
418
|
+
response_id: str,
|
|
419
|
+
path: Path,
|
|
420
|
+
entry: Gtk.Entry,
|
|
421
|
+
) -> None:
|
|
422
|
+
if response_id != "add":
|
|
423
|
+
return
|
|
424
|
+
|
|
425
|
+
name = entry.get_text().strip()
|
|
426
|
+
if not name:
|
|
427
|
+
name = path.name
|
|
428
|
+
|
|
429
|
+
config = ProjectConfig(name=name, path=path)
|
|
430
|
+
|
|
431
|
+
try:
|
|
432
|
+
save_project(config)
|
|
433
|
+
logger.info(f"Saved project: {name}")
|
|
434
|
+
except Exception as e:
|
|
435
|
+
logger.error(f"Failed to save project: {e}")
|
|
436
|
+
error = Adw.MessageDialog.new(
|
|
437
|
+
self,
|
|
438
|
+
"Could not save project",
|
|
439
|
+
str(e),
|
|
440
|
+
)
|
|
441
|
+
error.add_response("ok", "OK")
|
|
442
|
+
error.present()
|
|
443
|
+
return
|
|
444
|
+
|
|
445
|
+
self.project_list.refresh()
|
|
446
|
+
self.stack.set_visible_child_name("projects")
|