Skip to content

Conversation

@FaserF
Copy link
Owner

@FaserF FaserF commented Jan 20, 2026

Summary by CodeRabbit

  • New Features

    • Expanded time-of-day greetings and new German/English UI strings (thumbprint copy, group management).
    • Thumbprint copy action and selectable thumbprint display.
  • Bug Fixes

    • Improved crash/error recovery and safer dialog handling across the UI.
    • More robust package/details loading and directory scanning behavior.
    • Safer addon installation from ZIPs with better manifest detection.
  • Refactor

    • Centralized version parsing and consolidated UI update/error-handling paths.
  • Tests

    • Added extensive UI and integration tests for dialogs, group management, GitHub login.
  • Documentation

    • New GPO troubleshooting guide and OMA‑URI path corrections.
  • Chores

    • Improved logging and CI merge pre-checks.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 20, 2026

📝 Walkthrough

Walkthrough

Adds robust error handling and UI-safety utilities across the modern GUI, centralizes version parsing in the release script, hardens addon ZIP handling, expands localization greetings, modifies CI auto-merge to pre-check review comments, and adds/updates many tests and docs.

Changes

Cohort / File(s) Summary
Workflows
.github/workflows/release.yml, .github/workflows/review-auto-merge.yml
release.yml: improved sed version-replace regex to preserve indentation; review-auto-merge.yml: new pre-check stage that detects inline review/CodeRabbit comments and skips auto-merge when present.
Build tooling
scripts/build_release.ps1
Added Extract-VersionInfo function and unified version parsing/validation with fallbacks and improved error logging.
Localization
src/switchcraft/assets/lang/en.json, src/switchcraft/assets/lang/de.json
Added many time-of-day greeting keys and thumbprint/group-management UI strings.
GUI core / utilities
src/switchcraft/gui_modern/app.py, src/switchcraft/gui_modern/utils/view_utils.py
New runtime/crash rendering and safe UI helpers: _show_runtime_error, _show_error_view, _safe_event_handler, _open_dialog_safe, _run_task_with_fallback and related defensive update patterns.
GUI views — general
src/switchcraft/gui_modern/views/*.py (dashboard_view, group_manager_view, home_view, intune_store_view, library_view, settings_view, winget_view, ...)
Extensive hardening: replace ad-hoc run_task usage with centralized safe runners, add try/except boundaries, safer dialog/open/close flows, randomized greeting selection (home), improved scanning and listing (library), group management safety and member dialogs (group_manager), addon handling and cert thumbprint UI tweaks (settings).
Services & packaging
src/switchcraft/services/addon_service.py, switchcraft_modern.spec, src/switchcraft_winget/utils/winget.py
AddonService: robust manifest discovery, Zip Slip protection, normalized path handling and clearer errors; spec: explicit hidden_imports collection for gui_modern; winget: centralized subprocess kwargs, PowerShell parameterization, existence verification, and auto-install flag.
Logging utility
src/switchcraft/utils/logging_handler.py
set_debug_mode now updates root logger and propagates level to existing handlers with user-facing messages.
Tests
tests/* (many new/updated tests)
Added and updated tests for dialog safety, group manager members dialog, GitHub login flow, critical UI flows, winget/details behavior, addon reproduce tests, and relaxed/flakiness-tolerant assertions.
Docs
docs/* (Intune_Configuration_Guide.md, GPO_TROUBLESHOOTING.md, PolicyDefinitions/README.md, config)
Added GPO troubleshooting doc; corrected ADMX/OMA-URI casing/path guidance and navigation/mapping updates.

Sequence Diagram(s)

(omitted)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I hopped through code and caught each crack,
Wrapped dialogs safe and guarded the stack.
Greetings multiplied for dawn to night,
CI checks now pause if reviewers write.
A nibble, a patch — the app stays bright! 🥕

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 46.51% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'View & CI fixes' is vague and generic, using non-descriptive terms that don't clearly convey the specific changes made in this comprehensive pull request. Consider using a more specific title that highlights the main focus, such as 'Add error handling and safety wrappers for UI views and workflows' or 'Improve UI stability with comprehensive error handling and CI workflow enhancements'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/switchcraft/services/addon_service.py (1)

210-222: Zip Slip protection uses string prefix check which can be bypassed with sibling directory paths.

The check at line 220 is vulnerable to path confusion when a zip contains members in sibling directories. For example, if target resolves to /home/user/addons/foo, a member like ../foobar/file.txt would be normalized and resolved to /home/user/addons/foobar/file.txt, which passes the startswith check even though it's outside the target directory (sibling, not child).

The project requires Python 3.9+, so use Path.relative_to() to safely validate path containment:

🔒 Safer Zip Slip protection
                    # Ensure the resolved path starts with the target directory (prevent Zip Slip)
-                    if not str(file_path).startswith(str(target.resolve())):
+                    try:
+                        file_path.relative_to(target.resolve())
+                    except ValueError:
                         logger.error(f"Security Alert: Attempted Zip Slip with {member.filename}")
                         continue
🤖 Fix all issues with AI agents
In @.github/workflows/review-auto-merge.yml:
- Around line 76-113: The notification double-counts CodeRabbit inline comments
because INLINE_REVIEW_COMMENTS includes all inline comments while
TOTAL_CODERABBIT_COMMENTS also sums CodeRabbit inline comments; update the
messaging to avoid overlap by: stop using TOTAL_CODERABBIT_COMMENTS in the
notification summary, instead report INLINE_REVIEW_COMMENTS for all inline
comments and separately report CODERABBIT_INLINE (how many of those inline
comments are from CodeRabbit) and CODERABBIT_PR_COMMENTS (CodeRabbit non-inline
PR comments), and adjust the BODY construction (replace uses of
TOTAL_CODERABBIT_COMMENTS) so it mentions CODERABBIT_INLINE and
CODERABBIT_PR_COMMENTS distinctly; keep existing variables
INLINE_REVIEW_COMMENTS, CODERABBIT_INLINE, CODERABBIT_PR_COMMENTS,
COMMENT_EXISTS and BODY to locate the changes.

In `@scripts/build_release.ps1`:
- Around line 172-186: The parsed pyproject version may be malformed so
$VersionInfo.Numeric can be empty; after extracting with Extract-VersionInfo
(when reading $PyProjectFile and matching $VersionLine) validate that
$VersionInfo.Numeric is non-empty and well-formed, and if not, reassign
$VersionInfo by calling Extract-VersionInfo -VersionString $FallbackVersion (or
otherwise set $AppVersion/$AppVersionNumeric/$AppVersionInfo from
$FallbackVersion), then Write-Host a warning mentioning the fallback; update
references to $AppVersion, $AppVersionNumeric, and $AppVersionInfo accordingly
to ensure installer metadata never receives an empty numeric version.

In `@src/switchcraft_winget/utils/winget.py`:
- Around line 208-215: get_package_details now raises on "not found", but the
legacy GUI in winget_view.py does not catch those exceptions (it runs in a
daemon thread), causing silent failures; update the legacy view
(src/switchcraft/gui/views/winget_view.py) to wrap calls to get_package_details
in a try/except that catches the exception raised for missing packages and
handles it like the modern GUI (e.g., show a "not found" state or log and
continue), or if the old view is deprecated, document/confirm deprecation and
remove its usage; specifically locate the call sites in winget_view.py where
get_package_details is invoked and add explicit exception handling consistent
with the behavior in src/switchcraft/gui_modern/views/winget_view.py so
missing-package exceptions are handled rather than causing silent thread
failures.

In `@src/switchcraft/gui_modern/utils/view_utils.py`:
- Around line 194-206: The fallback incorrectly calls func() directly (which
returns a coroutine) when page.run_task(func) fails; replace that direct call
with proper coroutine execution: import asyncio and attempt to run the coroutine
by using asyncio.run(func()) when no event loop is running, otherwise schedule
it on the running loop (e.g., loop.create_task(func()) or
asyncio.create_task(func())); update the code inside the except block that
currently references func() to detect a running loop and either
asyncio.run(func()) or create_task(func()) so the coroutine actually executes.

In `@src/switchcraft/gui_modern/views/intune_store_view.py`:
- Around line 213-215: The stray logger.exception call referencing an undefined
ex must be removed or moved into the corresponding except block; locate the call
in intune_store_view.py near the UI update code (around the call to
self._run_task_safe(_update_ui)) and either delete the line
"logger.exception(f\"Failed to update UI: {ex}\")" or wrap the scheduling in a
try/except that logs the actual exception variable (e.g., except Exception as
ex: logger.exception("Failed to update UI", exc_info=ex)); ensure no reference
to ex exists outside an except block and that logging uses exc_info or the
caught exception variable.

In `@src/switchcraft/gui_modern/views/settings_view.py`:
- Around line 1063-1064: The assignment to app must be inside the hasattr check
to avoid a syntax/control-flow bug: indent "app = self.app_page.switchcraft_app"
so it is executed only when hasattr(self.app_page, 'switchcraft_app') is True
(i.e., make it part of the if block in settings_view.py), and if subsequent code
expects app to exist consider adding an else or guard using that same attribute
check where functions/methods reference app.
- Around line 1354-1358: The current duplicate-check uses `if handler not in
root_logger.handlers` which fails because `handler` is a new instance; instead
check for existing handler by type: before adding, use something like `if not
any(isinstance(h, FletLogHandler) for h in root_logger.handlers)` (or compare
`h.__class__ is handler.__class__`) and only then call
`root_logger.addHandler(handler)` and `logger.info(...)`; update the block
around `root_logger`, `handler`, and the info log to use this type-based
presence check.

In `@src/switchcraft/utils/logging_handler.py`:
- Around line 123-126: The code in set_debug_mode uses an undefined variable
`logger`, causing a NameError at runtime; fix it by replacing `logger` with the
existing `root_logger` (used elsewhere in this module) or by adding a
module-level logger (e.g., set `logger = logging.getLogger(__name__)`) and then
use that `logger` inside set_debug_mode to emit the two info messages about
debug mode. Ensure the chosen symbol (`root_logger` or newly defined `logger`)
is in scope for the set_debug_mode function.

In `@tests/test_gui_views.py`:
- Around line 99-105: The test replaces GroupManagerView._has_credentials with a
zero-arg function which will raise TypeError when bound and also leaks the
override globally; change mock_has_credentials to accept self (def
mock_has_credentials(self): return True) and apply it using
unittest.mock.patch.object (or patch.object as a context manager/decorator)
targeting GroupManagerView._has_credentials so the override is scoped to the
test and automatically restored after the test finishes; update the test to
create the view inside the patch scope (where view = GroupManagerView(page) is
executed).
🧹 Nitpick comments (12)
src/switchcraft/services/addon_service.py (1)

164-180: Manifest search logic has redundant condition.

At line 174, checking normalized == 'manifest.json' is redundant since this case is already handled by line 165. If we reach the else block (lines 167-180), then "manifest.json" in files was already false, meaning exact root-level match won't exist.

The endswith('/manifest.json') pattern is correct for subdirectory manifests. The logic still works but has unnecessary branching.

♻️ Simplified manifest search
-                # Check for manifest.json at root first
-                if "manifest.json" in files:
-                    manifest_path = "manifest.json"
-                else:
-                    # Check for manifest.json in common subdirectories
-                    # Some ZIPs might have it in a subfolder
-                    for file_path in files:
-                        # Normalize path separators
-                        normalized = file_path.replace('\\', '/')
-                        # Check if it's manifest.json at any level (but prefer root)
-                        if normalized.endswith('/manifest.json') or normalized == 'manifest.json':
-                            # Prefer root-level, but accept subdirectory if root not found
-                            if manifest_path is None or normalized == 'manifest.json':
-                                manifest_path = file_path
-                                # If we found root-level, stop searching
-                                if normalized == 'manifest.json':
-                                    break
+                # Check for manifest.json at root first
+                if "manifest.json" in files:
+                    manifest_path = "manifest.json"
+                else:
+                    # Check for manifest.json in subdirectories
+                    for file_path in files:
+                        normalized = file_path.replace('\\', '/')
+                        if normalized.endswith('/manifest.json'):
+                            manifest_path = file_path
+                            break  # Use first found subdirectory manifest
src/switchcraft/gui_modern/views/settings_view.py (2)

124-144: Double-wrapped error handler is redundant.

safe_lang_handler wraps _handle_lang_change, which already has its own try/except. If an exception occurs in _handle_lang_change, it's caught at line 132-134, showing a snack. Then safe_lang_handler at lines 140-142 catches nothing (since the inner handler already caught it) and would only trigger if the snack itself fails.

Consider consolidating to a single error handling layer.

♻️ Consolidated error handler
-        # Set on_change handler - wrap in safe handler to catch errors
-        def _handle_lang_change(e):
-            try:
-                if e.control.value:
-                    logger.info(f"Language dropdown changed to: {e.control.value}")
-                    self._on_lang_change(e.control.value)
-                else:
-                    logger.warning("Language dropdown changed but value is None/empty")
-            except Exception as ex:
-                logger.exception(f"Error handling language change: {ex}")
-                self._show_snack(f"Failed to change language: {ex}", "RED")
-
-        # Wrap handler to catch exceptions and show in error view
-        def safe_lang_handler(e):
-            try:
-                _handle_lang_change(e)
-            except Exception as ex:
-                logger.exception(f"Error in language change handler: {ex}")
-                self._show_error_view(ex, "Language dropdown change")
-
-        lang_dd.on_change = safe_lang_handler
+        def _handle_lang_change(e):
+            try:
+                if e.control.value:
+                    logger.info(f"Language dropdown changed to: {e.control.value}")
+                    self._on_lang_change(e.control.value)
+                else:
+                    logger.warning("Language dropdown changed but value is None/empty")
+            except Exception as ex:
+                logger.exception(f"Error handling language change: {ex}")
+                self._show_snack(f"Failed to change language: {ex}", "RED")
+
+        lang_dd.on_change = _handle_lang_change

1813-1859: GPO certificate detection logic improved but has verification gap.

The logic now properly checks is_managed() first (lines 1819) before attempting to use GPO values. However, when verification fails (line 1850-1859), the code unconditionally treats GPO as configured even if the thumbprint/path is empty or invalid, which could lead to signing failures later.

Consider adding validation that the GPO-configured value is actually usable before returning success.

src/switchcraft/gui_modern/views/home_view.py (2)

36-38: Move import random to module level.

Importing random inside the method on each call is inefficient. Move this to the top of the file with other imports.

♻️ Suggested refactor

At the top of the file (after line 2):

import random

Then remove line 37.


98-103: Defensive bounds check is good, but arrays are always equal length.

The check on line 101 (if selected_index < len(default_greetings)) is defensive but since greeting_keys and default_greetings are defined together with matching lengths in each branch, this condition will always be true. The defensive check doesn't hurt, but consider using zip or a list of tuples to guarantee parity:

♻️ Alternative approach using paired tuples
# Example for one time period:
greetings = [
    ("greeting_morning_1", "Good Morning"),
    ("greeting_morning_2", "Morning!"),
    # ...
]
greeting_key, default_greeting = random.choice(greetings)

This eliminates the possibility of mismatched array lengths.

src/switchcraft/gui_modern/utils/view_utils.py (2)

5-5: Module-level traceback import is unused directly.

The traceback import at line 5 is shadowed by import traceback as tb inside _show_error_view (line 24). Either remove line 5 or use it consistently instead of re-importing.

♻️ Remove unused import
-import traceback

The module-level import is not used; the method imports it locally as tb.


151-243: Thorough fallback handling, but high complexity.

The method handles many edge cases (no page, page without run_task, sync vs async functions) with multiple fallback paths. While comprehensive, the nested try/except blocks make this difficult to follow.

Consider documenting the expected fallback chain in the docstring or extracting some paths into helper methods.

src/switchcraft/gui_modern/app.py (2)

27-84: Duplication with ViewMixin._show_error_view.

This static method largely duplicates the logic in ViewMixin._show_error_view from view_utils.py. Consider refactoring to share code:

  1. Extract the core crash view display logic into a shared utility function
  2. Have both methods call the shared function

The main difference is page.views[-1].controls access (lines 61-64) vs ViewMixin's self.controls approach.

♻️ Consider extracting shared logic
# In view_utils.py or a new crash_utils.py
def display_crash_view(page: ft.Page, error: Exception, context: str = None):
    """Shared crash view display logic."""
    # Common implementation here
    pass

# Then in app.py:
`@staticmethod`
def _show_runtime_error(page, error, context=None):
    from switchcraft.gui_modern.utils.view_utils import display_crash_view
    display_crash_view(page, error, context)

369-383: Double page.update() may cause visual flicker.

Lines 377 and 381 both call page.update(). The second update (lines 379-383) with a try/except is defensive, but calling update twice in quick succession may cause visual flicker. Consider consolidating or adding a comment explaining why the second update is necessary.

src/switchcraft_winget/utils/winget.py (1)

340-350: Use _get_subprocess_kwargs to avoid duplicated window‑hiding logic.
Install/verify paths still rebuild startupinfo/creationflags manually (Lines 340–349, 668–677). Since _get_subprocess_kwargs now centralizes this, reusing it keeps behavior consistent and reduces drift.

♻️ Consolidate kwargs usage
-            startupinfo = self._get_startup_info()
-            kwargs = {}
-            if startupinfo:
-                kwargs['startupinfo'] = startupinfo
-            import sys
-            if sys.platform == "win32":
-                if hasattr(subprocess, 'CREATE_NO_WINDOW'):
-                    kwargs['creationflags'] = subprocess.CREATE_NO_WINDOW
-                else:
-                    kwargs['creationflags'] = 0x08000000  # CREATE_NO_WINDOW constant
-            proc = subprocess.run(cmd, capture_output=True, text=True, timeout=300, **kwargs)
+            kwargs = self._get_subprocess_kwargs()
+            proc = subprocess.run(cmd, capture_output=True, text=True, timeout=300, **kwargs)
-            startupinfo = self._get_startup_info()
-            kwargs = {}
-            if startupinfo:
-                kwargs['startupinfo'] = startupinfo
-            import sys
-            if sys.platform == "win32":
-                if hasattr(subprocess, 'CREATE_NO_WINDOW'):
-                    kwargs['creationflags'] = subprocess.CREATE_NO_WINDOW
-                else:
-                    kwargs['creationflags'] = 0x08000000
-            proc = subprocess.run(cmd, capture_output=True, text=True, encoding="utf-8", errors="ignore", timeout=30, **kwargs)
+            kwargs = self._get_subprocess_kwargs()
+            proc = subprocess.run(cmd, capture_output=True, text=True, encoding="utf-8", errors="ignore", timeout=30, **kwargs)

Also applies to: 668-678

src/switchcraft/gui_modern/views/library_view.py (1)

136-197: Directory scans on the UI thread may still block the view.
Lines 136–197 do filesystem IO synchronously. For large folders this can freeze the UI. Consider moving the scan to a background thread and marshal UI updates via _run_task_safe.

src/switchcraft/assets/lang/en.json (1)

558-594: LGTM! Comprehensive time-of-day greeting variants.

The expanded greeting keys provide good UX variety and align with the German localization additions. The structure is consistent and the JSON is valid.

One minor semantic note: greeting_night_2 (line 592) uses "Evening!" which is slightly inconsistent with the "night" category—consider using a night-specific phrase like "Good night!" or "Nighttime!" for consistency.

🔧 Optional: Align night greeting variant
-    "greeting_night_2": "Evening!",
+    "greeting_night_2": "Good night!",

Comment on lines 123 to 126
if enabled:
logger.info("Debug mode enabled - all log levels (DEBUG, INFO, WARNING, ERROR, CRITICAL) will be captured")
else:
logger.info("Debug mode disabled - only INFO and above will be captured")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Undefined logger variable will cause NameError at runtime.

The logger variable used on lines 124 and 126 is never defined in this module. When set_debug_mode is called, this will raise a NameError.

🐛 Proposed fix

Either use root_logger which is already available in scope:

         if enabled:
-            logger.info("Debug mode enabled - all log levels (DEBUG, INFO, WARNING, ERROR, CRITICAL) will be captured")
+            root_logger.info("Debug mode enabled - all log levels (DEBUG, INFO, WARNING, ERROR, CRITICAL) will be captured")
         else:
-            logger.info("Debug mode disabled - only INFO and above will be captured")
+            root_logger.info("Debug mode disabled - only INFO and above will be captured")

Or define a module-level logger at the top of the file:

logger = logging.getLogger(__name__)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if enabled:
logger.info("Debug mode enabled - all log levels (DEBUG, INFO, WARNING, ERROR, CRITICAL) will be captured")
else:
logger.info("Debug mode disabled - only INFO and above will be captured")
if enabled:
root_logger.info("Debug mode enabled - all log levels (DEBUG, INFO, WARNING, ERROR, CRITICAL) will be captured")
else:
root_logger.info("Debug mode disabled - only INFO and above will be captured")
🤖 Prompt for AI Agents
In `@src/switchcraft/utils/logging_handler.py` around lines 123 - 126, The code in
set_debug_mode uses an undefined variable `logger`, causing a NameError at
runtime; fix it by replacing `logger` with the existing `root_logger` (used
elsewhere in this module) or by adding a module-level logger (e.g., set `logger
= logging.getLogger(__name__)`) and then use that `logger` inside set_debug_mode
to emit the two info messages about debug mode. Ensure the chosen symbol
(`root_logger` or newly defined `logger`) is in scope for the set_debug_mode
function.

@github-actions github-actions bot added the documentation Improvements or additions to documentation label Jan 20, 2026
@github-actions
Copy link
Contributor

PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://FaserF.github.io/SwitchCraft/pr-preview/pr-49/

Built to branch gh-pages at 2026-01-20 10:11 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/switchcraft/gui_modern/utils/view_utils.py (2)

168-188: Handle async funcs in the no-page fallback

Lines 174 and 182 call func() directly; if func is async, it won't execute and you'll leak a coroutine. Apply the same async handling pattern already used elsewhere in this method (lines 190+): check inspect.iscoroutinefunction(func), then use asyncio.create_task() for running event loops or asyncio.run() as fallback.

🔧 Proposed fix
                 except (RuntimeError, AttributeError):
                     # No page available, try direct call as fallback
                     try:
-                        func()
+                        if inspect.iscoroutinefunction(func):
+                            try:
+                                loop = asyncio.get_running_loop()
+                                asyncio.create_task(func())
+                            except RuntimeError:
+                                asyncio.run(func())
+                        else:
+                            func()
                         return True
                     except Exception as e:
                         logger.warning(f"Failed to execute function directly (no page): {e}", exc_info=True)
                         return False
             if not page:
                 # No page available, try direct call as fallback
                 try:
-                    func()
+                    if inspect.iscoroutinefunction(func):
+                        try:
+                            loop = asyncio.get_running_loop()
+                            asyncio.create_task(func())
+                        except RuntimeError:
+                            asyncio.run(func())
+                    else:
+                        func()
                     return True
                 except Exception as e:
                     logger.warning(f"Failed to execute function directly (page None): {e}", exc_info=True)
                     return False

359-454: Run fallback even without a page + respect async fallback types in sync path

Line 371 returns early when no page is present, completely skipping the promised fallback execution since fallback_func is only assigned at line 373. Additionally, the sync path (line 420) calls fallback_func() directly without checking if it's a coroutine function, while the else branch correctly handles this at line 430. This creates an inconsistency: if task_func is sync but fallback_func is async, line 420 will call it without awaiting, causing a coroutine object to be created and discarded rather than executed.

Move fallback_func assignment before the page check and add async handling to the sync path fallback to match the pattern already used in the else branch.

src/switchcraft/gui_modern/views/group_manager_view.py (1)

153-265: Re-enable the list on error paths

Line 162 disables list_container, but the HTTPError, ConnectionError, and generic Exception handlers (lines 188–244) only display error messages without re-enabling it, leaving the list permanently disabled after a failure. Only the BaseException handler and the success path re-enable it.

Proposed fix
                 def show_error():
                     try:
                         self._show_snack(error_msg, "RED")
+                        self.list_container.disabled = False
+                        self.update()
                     except (RuntimeError, AttributeError) as e:
                         logger.debug(f"Control not added to page (RuntimeError/AttributeError): {e}")
@@
                 def show_error():
                     try:
                         self._show_snack(error_msg, "RED")
+                        self.list_container.disabled = False
+                        self.update()
                     except (RuntimeError, AttributeError) as e:
                         logger.debug(f"Control not added to page (RuntimeError/AttributeError): {e}")
@@
                 def show_error():
                     try:
                         self._show_snack(error_msg, "RED")
                         # Also update the list to show error
                         if hasattr(self, 'groups_list'):
                             self.groups_list.controls.clear()
                             self.groups_list.controls.append(
                                 ft.Container(
                                     content=ft.Column([
                                         ft.Icon(ft.Icons.ERROR_OUTLINE, color="RED", size=48),
                                         ft.Text(error_msg, color="RED", text_align=ft.TextAlign.CENTER)
                                     ], horizontal_alignment=ft.CrossAxisAlignment.CENTER),
                                     alignment=ft.alignment.center,
                                     padding=20
                                 )
                             )
                             self.groups_list.update()
+                        self.list_container.disabled = False
+                        self.update()
                     except (RuntimeError, AttributeError) as e:
                         logger.debug(f"Control not added to page (RuntimeError/AttributeError): {e}")
🤖 Fix all issues with AI agents
In @.github/workflows/review-auto-merge.yml:
- Around line 81-88: The gh api calls that compute INLINE_REVIEW_COMMENTS,
CODERABBIT_INLINE, and CODERABBIT_PR_COMMENTS need pagination to avoid
undercounting; update each gh api invocation used to populate
INLINE_REVIEW_COMMENTS, CODERABBIT_INLINE and CODERABBIT_PR_COMMENTS to include
the --paginate flag so results are aggregated across all pages (keeping the same
--jq filters), ensuring the final counts reflect all pages of pull request
review comments and issue comments.

In `@src/switchcraft/gui_modern/app.py`:
- Around line 42-49: The code uses traceback.format_exc() which relies on an
active exception context; instead format the passed-in exception object `error`
in the helper (where `tb_str` is created). Replace tb.format_exc() with
formatting of the exception, e.g. use tb.format_exception(type(error), error,
error.__traceback__) (or tb.TracebackException.from_exception(error).format())
and join the result into a single string assigned to `tb_str`, preserving the
existing `if context:` prefix logic and leaving CrashDumpView usage unchanged.

In `@src/switchcraft/gui_modern/views/library_view.py`:
- Around line 273-283: The grid currently shows nothing when a search yields
zero matches; after computing filtered_files from self.all_files using
self.search_val, detect if filtered_files is empty and append a suitable "no
results" control to self.grid.controls instead of tiles (e.g., a centered
label/container with "No results" or similar). Update the block that iterates
over filtered_files and the call to self._create_tile(item) so that when
filtered_files is empty you create and append the empty-state control (and skip
creating tiles) to provide clear feedback to the user.

In `@src/switchcraft/gui_modern/views/settings_view.py`:
- Around line 714-719: The login button is set to "Starting..." with
ft.Icons.HOURGLASS_EMPTY in the block that references self.login_btn but never
restored on init/poll failures; update the failure and completion paths to reset
the button state. Specifically, in the init routine(s) and any poll/error
callbacks that handle initialization or polling failures (the methods that call
or wrap the block setting self.login_btn.text/icon), ensure you set
self.login_btn.text back to the original label, restore the original icon (or
None), and call self.login_btn.update() on both error paths and on successful
completion of polling so the button is never left stuck in the "Starting..."
state.
- Around line 1059-1061: There's a stray opening triple-quote that is turning
the body of _on_lang_change into a multiline string; remove the unintended
triple-quote so the subsequent lines (logger.info and logger.debug) and the rest
of the method become executable again, ensuring the method _on_lang_change
contains the logger.info(f"Language change requested: {val}") and
logger.debug(...) calls as real statements rather than part of a string.

In `@src/switchcraft/services/addon_service.py`:
- Around line 162-205: The manifest search currently accepts a nested
manifest_path but extraction still preserves the ZIP subfolder, so
read_manifest() fails expecting target/manifest.json; update the extraction
logic where files are written (the code that uses manifest_path and extracts ZIP
entries) to compute a manifest_dir =
os.path.dirname(manifest_path.replace('\\','/')) and, if manifest_dir is
non-empty, strip that leading prefix from every extracted file path (rebase
extraction into manifest_dir) so manifest.json and entry points end up at
target/manifest.json; also update the user-facing Exception message that lists
root_files to indicate if manifest was found under a subdirectory and show that
subdirectory; apply the same change to the other extraction branch referenced
(the block around lines 215-241) so both paths normalize extraction to the
manifest directory.

In `@tests/test_critical_ui_fixes.py`:
- Around line 141-145: The test currently contains a no-op assertion ("assert
True") which masks failures; replace it with a concrete assertion that verifies
the refresh action triggered data loading: patch or mock the _load_data method
before creating the view (or attach a spy), trigger the refresh button, wait for
the background thread, and assert that _load_data was called (or assert that
dir_info or the grid state changed accordingly). Alternatively, assert that
helper actions like goto_tab or the language setter were invoked by mocking
those methods and checking their call state instead of using assert True.

In `@tests/test_github_login_integration.py`:
- Around line 13-35: The fallback import block is missing a poll_until
implementation which will raise NameError if conftest import fails; add a
minimal poll_until function alongside is_ci_environment, skip_if_ci and
mock_page that accepts a predicate (callable), timeout and interval parameters,
polls the predicate until it returns True or the timeout elapses, and raises a
TimeoutError (or returns False) on timeout so tests depending on poll_until
behave consistently; ensure the symbol name is poll_until and its signature
matches how tests call it.
♻️ Duplicate comments (1)
scripts/build_release.ps1 (1)

182-187: Strengthen numeric validation (non-empty isn’t enough).
Values like 1.2 or 1.2. still pass and can yield inconsistent metadata. Validate MAJOR.MINOR.PATCH (or normalize) before accepting.

🔧 Suggested hardening
-            # Validate that the parsed version is non-empty and well-formed
-            if ([string]::IsNullOrWhiteSpace($VersionInfo.Numeric)) {
-                Write-Warning "Parsed version from pyproject.toml has empty numeric component, using fallback: $FallbackVersion"
-                $VersionInfo = Extract-VersionInfo -VersionString $FallbackVersion
-            }
+            # Validate numeric format (MAJOR.MINOR.PATCH)
+            if ($VersionInfo.Numeric -notmatch '^\d+\.\d+\.\d+$') {
+                Write-Warning "Invalid version format '$($VersionInfo.Full)' in pyproject.toml, using fallback: $FallbackVersion"
+                $VersionInfo = Extract-VersionInfo -VersionString $FallbackVersion
+            }
🧹 Nitpick comments (6)
src/switchcraft/utils/logging_handler.py (1)

111-127: Previous NameError is fixed, but line 123 is redundant.

The fix for the undefined logger variable is correct—using root_logger throughout resolves the runtime error. However, line 123 reassigns root_logger = logging.getLogger() unnecessarily since it's already in scope from line 113.

♻️ Remove redundant reassignment
         for handler in root_logger.handlers:
             if hasattr(handler, 'setLevel'):
                 handler.setLevel(level)
-        root_logger = logging.getLogger()
         if enabled:
             root_logger.info("Debug mode enabled - all log levels (DEBUG, INFO, WARNING, ERROR, CRITICAL) will be captured")
         else:
             root_logger.info("Debug mode disabled - only INFO and above will be captured")
switchcraft_modern.spec (1)

45-60: Filter gui_modern submodules with the same exclusion rules.

Right now collect_submodules('switchcraft.gui_modern') bypasses excluded_modules, which can reintroduce optional/addon modules and their deps. Consider applying the same filter and centralizing the explicit app inclusion to keep one consistent path.

♻️ Suggested refactor
-# Collect everything from gui_modern explicitly to ensure app.py is included
-try:
-    gui_modern_submodules = collect_submodules('switchcraft.gui_modern')
-    hidden_imports += gui_modern_submodules
-except Exception as e:
-    print(f"WARNING: Failed to collect gui_modern submodules: {e}")
+# Filter out modules that were moved to addons or don't exist
+excluded_modules = [
+    'switchcraft.utils.winget', 'switchcraft.gui.views.ai_view',
+    'switchcraft_winget', 'switchcraft_ai', 'switchcraft_advanced',
+    'switchcraft.utils.updater'
+]
+
+def _not_excluded(m):
+    return not any(m.startswith(ex) for ex in excluded_modules)
+
+# Collect everything from gui_modern explicitly to ensure app.py is included
+try:
+    gui_modern_submodules = collect_submodules('switchcraft.gui_modern')
+    hidden_imports += [m for m in gui_modern_submodules if _not_excluded(m)]
+except Exception as e:
+    print(f"WARNING: Failed to collect gui_modern submodules: {e}")
@@
-all_submodules = collect_submodules('switchcraft')
-# Filter out modules that were moved to addons or don't exist
-excluded_modules = ['switchcraft.utils.winget', 'switchcraft.gui.views.ai_view', 'switchcraft_winget', 'switchcraft_ai', 'switchcraft_advanced', 'switchcraft.utils.updater']
-# Ensure app.py is explicitly included (it might be filtered out otherwise)
-filtered_submodules = [m for m in all_submodules if not any(m.startswith(ex) for ex in excluded_modules)]
-# Explicitly add app.py if it's not already in the list
-if 'switchcraft.gui_modern.app' not in filtered_submodules:
-    filtered_submodules.append('switchcraft.gui_modern.app')
+all_submodules = collect_submodules('switchcraft')
+filtered_submodules = [m for m in all_submodules if _not_excluded(m)]
+filtered_submodules.append('switchcraft.gui_modern.app')
src/switchcraft/gui/views/winget_view.py (1)

193-208: Localize the new error-state labels.

The new user-facing strings (“Package ID”, “Version”, and the error line) are hardcoded; consider i18n keys so this view stays consistent with the rest of the localized UI.

tests/reproduce_addon_issue.py (1)

49-94: Convert print-only checks into real assertions.

The tests currently catch exceptions and only print failures, so they won’t fail in CI even when behavior regresses. Prefer direct assertions and let exceptions propagate.

✅ Example tightening (pytest-style assertions)
-            try:
-                # Attempt install
-                service.install_addon(str(zip_path))
-
-                # Verify installation
-                installed_path = service.addons_dir / "test.addon"
-                if installed_path.exists() and (installed_path / "manifest.json").exists():
-                    print("SUCCESS: Addon installed and manifest found.")
-                    # Check if script was extracted
-                    if (installed_path / "script.py").exists():
-                        print("SUCCESS: Subfiles extracted correctly.")
-                    else:
-                        print("FAILURE: script.py not found in installed folder.")
-                else:
-                    print("FAILURE: Addon folder or manifest not found after install.")
-
-            except Exception as e:
-                print(f"FAILURE: Install raised exception: {e}")
-                import traceback
-                traceback.print_exc()
+            service.install_addon(str(zip_path))
+            installed_path = service.addons_dir / "test.addon"
+            assert installed_path.exists()
+            assert (installed_path / "manifest.json").exists()
+            assert (installed_path / "script.py").exists()
-            try:
-                service.install_addon(str(zip_path))
-                installed_path = service.addons_dir / "test.addon.root"
-                if installed_path.exists() and (installed_path / "manifest.json").exists():
-                    print("SUCCESS: Root addon installed.")
-                else:
-                    print("FAILURE: Root addon failed.")
-            except Exception as e:
-                print(f"FAILURE: Root install raised exception: {e}")
+            service.install_addon(str(zip_path))
+            installed_path = service.addons_dir / "test.addon.root"
+            assert installed_path.exists()
+            assert (installed_path / "manifest.json").exists()
src/switchcraft/gui_modern/views/winget_view.py (2)

360-365: Localize the new loading text.

The new “Loading package details...” string is hardcoded. Consider an i18n key for consistent localization.

💡 Example
-                loading_area.controls.append(ft.Text("Loading package details...", color="GREY_500", italic=True))
+                loading_area.controls.append(
+                    ft.Text(i18n.get("winget_loading_details") or "Loading package details...", color="GREY_500", italic=True)
+                )

742-758: Remove the duplicate right_pane.update() call.

You update the same container twice back-to-back; consider keeping a single update to avoid redundant refresh work.

♻️ Suggested cleanup
-        try:
-            # Then update right pane container - CRITICAL for visibility
-            self.right_pane.update()
-            logger.debug("right_pane.update() called successfully")
-        except Exception as ex:
-            logger.error(f"Error updating right_pane: {ex}", exc_info=True)
+        # right_pane.update() already called above; keep a single update to reduce redundant refreshes.

Comment on lines +81 to +88
INLINE_REVIEW_COMMENTS=$(gh api "repos/$REPO/pulls/$PR_NUMBER/comments" --jq '[.[] | select(.position != null or .original_position != null) | select(.user.login != "github-actions[bot]")] | length')
# Check for CodeRabbit review comments specifically (both inline and general)
CODERABBIT_INLINE=$(gh api "repos/$REPO/pulls/$PR_NUMBER/comments" --jq '[.[] | select(.user.login == "coderabbitai[bot]" and (.position != null or .original_position != null))] | length')
# Also check for CodeRabbit review comments in PR comments (not just inline)
CODERABBIT_PR_COMMENTS=$(gh api "repos/$REPO/issues/$PR_NUMBER/comments" --jq '[.[] | select(.user.login == "coderabbitai[bot]" and (.body | test("suggestion|review|feedback|issue|problem|error|warning"; "i")))] | length')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n .github/workflows/review-auto-merge.yml | sed -n '75,95p'

Repository: FaserF/SwitchCraft

Length of output: 1890


🏁 Script executed:

cat -n .github/workflows/review-auto-merge.yml | sed -n '70,130p'

Repository: FaserF/SwitchCraft

Length of output: 4632


🌐 Web query:

GitHub CLI gh api pagination default behavior --paginate flag

💡 Result:

Short answer: by default gh api does NOT paginate — it returns the single page from the API. Use --paginate to have gh automatically follow pages until there are no more; that will issue additional requests and output each page sequentially (multiple JSON documents). Add --slurp to combine pages into one outer JSON array. [1][2]

Sources:

  • gh api manual (describes default and --paginate / --slurp behavior). [1]
  • GH CLI discussion/issue noting --paginate returns multiple JSON documents and recommending --jq or --slurp to combine. [2]

🏁 Script executed:

# Test the jq aggregation syntax proposed in the fix
echo '[{"id": 1}, {"id": 2}]' | jq '.[]' | jq -s '[.[] | select(.id > 0)] | length'

Repository: FaserF/SwitchCraft

Length of output: 63


Add pagination to comment API calls to prevent undercounting.

GitHub CLI's gh api returns only the first page (30 items) by default. If a PR has more than 30 comments, or if CodeRabbit feedback appears beyond the first page, the review comment detection can return zero and allow auto-merge to proceed incorrectly. Add --paginate and aggregate results across pages.

🔧 Suggested fix
-INLINE_REVIEW_COMMENTS=$(gh api "repos/$REPO/pulls/$PR_NUMBER/comments" --jq '[.[] | select(.position != null or .original_position != null) | select(.user.login != "github-actions[bot]")] | length')
+INLINE_REVIEW_COMMENTS=$(gh api --paginate "repos/$REPO/pulls/$PR_NUMBER/comments" --jq '.[]' | jq -s '[.[] | select(.position != null or .original_position != null) | select(.user.login != "github-actions[bot]")] | length')

-CODERABBIT_INLINE=$(gh api "repos/$REPO/pulls/$PR_NUMBER/comments" --jq '[.[] | select(.user.login == "coderabbitai[bot]" and (.position != null or .original_position != null))] | length')
+CODERABBIT_INLINE=$(gh api --paginate "repos/$REPO/pulls/$PR_NUMBER/comments" --jq '.[]' | jq -s '[.[] | select(.user.login == "coderabbitai[bot]" and (.position != null or .original_position != null))] | length')

-CODERABBIT_PR_COMMENTS=$(gh api "repos/$REPO/issues/$PR_NUMBER/comments" --jq '[.[] | select(.user.login == "coderabbitai[bot]" and (.body | test("suggestion|review|feedback|issue|problem|error|warning"; "i")))] | length')
+CODERABBIT_PR_COMMENTS=$(gh api --paginate "repos/$REPO/issues/$PR_NUMBER/comments" --jq '.[]' | jq -s '[.[] | select(.user.login == "coderabbitai[bot]" and (.body | test("suggestion|review|feedback|issue|problem|error|warning"; "i")))] | length')
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
INLINE_REVIEW_COMMENTS=$(gh api "repos/$REPO/pulls/$PR_NUMBER/comments" --jq '[.[] | select(.position != null or .original_position != null) | select(.user.login != "github-actions[bot]")] | length')
# Check for CodeRabbit review comments specifically (both inline and general)
CODERABBIT_INLINE=$(gh api "repos/$REPO/pulls/$PR_NUMBER/comments" --jq '[.[] | select(.user.login == "coderabbitai[bot]" and (.position != null or .original_position != null))] | length')
# Also check for CodeRabbit review comments in PR comments (not just inline)
CODERABBIT_PR_COMMENTS=$(gh api "repos/$REPO/issues/$PR_NUMBER/comments" --jq '[.[] | select(.user.login == "coderabbitai[bot]" and (.body | test("suggestion|review|feedback|issue|problem|error|warning"; "i")))] | length')
INLINE_REVIEW_COMMENTS=$(gh api --paginate "repos/$REPO/pulls/$PR_NUMBER/comments" --jq '.[]' | jq -s '[.[] | select(.position != null or .original_position != null) | select(.user.login != "github-actions[bot]")] | length')
# Check for CodeRabbit review comments specifically (both inline and general)
CODERABBIT_INLINE=$(gh api --paginate "repos/$REPO/pulls/$PR_NUMBER/comments" --jq '.[]' | jq -s '[.[] | select(.user.login == "coderabbitai[bot]" and (.position != null or .original_position != null))] | length')
# Also check for CodeRabbit review comments in PR comments (not just inline)
CODERABBIT_PR_COMMENTS=$(gh api --paginate "repos/$REPO/issues/$PR_NUMBER/comments" --jq '.[]' | jq -s '[.[] | select(.user.login == "coderabbitai[bot]" and (.body | test("suggestion|review|feedback|issue|problem|error|warning"; "i")))] | length')
🤖 Prompt for AI Agents
In @.github/workflows/review-auto-merge.yml around lines 81 - 88, The gh api
calls that compute INLINE_REVIEW_COMMENTS, CODERABBIT_INLINE, and
CODERABBIT_PR_COMMENTS need pagination to avoid undercounting; update each gh
api invocation used to populate INLINE_REVIEW_COMMENTS, CODERABBIT_INLINE and
CODERABBIT_PR_COMMENTS to include the --paginate flag so results are aggregated
across all pages (keeping the same --jq filters), ensuring the final counts
reflect all pages of pull request review comments and issue comments.

Comment on lines +42 to +49
try:
import traceback as tb
from switchcraft.gui_modern.views.crash_view import CrashDumpView

# Get traceback
tb_str = tb.format_exc()
if context:
tb_str = f"Context: {context}\n\n{tb_str}"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the complete function context
head -100 src/switchcraft/gui_modern/app.py | tail -60

Repository: FaserF/SwitchCraft

Length of output: 3043


🏁 Script executed:

# Let's find the complete _show_runtime_error function
rg -A 30 "def _show_runtime_error" src/switchcraft/gui_modern/app.py

Repository: FaserF/SwitchCraft

Length of output: 1282


🏁 Script executed:

# Look for all calls to _show_runtime_error to understand when it's invoked
rg "_show_runtime_error" src/switchcraft/gui_modern/

Repository: FaserF/SwitchCraft

Length of output: 299


🏁 Script executed:

# Let me search for calls to _show_runtime_error more comprehensively
rg -B 3 -A 3 "_show_runtime_error\(" src/switchcraft/

Repository: FaserF/SwitchCraft

Length of output: 559


🏁 Script executed:

# Also check if it's a method defined on a class or static method
rg -B 2 "def _show_runtime_error" src/switchcraft/gui_modern/app.py

Repository: FaserF/SwitchCraft

Length of output: 163


🏁 Script executed:

# Search for all calls to _show_runtime_error - it's a static method so could be called as ModernApp._show_runtime_error or similar
rg "show_runtime_error" src/switchcraft/ -B 2 -A 2

Repository: FaserF/SwitchCraft

Length of output: 880


🏁 Script executed:

# Let me check the entire file structure to understand the class better
rg -B 5 "@staticmethod" src/switchcraft/gui_modern/app.py | head -30

Repository: FaserF/SwitchCraft

Length of output: 196


🏁 Script executed:

# Search for actual invocations - look for ModernApp._show_runtime_error or just _show_runtime_error with context
rg "ModernApp\._show_runtime_error|_show_runtime_error\(" src/switchcraft/ --type py

Repository: FaserF/SwitchCraft

Length of output: 178


🏁 Script executed:

# Let's also check git history to see if there are any error handling wrappers or error callbacks
git log --oneline -n 20 -- src/switchcraft/gui_modern/app.py

Repository: FaserF/SwitchCraft

Length of output: 86


🏁 Script executed:

# Check if there's an error handler callback that might call this
rg "on_error|error_handler|exception" src/switchcraft/gui_modern/app.py -B 2 -A 2 | head -50

Repository: FaserF/SwitchCraft

Length of output: 2573


🏁 Script executed:

# Let's check the entire git diff for this commit to see context of when this was added
git show ab9b527 -- src/switchcraft/gui_modern/app.py | head -200

Repository: FaserF/SwitchCraft

Length of output: 8541


🏁 Script executed:

# Let's check how CrashDumpView is used and if it receives traceback_str
rg -B 5 -A 10 "class CrashDumpView" src/switchcraft/

Repository: FaserF/SwitchCraft

Length of output: 1838


🏁 Script executed:

# Let's check if there are any event handlers or callbacks that might invoke _show_runtime_error
rg "page.on_error|try:|except" src/switchcraft/gui_modern/app.py -B 3 -A 3 | head -100

Repository: FaserF/SwitchCraft

Length of output: 3994


🏁 Script executed:

# Check the CrashDumpView implementation to see how it uses traceback_str
cat src/switchcraft/gui_modern/views/crash_view.py | head -80

Repository: FaserF/SwitchCraft

Length of output: 4067


🏁 Script executed:

# Let me verify if error objects passed to this function would have __traceback__ attributes
python3 << 'EOF'
import traceback
import sys

# Simulate a typical calling pattern
def test_format_exc():
    """Test format_exc outside of an exception context"""
    try:
        raise ValueError("Test error")
    except ValueError as e:
        # This works because we're in an except block
        exc_outside = e
        formatted = traceback.format_exc()
        print("=== format_exc() inside except block ===")
        print(formatted[:100])
        
    # Now outside the except block - what does format_exc return?
    print("\n=== format_exc() outside except block ===")
    formatted_outside = traceback.format_exc()
    print(f"Result: {repr(formatted_outside)}")
    
    # But the error object still has __traceback__
    print(f"\n=== error.__traceback__ exists: {exc_outside.__traceback__ is not None}")
    
    # Using format_exception with the error
    print("\n=== format_exception with error object ===")
    formatted_proper = "".join(traceback.format_exception(type(exc_outside), exc_outside, exc_outside.__traceback__))
    print(formatted_proper[:100])

test_format_exc()
EOF

Repository: FaserF/SwitchCraft

Length of output: 456


🏁 Script executed:

# Let me search for where this function might be called or if there's a pattern where it's used
rg "except|try:" src/switchcraft/gui_modern/app.py | grep -A 5 -B 5 "_show_runtime_error"

Repository: FaserF/SwitchCraft

Length of output: 44


🏁 Script executed:

# Check if there's any page.on_error handler setup
rg "on_error|page\." src/switchcraft/gui_modern/app.py | head -30

Repository: FaserF/SwitchCraft

Length of output: 1821


🏁 Script executed:

# Let me check if there's a setup_page method or any event handlers
rg "def setup_page" src/switchcraft/gui_modern/app.py -A 30

Repository: FaserF/SwitchCraft

Length of output: 1301


🏁 Script executed:

# Check if _show_runtime_error is perhaps being set as a callback or referenced as a string
rg "show_runtime_error" src/switchcraft/ -i

Repository: FaserF/SwitchCraft

Length of output: 299


🏁 Script executed:

# Let me check the entire file to see if there's any page-level error handler or callback registration
wc -l src/switchcraft/gui_modern/app.py

Repository: FaserF/SwitchCraft

Length of output: 100


🏁 Script executed:

# Check for any page callbacks or event handlers that might indirectly call this
rg "page\." src/switchcraft/gui_modern/app.py | grep -E "callback|handler|on_" | head -20

Repository: FaserF/SwitchCraft

Length of output: 662


🏁 Script executed:

# Let me check if there are any tests or examples calling this function
find src/switchcraft -name "*test*" -type f

Repository: FaserF/SwitchCraft

Length of output: 119


🏁 Script executed:

# Check the modern_main.py to see how ModernApp is instantiated
rg "ModernApp" src/switchcraft/ -B 2 -A 2

Repository: FaserF/SwitchCraft

Length of output: 4806


Use the passed exception's traceback instead of format_exc().

format_exc() depends on an active exception context; since this helper receives error directly, it should format that instead. Without an active exception, the call returns "NoneType: None", losing the passed error's traceback.

💡 Suggested fix
-            tb_str = tb.format_exc()
+            if error is not None and getattr(error, "__traceback__", None) is not None:
+                tb_str = "".join(tb.format_exception(type(error), error, error.__traceback__))
+            else:
+                tb_str = tb.format_exc()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try:
import traceback as tb
from switchcraft.gui_modern.views.crash_view import CrashDumpView
# Get traceback
tb_str = tb.format_exc()
if context:
tb_str = f"Context: {context}\n\n{tb_str}"
try:
import traceback as tb
from switchcraft.gui_modern.views.crash_view import CrashDumpView
# Get traceback
if error is not None and getattr(error, "__traceback__", None) is not None:
tb_str = "".join(tb.format_exception(type(error), error, error.__traceback__))
else:
tb_str = tb.format_exc()
if context:
tb_str = f"Context: {context}\n\n{tb_str}"
🤖 Prompt for AI Agents
In `@src/switchcraft/gui_modern/app.py` around lines 42 - 49, The code uses
traceback.format_exc() which relies on an active exception context; instead
format the passed-in exception object `error` in the helper (where `tb_str` is
created). Replace tb.format_exc() with formatting of the exception, e.g. use
tb.format_exception(type(error), error, error.__traceback__) (or
tb.TracebackException.from_exception(error).format()) and join the result into a
single string assigned to `tb_str`, preserving the existing `if context:` prefix
logic and leaving CrashDumpView usage unchanged.

Comment on lines +273 to 283
# Filter files based on search
filtered_files = []
for item in self.all_files:
name = item.get('filename', '').lower()
if not self.search_val or self.search_val in name:
filtered_files.append(item)

# Add tiles for filtered files
for item in filtered_files:
self.grid.controls.append(self._create_tile(item))

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Show an empty-state when the search filters out all items.

Currently, a search with zero matches leaves a blank grid (no message), which is confusing. Consider showing a “No results” message when filtered_files is empty.

💡 Suggested fix
-            # Add tiles for filtered files
-            for item in filtered_files:
-                self.grid.controls.append(self._create_tile(item))
+            if not filtered_files:
+                self.grid.controls.append(
+                    ft.Text(i18n.get("no_search_results") or "No results found.", color="GREY_500")
+                )
+            else:
+                for item in filtered_files:
+                    self.grid.controls.append(self._create_tile(item))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Filter files based on search
filtered_files = []
for item in self.all_files:
name = item.get('filename', '').lower()
if not self.search_val or self.search_val in name:
filtered_files.append(item)
# Add tiles for filtered files
for item in filtered_files:
self.grid.controls.append(self._create_tile(item))
# Filter files based on search
filtered_files = []
for item in self.all_files:
name = item.get('filename', '').lower()
if not self.search_val or self.search_val in name:
filtered_files.append(item)
if not filtered_files:
self.grid.controls.append(
ft.Text(i18n.get("no_search_results") or "No results found.", color="GREY_500")
)
else:
for item in filtered_files:
self.grid.controls.append(self._create_tile(item))
🤖 Prompt for AI Agents
In `@src/switchcraft/gui_modern/views/library_view.py` around lines 273 - 283, The
grid currently shows nothing when a search yields zero matches; after computing
filtered_files from self.all_files using self.search_val, detect if
filtered_files is empty and append a suitable "no results" control to
self.grid.controls instead of tiles (e.g., a centered label/container with "No
results" or similar). Update the block that iterates over filtered_files and the
call to self._create_tile(item) so that when filtered_files is empty you create
and append the empty-state control (and skip creating tiles) to provide clear
feedback to the user.

Comment on lines +714 to +719
# Immediate visual feedback
if hasattr(self, 'login_btn'):
self.login_btn.text = "Starting..."
self.login_btn.icon = ft.Icons.HOURGLASS_EMPTY
self.login_btn.update()

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Reset the login button state on failure.
The “Starting…” label/icon is set but never restored on init/poll failures, leaving the button stuck. Consider restoring in the failure paths and after polling completes.

🤖 Prompt for AI Agents
In `@src/switchcraft/gui_modern/views/settings_view.py` around lines 714 - 719,
The login button is set to "Starting..." with ft.Icons.HOURGLASS_EMPTY in the
block that references self.login_btn but never restored on init/poll failures;
update the failure and completion paths to reset the button state. Specifically,
in the init routine(s) and any poll/error callbacks that handle initialization
or polling failures (the methods that call or wrap the block setting
self.login_btn.text/icon), ensure you set self.login_btn.text back to the
original label, restore the original icon (or None), and call
self.login_btn.update() on both error paths and on successful completion of
polling so the button is never left stuck in the "Starting..." state.

Comment on lines +1059 to +1061
"""
logger.info(f"Language change requested: {val}")
from switchcraft.utils.config import SwitchCraftConfig
from switchcraft.utils.i18n import i18n

# Save preference
SwitchCraftConfig.set_user_preference("Language", val)
logger.debug(f"Language preference saved: {val}")

# Actually update the i18n singleton
i18n.set_language(val)
logger.debug(f"i18n language updated: {val}")

# Immediately refresh the current view to apply language change
# Get current tab index and reload the view
if hasattr(self.app_page, 'switchcraft_app'):
app = self.app_page.switchcraft_app
current_idx = getattr(app, '_current_tab_index', 0)
logger.debug(f"Current app_page: {getattr(self, 'app_page', 'Not Set')}, type: {type(getattr(self, 'app_page', None))}")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Remove the stray triple‑quote that swallows the method body.
Line 1059 starts a new multiline string, which makes most of _on_lang_change inert until the next """.

🐛 Proposed fix
-        """
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"""
logger.info(f"Language change requested: {val}")
from switchcraft.utils.config import SwitchCraftConfig
from switchcraft.utils.i18n import i18n
# Save preference
SwitchCraftConfig.set_user_preference("Language", val)
logger.debug(f"Language preference saved: {val}")
# Actually update the i18n singleton
i18n.set_language(val)
logger.debug(f"i18n language updated: {val}")
# Immediately refresh the current view to apply language change
# Get current tab index and reload the view
if hasattr(self.app_page, 'switchcraft_app'):
app = self.app_page.switchcraft_app
current_idx = getattr(app, '_current_tab_index', 0)
logger.debug(f"Current app_page: {getattr(self, 'app_page', 'Not Set')}, type: {type(getattr(self, 'app_page', None))}")
logger.info(f"Language change requested: {val}")
logger.debug(f"Current app_page: {getattr(self, 'app_page', 'Not Set')}, type: {type(getattr(self, 'app_page', None))}")
🤖 Prompt for AI Agents
In `@src/switchcraft/gui_modern/views/settings_view.py` around lines 1059 - 1061,
There's a stray opening triple-quote that is turning the body of _on_lang_change
into a multiline string; remove the unintended triple-quote so the subsequent
lines (logger.info and logger.debug) and the rest of the method become
executable again, ensuring the method _on_lang_change contains the
logger.info(f"Language change requested: {val}") and logger.debug(...) calls as
real statements rather than part of a string.

Comment on lines +162 to +205
manifest_path = None

# TODO: Support nested, but let's strict for now
if not valid:
raise Exception("Invalid addon: manifest.json missing from root")
# Check for manifest.json at root first
if "manifest.json" in files:
manifest_path = "manifest.json"
else:
# Check for manifest.json in common subdirectories
# Some ZIPs might have it in a subfolder
for file_path in files:
# Normalize path separators
normalized = file_path.replace('\\', '/')
# Check if it's manifest.json in a subdirectory (root already checked above)
if normalized.endswith('/manifest.json'):
# Accept subdirectory manifest if root not found
if manifest_path is None:
manifest_path = file_path

if not manifest_path:
# Fallback: Recursive search (max depth 2)
for file_in_zip in files:
parts = file_in_zip.replace('\\', '/').split('/')
if len(parts) <= 3 and parts[-1].lower() == 'manifest.json':
manifest_path = file_in_zip
break

if not manifest_path:
# Provide helpful error message
root_files = [f for f in files if '/' not in f.replace('\\', '/') or f.replace('\\', '/').count('/') == 0]
raise Exception(
f"Invalid addon: manifest.json not found in ZIP archive.\n"
f"The addon ZIP must contain a manifest.json file at the root level.\n"
f"Root files found: {', '.join(root_files[:10]) if root_files else 'none'}"
)

# Check ID from manifest (use found path, not hardcoded)
with z.open(manifest_path) as f:
try:
data = json.load(f)
except json.JSONDecodeError as e:
raise Exception(f"Invalid manifest.json: JSON parse error - {e}")

# Check ID from manifest
with z.open("manifest.json") as f:
data = json.load(f)
addon_id = data.get("id")
if not addon_id:
raise Exception("Invalid manifest: missing id")
raise Exception("Invalid manifest.json: missing required field 'id'")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Nested manifest installs still extract into a subfolder.

You now accept manifest.json in a subdirectory, but extraction preserves the original path. That leaves manifest.json (and entry points) under target/<subdir>/..., while read_manifest() expects it at target/manifest.json. This will still break addon discovery and the new tests.

🛠️ Suggested fix: rebase extraction to the manifest directory (and update the error message)
-                if not manifest_path:
+                if not manifest_path:
                     # Provide helpful error message
                     root_files = [f for f in files if '/' not in f.replace('\\', '/') or f.replace('\\', '/').count('/') == 0]
                     raise Exception(
                         f"Invalid addon: manifest.json not found in ZIP archive.\n"
-                        f"The addon ZIP must contain a manifest.json file at the root level.\n"
+                        f"The addon ZIP must contain a manifest.json file at the root level or inside a single top-level folder.\n"
                         f"Root files found: {', '.join(root_files[:10]) if root_files else 'none'}"
                     )

                 # Secure extraction
                 # If manifest was in a subdirectory, we need to handle path normalization
                 target_resolved = target.resolve()
+                base_prefix = Path(manifest_path).parent.as_posix()
+                if base_prefix in ("", "."):
+                    base_prefix = None
                 for member in z.infolist():
                     # Normalize path separators for cross-platform compatibility
                     normalized_name = member.filename.replace('\\', '/')
+                    if base_prefix:
+                        if not normalized_name.startswith(f"{base_prefix}/"):
+                            continue
+                        normalized_name = normalized_name[len(base_prefix) + 1:]
+                        if not normalized_name:
+                            continue

                     # Resolve the target path for this member
                     file_path = (target / normalized_name).resolve()

Also applies to: 215-241

🤖 Prompt for AI Agents
In `@src/switchcraft/services/addon_service.py` around lines 162 - 205, The
manifest search currently accepts a nested manifest_path but extraction still
preserves the ZIP subfolder, so read_manifest() fails expecting
target/manifest.json; update the extraction logic where files are written (the
code that uses manifest_path and extracts ZIP entries) to compute a manifest_dir
= os.path.dirname(manifest_path.replace('\\','/')) and, if manifest_dir is
non-empty, strip that leading prefix from every extracted file path (rebase
extraction into manifest_dir) so manifest.json and entry points end up at
target/manifest.json; also update the user-facing Exception message that lists
root_files to indicate if manifest was found under a subdirectory and show that
subdirectory; apply the same change to the other extraction branch referenced
(the block around lines 215-241) so both paths normalize extraction to the
manifest directory.

Comment on lines +141 to +145
# Wait a bit for background thread to start
time.sleep(0.1)

# Verify that _load_data was triggered (check if dir_info was updated or grid was refreshed)
assert True, "Refresh button should trigger data load"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Avoid no-op assertions that always pass.

assert True and ... or True make these tests succeed even if the behavior breaks. Consider asserting a concrete effect (e.g., patch _load_data before view creation and assert it was called; or assert goto_tab/language setter was invoked).

Also applies to: 351-351

🤖 Prompt for AI Agents
In `@tests/test_critical_ui_fixes.py` around lines 141 - 145, The test currently
contains a no-op assertion ("assert True") which masks failures; replace it with
a concrete assertion that verifies the refresh action triggered data loading:
patch or mock the _load_data method before creating the view (or attach a spy),
trigger the refresh button, wait for the background thread, and assert that
_load_data was called (or assert that dir_info or the grid state changed
accordingly). Alternatively, assert that helper actions like goto_tab or the
language setter were invoked by mocking those methods and checking their call
state instead of using assert True.

Comment on lines +13 to +35
try:
from conftest import is_ci_environment, skip_if_ci, poll_until, mock_page
except ImportError:
def is_ci_environment():
return (
os.environ.get('CI') == 'true' or
os.environ.get('GITHUB_ACTIONS') == 'true' or
os.environ.get('GITHUB_RUN_ID') is not None
)
def skip_if_ci(reason="Test not suitable for CI environment"):
if is_ci_environment():
pytest.skip(reason)
@pytest.fixture
def mock_page():
page = MagicMock(spec=ft.Page)
page.dialog = None
page.update = MagicMock()
page.snack_bar = MagicMock(spec=ft.SnackBar)
page.snack_bar.open = False
page.open = MagicMock()
page.run_task = lambda func: func()
return page

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fallback block defines no poll_until, causing NameError if conftest import fails.
Add a minimal fallback implementation alongside skip_if_ci and mock_page.

🐛 Proposed fix
     def skip_if_ci(reason="Test not suitable for CI environment"):
         if is_ci_environment():
             pytest.skip(reason)
     `@pytest.fixture`
     def mock_page():
         page = MagicMock(spec=ft.Page)
         page.dialog = None
         page.update = MagicMock()
         page.snack_bar = MagicMock(spec=ft.SnackBar)
         page.snack_bar.open = False
         page.open = MagicMock()
         page.run_task = lambda func: func()
         return page
+    def poll_until(condition, timeout=2.0, interval=0.05):
+        start = time.time()
+        while time.time() - start < timeout:
+            if condition():
+                return True
+            time.sleep(interval)
+        return False
🤖 Prompt for AI Agents
In `@tests/test_github_login_integration.py` around lines 13 - 35, The fallback
import block is missing a poll_until implementation which will raise NameError
if conftest import fails; add a minimal poll_until function alongside
is_ci_environment, skip_if_ci and mock_page that accepts a predicate (callable),
timeout and interval parameters, polls the predicate until it returns True or
the timeout elapses, and raises a TimeoutError (or returns False) on timeout so
tests depending on poll_until behave consistently; ensure the symbol name is
poll_until and its signature matches how tests call it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend ci-cd documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants