Skip to content

Release versi#43

Merged
AlanRockefeller merged 8 commits intomainfrom
test
Feb 16, 2026
Merged

Release versi#43
AlanRockefeller merged 8 commits intomainfrom
test

Conversation

@AlanRockefeller
Copy link
Copy Markdown
Owner

@AlanRockefeller AlanRockefeller commented Feb 16, 2026

Summary by CodeRabbit

  • New Features

    • Full Screen Mode: Press F11 to toggle fullscreen in loupe view for immersive editing.
    • Spark Line Display: Grid view now shows upload progress indicators per folder.
  • Improvements

    • Enhanced keyboard shortcuts: Esc now exits fullscreen and closes dialogs/editors.
    • Optimized grid view performance and prefetch behavior.
  • Bug Fixes

    • Fixed race condition in temporary file cleanup during shutdown.
    • Improved cache eviction and lock handling.
  • Dependencies

    • Updated OpenCV to 4.10+ for NumPy 2.0 compatibility.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 16, 2026

Walkthrough

This PR introduces a comprehensive variant management system for image files (developed/backup variants), adds full-screen loupe mode with F11 support and spark line progress visualization, enhances thumbnail prefetching with detailed timing/trace debugging, improves cache eviction to prevent lock contention, and includes extensive test coverage for new functionality. Version bumped to 1.5.9.

Changes

Cohort / File(s) Summary
Variant System Foundation
faststack/io/variants.py, faststack/io/indexer.py, faststack/models.py
New module for parsing and grouping image variants (developed, backup, main); refactored indexer to use variant-aware filtering and tagging of images with has_backups/has_developed flags.
Core App Variant Support
faststack/app.py, faststack/imaging/orientation.py, faststack/imaging/editor.py, faststack/imaging/prefetch.py
AppController now tracks variant overrides, computes variant badges, and routes decoding/saving through variant-aware paths; orientation and prefetch modules adapted to accept optional override paths; editor extended with save_target_path for variant outputs.
Full-Screen Loupe & UI Mode
faststack/qml/Main.qml, faststack/qml/Components.qml, faststack/qml/ThumbnailGridView.qml, faststack/qml/ThumbnailTile.qml, faststack/qml/ThumbnailGridView.qml
Main.qml adds fullscreen loupe toggling (F11), window geometry preservation, and prefetch gating on view switch; Components.qml routes Escape to exit fullscreen; grid and tile QML updated for variant badge display and prefetch control.
Thumbnail System & Variant Badges
faststack/thumbnail_view/model.py, faststack/thumbnail_view/prefetcher.py, faststack/thumbnail_view/provider.py
ThumbnailModel exposes has_backups/has_developed roles; prefetcher and provider extended with debug_timing and debug_trace flags for instrumentation; thumbnail sources now include reason parameter for tracing.
Cache Eviction & Lock Safety
faststack/imaging/cache.py, faststack/util/executors.py
ByteLRUCache refactored to use byte-aware sizing, deferred eviction callbacks to prevent reentrancy, and RLock for thread safety; DaemonThreadPoolExecutor introduced for daemon worker threads.
UI State & Prefetch Control
faststack/ui/provider.py
UIState adds variant-related signals (variantBadgesChanged, variantSaveHintChanged), clock-based prefetch deduplication (~30ms cooldown), prefetch budgeting with hard cap, and currentImageSource returns empty string during grid view to avoid full-res requests.
Debug & Instrumentation Utilities
faststack/util/thumb_debug.py, faststack/check_daemon.py, faststack/debug_path_norm.py, faststack/repro_cache_lock.py, faststack/verify_cache_fix.py
New thumb_debug module provides timing/trace logging for thumbnail pipeline with RID tracking and periodic summaries; helper scripts for daemon checking, path normalization, and cache lock verification.
Version, Dependencies & Config
pyproject.toml, README.md, .gitignore, ChangeLog.md
Bumps version to 1.5.9; upgrades OpenCV to >=4.10.0 for NumPy 2.0 compatibility; updates pytest config with minversion 8.0 and --import-mode=importlib; documents full-screen mode and spark line features.
Removed Test Files
faststack/test_executors.py, faststack/reactive_test_output.txt, tests/debug_import.py, tests/repro_exif_fix.py, tests/test_highlights_v2.py, tests/verify_manual.py, tests/verify_raw_mode.py
Deleted obsolete or manual test files and artifact; no functional impact.
New Test Coverage
faststack/tests/test_feedback_fixes.py, faststack/tests/test_helicon_cleanup.py, faststack/tests/test_startup_opt.py, faststack/tests/test_startup_optimization.py, faststack/tests/test_ui_prefetch_safety.py, faststack/tests/test_variants.py, faststack/tests/test_variants_logic.py, faststack/tests/thumbnail_view/test_thumbnail_ready.py
Added comprehensive test coverage for variant logic, cache eviction callbacks, startup optimization, prefetch budgeting/deduplication, and thumbnail readiness signaling.
Updated Tests & Helpers
faststack/tests/debug_editor_error.py, faststack/tests/repro_futures_cleanup.py, faststack/tests/test_exif_display_rotation.py, faststack/tests/test_handle_failures_isolated.py, faststack/tests/test_refresh_crash.py, faststack/tests/test_thumbnail_ready_emits_datachanged.py
Refactored test mocking strategies, fixed import paths, converted fixtures to generators, added QCoreApplication fixture for Qt tests, and simplified test assertions.
Implementation Notes
implementation_summary.md
Documents bug fixes: _loupe_decode_allowed gating in prefetch, stale pending_prefetch_index cleanup on grid view enter, UIState signal connection optimization, and ThumbnailGridView prefetch gating robustness.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant UI as QML (Main.qml)
    participant AppCtrl as AppController
    participant Prefetch as Prefetcher
    participant Editor as ImageEditor

    User->>UI: Click variant badge or setVariantOverride()
    UI->>AppCtrl: set_variant_override(path_str)
    activate AppCtrl
    AppCtrl->>AppCtrl: _variant_overrides[current_index] = path
    AppCtrl->>AppCtrl: get_variant_badges() for UI update
    AppCtrl-->>UI: variantBadgesChanged signal
    deactivate AppCtrl
    
    User->>UI: Trigger save (Ctrl+S)
    UI->>AppCtrl: save_image() via signal
    activate AppCtrl
    AppCtrl->>AppCtrl: _get_save_target_path_for_current_view()
    note over AppCtrl: Returns main, developed, or backup path<br/>based on variant override
    AppCtrl->>Editor: save_image(save_target_path=computed_path)
    activate Editor
    Editor->>Editor: resolve original_path from save_target_path
    Editor->>Editor: compute rotation/straighten from edits
    Editor->>Editor: write JPEG/PNG to save_target_path
    Editor-->>AppCtrl: returns (path, developed_path) or error
    deactivate Editor
    AppCtrl->>Prefetch: decode override_path for display
    activate Prefetch
    Prefetch->>Prefetch: submit_task(override_path=computed_path)
    Prefetch-->>AppCtrl: future with decoded image
    deactivate Prefetch
    AppCtrl-->>UI: image_changed signal with new decoded data
    deactivate AppCtrl
    UI->>UI: Update loupe display
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 1 | ❌ 3

❌ Failed checks (2 warnings, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 71.68% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Merge Conflict Detection ⚠️ Warning ❌ Merge conflicts detected (26 files):

⚔️ .gitignore (content)
⚔️ ChangeLog.md (content)
⚔️ README.md (content)
⚔️ faststack/app.py (content)
⚔️ faststack/imaging/cache.py (content)
⚔️ faststack/imaging/editor.py (content)
⚔️ faststack/imaging/orientation.py (content)
⚔️ faststack/imaging/prefetch.py (content)
⚔️ faststack/io/indexer.py (content)
⚔️ faststack/models.py (content)
⚔️ faststack/qml/Components.qml (content)
⚔️ faststack/qml/Main.qml (content)
⚔️ faststack/qml/ThumbnailGridView.qml (content)
⚔️ faststack/qml/ThumbnailTile.qml (content)
⚔️ faststack/tests/debug_editor_error.py (content)
⚔️ faststack/tests/repro_futures_cleanup.py (content)
⚔️ faststack/tests/test_exif_display_rotation.py (content)
⚔️ faststack/tests/test_handle_failures_isolated.py (content)
⚔️ faststack/tests/test_refresh_crash.py (content)
⚔️ faststack/tests/test_thumbnail_ready_emits_datachanged.py (content)
⚔️ faststack/thumbnail_view/model.py (content)
⚔️ faststack/thumbnail_view/prefetcher.py (content)
⚔️ faststack/thumbnail_view/provider.py (content)
⚔️ faststack/ui/provider.py (content)
⚔️ faststack/util/executors.py (content)
⚔️ pyproject.toml (content)

These conflicts must be resolved before merging into main.
Resolve conflicts locally and push changes to this branch.
Title check ❓ Inconclusive The title 'Release versi' is incomplete and vague, appearing to be a truncated version string without clear context about the release scope or key changes. Complete the title to clearly describe the release (e.g., 'Release v1.5.9: Add variant support and fullscreen mode' or similar) to accurately reflect the changeset scope.
✅ 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
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch test
⚔️ Resolve merge conflicts (beta)
  • Auto-commit resolved conflicts to branch test
  • Create stacked PR with resolved conflicts
  • Post resolved changes as copyable diffs in a comment

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
Copy Markdown

@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: 15

Caution

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

⚠️ Outside diff range comments (2)
faststack/imaging/prefetch.py (1)

340-397: ⚠️ Potential issue | 🔴 Critical

TOCTOU: bounds check on stale snapshot, live access inside lock.

Lines 341-343 snapshot self.image_files and check bounds outside the lock, but line 383 accesses self.image_files[index] (the live reference) inside the lock. If set_image_files replaces self.image_files with a shorter list between lines 343 and 383, this will raise IndexError.

Move the snapshot and bounds check inside the lock, or re-check bounds on the live list inside the lock before accessing line 383.

Proposed fix
     def submit_task(
         self, index: int, generation: int, priority: bool = False, override_path: Optional[Path] = None
     ) -> Optional[Future]:
         """Submits a decoding task for a given index."""
         if self._stop_event.is_set():
             return None
 
-        # Capture list snapshot and check bounds before accessing path
-        image_files = self.image_files
-        if index < 0 or index >= len(image_files):
-            return None
-
-        requested_path = override_path if override_path is not None else image_files[index].path
-
         if self.debug and priority:
             _t_start = time.perf_counter()
             print(f"[DBGCACHE] {_t_start*1000:.3f} submit_task: PRIORITY index={index} gen={generation} override={override_path}")
 
         with self._futures_lock:
+            # Bounds check must happen inside the lock to stay consistent
+            # with self.image_files (which set_image_files can replace under lock).
+            if index < 0 or index >= len(self.image_files):
+                return None
+
+            requested_path = override_path if override_path is not None else self.image_files[index].path
+
             # We track by index. If we already have a job for this index,
             # we must cancel it if the requested path is different
             # (e.g. switching between main and variants).
faststack/app.py (1)

805-845: ⚠️ Potential issue | 🟠 Major

Fix load() signature mismatch with directory switching.
_switch_to_directory() still calls load(skip_thumbnail_refresh=True) but load() no longer accepts that parameter, which will raise a TypeError at runtime. Please reintroduce the parameter or remove the call‑site argument.

🧩 Proposed fix (restore param + guard refresh)
-    def load(self):
-        """Loads images, sidecar data, and starts services."""
+    def load(self, skip_thumbnail_refresh: bool = False):
+        """Loads images, sidecar data, and starts services."""
         # Reset instrumentation for this load operation
         self._scan_count_variant = 0
         self._grid_refreshes = 0
         self._grid_model_dirty = True
@@
-        if self._is_grid_view_active:
+        if self._is_grid_view_active and not skip_thumbnail_refresh:
             # Ensure grid model is populated if starting in grid mode
             if self._thumbnail_model and self._grid_model_dirty and self._thumbnail_model.rowCount() == 0:
                 self._grid_refreshes += 1
                 self._thumbnail_model.refresh_from_controller(self.image_files)
🤖 Fix all issues with AI agents
In `@faststack/app.py`:
- Around line 4738-4763: The _on_cache_evict callback currently declares
parameters key and value but doesn't use them, triggering a lint error; fix by
either renaming the parameters to _key and _value in the _on_cache_evict
signature to mark them intentionally unused, or incorporate them into the
warning/log and UI update (e.g., include key or a short value summary in the msg
passed to self.update_status_message and log.warning); adjust references inside
_on_cache_evict (and the lambda to self.update_status_message) accordingly so
the chosen approach is consistent.
- Around line 6908-6911: The global _debug_thumb_trace is declared but never
updated, so when the CLI flag --debug-thumbtrace is used the module-level
_debug_thumb_trace remains False; update the same block that sets _debug_mode
and _debug_thumb_timing to also set _debug_thumb_trace = debug_thumb_trace
(declared as global at top of that block) so the flag value is propagated to the
module state; ensure you reference the existing globals _debug_mode,
_debug_thumb_timing, and _debug_thumb_trace and use the incoming parameter name
debug_thumb_trace.

In `@faststack/check_daemon.py`:
- Around line 14-15: The module currently creates a ThreadPoolExecutor and
submits check_daemon at import time (using ThreadPoolExecutor, set_daemon, and
check_daemon); move that runtime behavior into a script entry point by wrapping
the executor block in an if __name__ == "__main__": guard so the
ThreadPoolExecutor(max_workers=1, initializer=set_daemon) creation and
executor.submit(check_daemon).result() call only run when the file is executed
as a script, preserving the same print behavior but preventing side effects on
import.

In `@faststack/debug_path_norm.py`:
- Around line 1-22: This debug script contains a duplicate norm_path function
and module-level print statements that run on import; remove this file
(faststack/debug_path_norm.py) from the PR or at minimum delete the debugging
code: eliminate the duplicate norm_path definition and all top-level print/test
code so the module has no side effects on import; if you want to keep the
behavior, move the example into a proper unit test or a __main__ guarded block
(if truly needed) and ensure only the canonical norm_path implementation remains
in its original module (norm_path symbol in faststack/io/variants.py).

In `@faststack/imaging/cache.py`:
- Around line 13-33: The fallback in get_decoded_image_size incorrectly assumes
4 channels because DecodedImage has no channels attribute; update the fallback
to compute bytes_per_pixel from available image metadata: if item has
bytes_per_line (or a similar attribute) and width, derive bytes_per_pixel =
bytes_per_line / width and use that (rounded or cast to int) as
channels/bytes-per-pixel, otherwise fall back to 4; adjust the branch inside
get_decoded_image_size that currently uses getattr(item, "channels", 4) to first
check for item.bytes_per_line and item.width and compute channels from that
before defaulting.

In `@faststack/io/indexer.py`:
- Around line 125-137: The timing variable elapsed in find_images is computed
but never logged; update the log.info call in function find_images to include
elapsed (e.g., append "in %.2fs" or similar) so the elapsed value is printed
alongside the image counts—refer to the existing log.info invocation that
currently logs len(image_files), paired_count, raw_only_count and add elapsed to
the message and its arguments to mirror the behavior in
find_images_with_variants.
- Around line 168-187: The comparison between img.path and group.main_path can
fail because group.main_path is normalized (Path(norm_path(...))) in
build_variant_map (variants.py) while img.path comes from _scan_directory and
may be relative; update the filter logic in indexer.py to compare normalized
paths: compute a normalized Path for img.path using the same norm_path helper
used in build_variant_map (or otherwise resolve/absolutize and casefold
consistently), then use that normalized value when checking equality against
group.main_path (refer to img.path, group.main_path, variant_map,
parse_variant_stem and JPG_EXTENSIONS to locate the code). Ensure you do not
mutate the original img.path unless intended.

In `@faststack/tests/test_handle_failures_isolated.py`:
- Around line 30-32: The two `@patch` decorators for confirm_permanent_delete and
confirm_batch_permanent_delete are applied bottom-up so the innermost
(confirm_batch_permanent_delete) is passed as the first argument to
test_handle_delete_failures_recycle_codes_isolation; rename the test parameters
to match that order (e.g., first param mock_confirm_batch_permanent_delete,
second param mock_confirm_permanent_delete) or swap their positions so the
parameter names align with the patched functions.

In `@faststack/tests/test_startup_opt.py`:
- Around line 83-85: Remove the two debug print statements around the
apply_filter call: delete the lines printing f"DEBUG: before apply_filter:
grid_active={controller._is_grid_view_active},
dirty={controller._grid_model_dirty}" and the matching "after" print; if you
need diagnostic output keep it via the test framework or use logging (e.g.,
replace prints with logger.debug) but do not leave raw print calls next to
controller.apply_filter("test", []).
- Around line 34-35: The test is asserting controller._scan_count_simple which
does not exist on AppController; remove the assertion line referencing
_scan_count_simple from faststack/tests/test_startup_opt.py so the test only
asserts the existing controller._scan_count_variant (and any other valid
attributes), or alternatively add initialization of _scan_count_simple in
AppController.__init__ if that counter is actually required; reference symbols:
controller, _scan_count_simple, _scan_count_variant, AppController.__init__.

In `@faststack/tests/test_ui_prefetch_safety.py`:
- Line 29: The test assigns self.model.thumbnail_size = (256, 256) but
ThumbnailModel.thumbnail_size is an int; change the assignment in the test to an
integer (e.g., 256) so the test matches the real property type and surfaces any
type-dependent logic (refer to ThumbnailModel.thumbnail_size and to the test's
use of prefetcher.submit which is mocked).

In `@faststack/tests/test_variants_logic.py`:
- Line 10: The comment "Cas 1: Pure Main exists" contains a typo; update that
comment in faststack/tests/test_variants_logic.py to read "Case 1: Pure Main
exists" (replace "Cas 1" with "Case 1") so the test comment is spelled
correctly.

In `@faststack/thumbnail_view/model.py`:
- Around line 147-156: Remove the duplicate initialization of the instance
variable _next_source_reason: locate the duplicate assignments to
_next_source_reason (both set to Optional[str] = None) in the class initializer
and delete the redundant second one so the variable is only initialized once;
ensure _id_to_row and surrounding comments remain unchanged and run tests/lint
to confirm no other references relied on the duplicate.

In `@faststack/ui/provider.py`:
- Around line 559-562: The variantSaveHint property calls
self.app_controller.get_variant_save_hint() unguarded and can raise
AttributeError; update the variantSaveHint getter to check
hasattr(self.app_controller, "get_variant_save_hint") and return a safe default
(e.g., "" or None) when missing, mirroring the pattern used by variantBadges;
ensure you keep the `@Property` decorator and notify=variantSaveHintChanged and
only call get_variant_save_hint() when the attribute exists.

In `@README.md`:
- Line 15: Replace the incorrect phrase "Speak Line" with "Spark Line" wherever
it appears (starting with the README entry that currently reads "**Speak Line**:
In grid view...") and also update any references in the PR description or AI
summary to use "Spark Line" so the terminology is consistent across docs.
🧹 Nitpick comments (37)
faststack/qml/Components.qml (1)

34-38: Guard against root being undefined.

If loupeView is ever instantiated outside the context where root is defined (e.g., during testing or if the component hierarchy changes), accessing root.fullScreenLoupe will throw a ReferenceError. Consider adding a null guard similar to the uiState && pattern used elsewhere.

-        if (root.fullScreenLoupe) {
-            root.exitFullScreenLoupe()
+        if (typeof root !== "undefined" && root && root.fullScreenLoupe) {
+            root.exitFullScreenLoupe()

Note: This is consistent with the defensive pattern already used on line 1266 (typeof root !== "undefined" && root).

.gitignore (1)

85-90: Overlapping glob patterns for test output files.

Line 85 (test_output*.txt) covers suffixed variants while line 90 (*test_output.txt) covers prefixed variants. These overlap on the exact filename test_output.txt. Not a problem functionally, but worth noting the redundancy.

README.md (1)

30-30: Minor: "full screen" as a compound adjective should be hyphenated.

Per static analysis hint and standard English grammar, "full-screen mode" is the correct compound adjective form.

-- **Full Screen Mode:** Pressing F11 enters full screen mode - Esc/F11 exits.
+- **Full-Screen Mode:** Pressing F11 enters full-screen mode — Esc/F11 exits.
faststack/repro_cache_lock.py (1)

1-47: Debug/repro script looks reasonable.

The approach of using a separate thread to verify lock state during eviction is sound. Two minor observations:

  1. The on_evict_callback parameters key and value are required by the callback contract but unused — consider prefixing with _ to silence the Ruff ARG001 warnings.
  2. There's a potential race: the check_lock thread could run after the eviction callback's caller has already released the lock, producing a false negative. The t.join() mitigates this, but if eviction is very fast, timing could still vary.
Silence unused arg warnings
-    def on_evict_callback(key, value):
+    def on_evict_callback(_key, _value):
faststack/util/executors.py (1)

60-72: Return type annotation could be more specific.

The factory function returns DaemonThreadPoolExecutor but the return type is annotated as ThreadPoolExecutor. This is fine for polymorphism but could be tightened to match the docstring on Line 65.

Proposed fix
 def create_daemon_threadpool_executor(
     max_workers: int, thread_name_prefix: str = ""
-) -> ThreadPoolExecutor:
+) -> DaemonThreadPoolExecutor:
faststack/imaging/cache.py (1)

217-276: evict_paths does not invoke on_evict callbacks for removed entries.

super().pop(k, None) at Line 265 bypasses the overridden popitem() and its callback logic. If consumers rely on on_evict for cleanup (e.g., releasing GPU textures), evicted entries from evict_paths will be missed. If this is intentional (tombstoned paths don't need eviction signaling), a brief comment would clarify.

faststack/verify_cache_fix.py (1)

1-10: Imports use bare module names without the faststack package prefix.

Lines 8–9 import from imaging.cache and models instead of faststack.imaging.cache and faststack.models. This relies on the fragile sys.path.append(os.getcwd()) on Line 6 and will fail if run from a different working directory.

Proposed fix
-# Add current dir to path
-sys.path.append(os.getcwd())
-
-from imaging.cache import ByteLRUCache
-from models import DecodedImage
+from faststack.imaging.cache import ByteLRUCache
+from faststack.models import DecodedImage
faststack/imaging/editor.py (3)

1954-1957: Redundant exception handler — RuntimeError is immediately re-raised.

The try/except RuntimeError: raise block is a no-op; removing it lets the exception propagate naturally.

Proposed fix
-        try:
-            self._ensure_float_image()
-        except RuntimeError:
-            raise
+        self._ensure_float_image()

2131-2141: save_image_uint8_levels reads self.current_edits without lock, unlike save_image.

Line 2141 accesses self.current_edits directly without holding self._lock, while save_image (Line 1984) snapshots edits under the lock. Since this fast-path only reads primitive float values and any mid-read race would safely fall through to return None, this is low-risk but inconsistent.


1610-1656: New srgb_u8 alias parameter is unused at the call site.

The srgb_u8 keyword-only alias is declared (Line 1617) and resolved into effective_srgb_u8 (Line 1644), but the only call site in _apply_edits (Lines 1030–1037) still passes srgb_u8_stride=srgb_u8_stride only. If this is intentional prep for a future caller, consider adding a brief comment or TODO.

faststack/imaging/prefetch.py (1)

456-464: Silent except Exception: pass swallows all errors during EXIF read.

Static analysis flags this at lines 461-462. While silently falling back to orientation=1 may be intentional for robustness, logging at debug level would aid troubleshooting without adding noise.

Proposed fix
                                        except Exception:
-                                            pass
+                                            log.debug("Failed to read EXIF from mmap for %s", target_path, exc_info=True)

Same applies to the analogous block at lines 531-532.

faststack/tests/test_thumbnail_ready_emits_datachanged.py (1)

12-18: Session-scoped qapp fixture with [] instead of sys.argv.

The sibling fixture in test_thumbnail_ready.py uses QCoreApplication(sys.argv), while this one uses QCoreApplication([]). Both work, but using sys.argv is more conventional for Qt apps that may need to parse args. Minor inconsistency.

faststack/qml/ThumbnailTile.qml (2)

253-272: Variant badges look correct; consider consistent formatting with existing badges.

The new variant badge rows work correctly. However, the inline multi-property style (width: 18; height: 18; radius: 3; ...) differs from the multi-line format used by the existing flag badges (lines 157-250). Consider aligning the formatting for consistency, though this is purely stylistic.


502-505: Debug logging gated properly, but uiState reference is not null-safe in all contexts.

If ThumbnailTile is ever instantiated in a context where uiState is not injected (e.g., a test harness or different QML loader), the uiState && uiState.debugThumbTiming check guards against undefined, but QML may still emit a warning for an unresolved reference. This is minor since it only fires for index 0 with debug enabled.

faststack/tests/test_handle_failures_isolated.py (2)

25-28: Class-body import of AppController is fragile.

Importing AppController at class-body scope (line 27) triggers the full import of faststack.app at module load time during class definition. If faststack.app has heavy side effects or circular imports, this will break. Consider deferring the binding to setUp or a fixture.


8-9: Module-level MagicMock declarations appear to be dead code.

confirm_permanent_delete and confirm_batch_permanent_delete at lines 8-9 are never referenced—the @patch decorators on the test function replace the real functions in faststack.app directly. These can be removed.

Proposed fix
-# Mocks for global functions that might be called
-confirm_permanent_delete = MagicMock(return_value=True)
-confirm_batch_permanent_delete = MagicMock(return_value=True)
-
faststack/tests/test_helicon_cleanup.py (1)

9-33: Patches are not active when the fixture's return value is used by the test.

The with patch(...) context manager exits at the end of the with block (line 32), but return controller is on line 33, outside the block. This means the module-level patches are restored before the test body runs. It works here because __init__ already bound mock objects to the controller's instance attributes, and the test separately patches launch_helicon_focus. However, this is fragile — if any code path in the test triggers a fresh lookup on the faststack.app module (e.g., a lazy import or factory call), it will get the real class instead of the mock.

Consider yielding the controller from inside the with block to keep patches active during the test:

Suggested fix
 `@pytest.fixture`
 def mock_controller():
     # Mock dependencies required by AppController init
     engine = MagicMock()
     with patch("faststack.app.Watcher"), \
          patch("faststack.app.SidecarManager"), \
          patch("faststack.app.ImageEditor"), \
          patch("faststack.app.ByteLRUCache"), \
          patch("faststack.app.Prefetcher"), \
          patch("faststack.app.ThumbnailCache"), \
          patch("faststack.app.PathResolver"), \
          patch("faststack.app.ThumbnailPrefetcher"), \
          patch("faststack.app.ThumbnailModel"), \
          patch("faststack.app.ThumbnailProvider"), \
          patch("faststack.app.concurrent.futures.ThreadPoolExecutor"), \
          patch("faststack.app.QTimer"), \
          patch("faststack.app.QApplication"):  # Mock QApplication to avoid segfaults
 
         controller = AppController(image_dir=Path("c:/images"), engine=engine)
         
         # Setup dummy images for the controller
         img1 = ImageFile(Path("c:/images/img1.jpg"))
         controller.image_files = [img1]
     
-    return controller
+        yield controller
faststack/tests/test_variants.py (1)

74-85: Address static analysis warnings for unused unpacked variables.

Multiple test methods unpack variables that are never used (e.g., vmap, backup, dev, images). Prefixing with _ silences the linter and communicates intent. Also, list-comprehension-then-[0] on lines 302 and 314 can be replaced with next(...) for clarity.

Examples
 # Lines 74-78 — unused `backup`
-    key, dev, backup = parse_variant_stem("photo-undeveloped")
+    key, dev, _backup = parse_variant_stem("photo-undeveloped")

 # Lines 93-96 — unused `dev`
-    key, dev, backup = parse_variant_stem("photo-BACKUP3")
+    key, _dev, backup = parse_variant_stem("photo-BACKUP3")

 # Lines 262, 274, 286, 301, 313 — unused `vmap` / `images`
-    images, vmap = find_images_with_variants(tmp_path)
+    images, _vmap = find_images_with_variants(tmp_path)
 # (or `_images, vmap = ...` where images is unused)

 # Lines 302, 314 — prefer next()
-    photo = [img for img in images if img.path.name == "photo.jpg"][0]
+    photo = next(img for img in images if img.path.name == "photo.jpg")

Also applies to: 93-96, 262-266, 274-276, 286-291, 301-305, 313-317, 326-329

faststack/tests/test_startup_opt.py (1)

29-41: Potential duplication with test_startup_optimization.py.

test_startup_only_one_scan tests the same load-time invariants (variant scan count, grid refreshes, refresh_from_controller called once) as test_startup_optimization in the other file. Consider consolidating to avoid divergent expectations.

faststack/tests/test_startup_optimization.py (2)

12-16: Unused variable app and unused import os.

Line 15: app = QApplication([]) — the variable is never read; the instance is available via QApplication.instance(). Line 2: os is imported but never used.

Suggested fix
-import sys
-import os
+import sys
 from pathlib import Path
 `@pytest.fixture`(scope="session", autouse=True)
 def qapplication():
     if not QApplication.instance():
-        app = QApplication(sys.argv)
+        QApplication(sys.argv)
     yield QApplication.instance()

18-66: Duplicate coverage with test_startup_opt.py::test_startup_only_one_scan.

This test validates the same invariants (variant scan count == 1, grid refreshes == 1, refresh_from_controller called once, refresh not called) as the test in test_startup_opt.py. They use different mocking strategies (decorator-based patches here vs. fixture with context-manager patches there), which can lead to maintenance burden and divergent expectations over time. Consider keeping one canonical test file for startup optimization.

faststack/thumbnail_view/provider.py (2)

144-251: Significant code duplication between timer and non-timer paths in requestPixmap.

The ID parsing logic (split parts, length check, folder check, parse thumb_size/path_hash/mtime_ns) appears twice: once inside the timer branch (lines 163-178) and again in the non-timer fallback (lines 222-242). Additionally, path is resolved at line 182 (timer path) and then unconditionally resolved again at line 245, overwriting the previous result.

Consider extracting the common parsing into a small helper to reduce duplication and the risk of the two paths drifting out of sync.


190-192: Unconditional time.perf_counter() calls on the hot path.

t_cache_get_start and dt_cache_get are computed on every request, but they're only consumed when timer is set (lines 195-197, 214-216). On the non-debug fast path, this is wasted work. While perf_counter is lightweight, this method is called for every visible thumbnail.

Suggested fix
         # Check cache (O(1) lookup)
-        t_cache_get_start = time.perf_counter()
+        t_cache_get_start = time.perf_counter() if timer else 0
         cached_bytes = self._cache.get(cache_key)
-        dt_cache_get = (time.perf_counter() - t_cache_get_start) * 1000
+        dt_cache_get = (time.perf_counter() - t_cache_get_start) * 1000 if timer else 0
faststack/tests/test_feedback_fixes.py (2)

47-52: Remove unused tmp_path parameter.

tmp_path is injected by pytest but never used. This was flagged by Ruff (ARG001).

Suggested fix
-def test_image_editor_save_exception(tmp_path):
+def test_image_editor_save_exception():

73-93: Test files created with write_text("dummy") instead of valid JPEG bytes.

Other tests in this PR (e.g., test_variants.py) use write_bytes(b"\xff\xd8\xff\xe0") to create minimal JPEG files. Here, write_text("dummy") produces non-JPEG content. If find_images_with_variants ever adds JPEG header validation, this test will silently break. Consider using consistent file creation.

Suggested fix
-        orphan = tdp / "orphan-developed.jpg"
-        orphan.write_text("dummy")
+        orphan = tdp / "orphan-developed.jpg"
+        orphan.write_bytes(b"\xff\xd8\xff\xe0")
         
         # Also create a regular pair for comparison
         pair_main = tdp / "pair.jpg"
         pair_dev = tdp / "pair-developed.jpg"
-        pair_main.write_text("dummy")
-        pair_dev.write_text("dummy")
+        pair_main.write_bytes(b"\xff\xd8\xff\xe0")
+        pair_dev.write_bytes(b"\xff\xd8\xff\xe0")
faststack/util/thumb_debug.py (1)

192-202: Use is not None instead of truthiness checks for perf_counter timestamps.

self.t_queued, self.t_worker_start, and self.t_done are Optional[float] timestamps from time.perf_counter(). Checking if self.t_queued: would incorrectly skip the block if the value happened to be 0.0. While extremely unlikely with perf_counter, using is not None is the correct and idiomatic check for optionals.

Suggested fix
-        if self.t_queued:
+        if self.t_queued is not None:
             sched_ms = (self.t_queued - self.t_requested) * 1000
             parts.append(f"sched_ms={sched_ms:.1f}")
             
-            if self.t_worker_start:
+            if self.t_worker_start is not None:
                 wait_ms = (self.t_worker_start - self.t_queued) * 1000
                 parts.append(f"wait_ms={wait_ms:.1f}")
                 
-                if self.t_done:
+                if self.t_done is not None:
                     worker_ms = (self.t_done - self.t_worker_start) * 1000
                     parts.append(f"worker_ms={worker_ms:.1f}")
faststack/qml/Main.qml (1)

815-827: Minor redundancy: Escape is handled by both the Shortcut and Keys.onPressed.

The Shortcut at line 822 (with Qt.ApplicationShortcut context) handles Escape when fullScreenLoupe is true. The Keys.onPressed handler at lines 916–920 also checks fullScreenLoupe and handles Escape. In practice the Shortcut fires first and sets fullScreenLoupe = false, so the Keys handler becomes a no-op. Not a bug, just redundant defense-in-depth.

faststack/io/variants.py (3)

57-64: Redundant regex search — reuse the first match object.

_DEVELOPED_TOKEN_RE.search(stem) is called twice: once on line 58 to check presence, and again on line 63 to get the match. Save the first result and reuse it.

♻️ Proposed fix
-    # 1. Check for -developed token
-    is_developed = bool(_DEVELOPED_TOKEN_RE.search(stem))
-
-    # 2. Remove exactly one -developed token (first occurrence) -> stripped
-    if is_developed:
-        # Find the match and remove it, handling leading/trailing dashes
-        m = _DEVELOPED_TOKEN_RE.search(stem)
+    # 1. Check for -developed token
+    m = _DEVELOPED_TOKEN_RE.search(stem)
+    is_developed = m is not None
+
+    # 2. Remove exactly one -developed token (first occurrence) -> stripped
+    if is_developed:

236-238: Duplicate norm_path — identical implementation exists in faststack/debug_path_norm.py.

The relevant snippet shows faststack/debug_path_norm.py defines the same norm_path(p: Path) -> str with the same body. Consider extracting into a shared utility to avoid drift.

#!/bin/bash
# Check all definitions of norm_path across the codebase
rg -n "def norm_path" --type=py -C2

104-117: Paths are normalized at insertion but group_key uses the first-seen casing.

Line 116 sets group_key=group_key from whichever file is encountered first. If files have different casings of the same stem (e.g., Foo-developed.jpg vs foo.jpg), the group's display group_key depends on iteration order (which is os.scandir order, i.e., filesystem-dependent). This is cosmetic only since lookups use casefold(), but worth noting.

faststack/thumbnail_view/prefetcher.py (4)

262-265: Possible AttributeError if timer is None.

The _decode_worker type hint says timer: "thumb_debug.ThumbTimer" (non-Optional), but line 263 accesses timer.cancelled without a guard, while other call sites (e.g., line 389) defensively check timer and timer.cancelled. In practice, submit() always passes a non-None timer, so this is safe today. Just ensure the invariant holds if the method is ever called differently.


278-286: Move import io to the top of the file.

import io on line 282 is inside the hot decode path. While Python caches imports (making repeated imports cheap), placing it at the module level is the standard convention and avoids the per-call sys.modules lookup.

♻️ Proposed fix

Add at the top of the file alongside other stdlib imports:

import io

Then remove line 282.


309-340: timer.stage("decode") is entered twice for the TurboJPEG path.

When TurboJPEG succeeds but further PIL resizing is needed (line 335), a second timer.stage("decode") context is entered. Depending on how ThumbTimer.stage accumulates or overwrites timings, this may produce misleading metrics (overwriting the TurboJPEG decode duration or double-counting). Consider using a distinct stage name like "resize" for the PIL thumbnail step.

♻️ Proposed fix
                if w > target_size or h > target_size:
-                    with timer.stage("decode"):
+                    with timer.stage("resize"):
                         pil_img = Image.fromarray(rgb)

192-193: "inflight" and "qdepth" gauges are always set to the same value.

Both gauges at lines 192–193 (and again at 372–373, 426–427) track len(self._inflight). If they're meant to represent different concepts (e.g., in-flight = actively executing, qdepth = waiting in queue), they should diverge. Otherwise, one of them is redundant and adds noise to your metrics.

faststack/io/indexer.py (1)

213-227: Conditional logging: log.info inside log.isEnabledFor(logging.DEBUG) block.

The detailed elapsed-time log (line 214) is gated behind logging.DEBUG, but it logs at INFO level. This means the elapsed time is only visible when the logger is set to DEBUG, even though the message severity is INFO. If the intent is to always log elapsed time at INFO level, remove the isEnabledFor guard. If the intent is verbose-only output, consider logging at DEBUG level instead.

faststack/qml/ThumbnailGridView.qml (1)

157-166: onCountChanged restarts prefetchTimer regardless of prefetchEnabled.

When prefetchEnabled is false, triggerPrefetch() bails early (line 116), so this is harmless. But it fires a 50ms timer for no reason. Consider guarding this restart:

♻️ Proposed fix
         onCountChanged: {
             if (count <= 0) {
                 currentIndex = 0
                 return
             }
             if (currentIndex >= count) {
                 currentIndex = count - 1
             }
-            prefetchTimer.restart()
+            if (prefetchEnabled) prefetchTimer.restart()
         }
faststack/thumbnail_view/model.py (1)

206-209: One-shot reason is consumed on the first ThumbnailSourceRole access per batch.

When QML iterates the model after a refresh/filter, only the first entry's thumbnail URL will carry the actual reason (e.g., "filter", "jump"); all subsequent entries in the same pass will fall back to "scroll". If this is intentional for lightweight debugging, it's fine. If per-entry reason tracking matters, the reset should happen at the call site (e.g., after endResetModel).

Comment thread faststack/app.py
Comment on lines +4738 to +4763
def _on_cache_evict(self, key, value):
"""Callback for when the image cache evicts an item."""
now = time.time()

# 1. Record eviction timestamp
self._eviction_timestamps.append(now)

# 2. Prune timestamps older than window
# Keep list short
cutoff = now - CACHE_THRASH_WINDOW_SECS
self._eviction_timestamps = [t for t in self._eviction_timestamps if t > cutoff]

# 3. Check for thrashing (e.g., > threshold evictions in window)
if len(self._eviction_timestamps) > CACHE_THRASH_THRESHOLD:
# 4. Rate limit the warning
if now - self._last_cache_warning_time > CACHE_WARNING_COOLDOWN_SECS:
self._last_cache_warning_time = now
self._has_warned_cache_full = True

# Format usage info
used_gb = self.image_cache.currsize / (1024**3)
max_gb = self.image_cache.max_bytes / (1024**3)

msg = f"Cache thrashing! {len(self._eviction_timestamps)} evictions in {CACHE_THRASH_WINDOW_SECS}s. Usage: {used_gb:.1f}GB / {max_gb:.1f}GB."

# Use QTimer.singleShot to ensure this runs on the main thread
QTimer.singleShot(0, lambda: self.update_status_message(msg))
log.warning(msg)
with self._eviction_lock:
# 1. Record eviction timestamp / prune
self._eviction_timestamps.append(now)
cutoff = now - CACHE_THRASH_WINDOW_SECS
self._eviction_timestamps = [t for t in self._eviction_timestamps if t > cutoff]

# 2. Check for thrashing (e.g., > threshold evictions in window)
if len(self._eviction_timestamps) > CACHE_THRASH_THRESHOLD:
# 3. Rate limit the warning
if now - self._last_cache_warning_time > CACHE_WARNING_COOLDOWN_SECS:
self._last_cache_warning_time = now
self._has_warned_cache_full = True

# UI update logic
used_gb = self.image_cache.currsize / (1024**3)
max_gb = self.image_cache.max_bytes / (1024**3)
msg = f"Cache thrashing! {len(self._eviction_timestamps)} evictions in {CACHE_THRASH_WINDOW_SECS}s. Usage: {used_gb:.1f}GB / {max_gb:.1f}GB."

# Schedule UI work safely on main thread
# QTimer.singleShot(0, ...) is thread-safe entry to main loop
QTimer.singleShot(0, lambda: self.update_status_message(msg))
log.warning(msg)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Lint: unused key/value in cache eviction callback.
Ruff flags these as unused; please either use them (e.g., in log context) or rename to _key, _value.

✅ Simple lint fix
-    def _on_cache_evict(self, key, value):
+    def _on_cache_evict(self, _key, _value):
📝 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
def _on_cache_evict(self, key, value):
"""Callback for when the image cache evicts an item."""
now = time.time()
# 1. Record eviction timestamp
self._eviction_timestamps.append(now)
# 2. Prune timestamps older than window
# Keep list short
cutoff = now - CACHE_THRASH_WINDOW_SECS
self._eviction_timestamps = [t for t in self._eviction_timestamps if t > cutoff]
# 3. Check for thrashing (e.g., > threshold evictions in window)
if len(self._eviction_timestamps) > CACHE_THRASH_THRESHOLD:
# 4. Rate limit the warning
if now - self._last_cache_warning_time > CACHE_WARNING_COOLDOWN_SECS:
self._last_cache_warning_time = now
self._has_warned_cache_full = True
# Format usage info
used_gb = self.image_cache.currsize / (1024**3)
max_gb = self.image_cache.max_bytes / (1024**3)
msg = f"Cache thrashing! {len(self._eviction_timestamps)} evictions in {CACHE_THRASH_WINDOW_SECS}s. Usage: {used_gb:.1f}GB / {max_gb:.1f}GB."
# Use QTimer.singleShot to ensure this runs on the main thread
QTimer.singleShot(0, lambda: self.update_status_message(msg))
log.warning(msg)
with self._eviction_lock:
# 1. Record eviction timestamp / prune
self._eviction_timestamps.append(now)
cutoff = now - CACHE_THRASH_WINDOW_SECS
self._eviction_timestamps = [t for t in self._eviction_timestamps if t > cutoff]
# 2. Check for thrashing (e.g., > threshold evictions in window)
if len(self._eviction_timestamps) > CACHE_THRASH_THRESHOLD:
# 3. Rate limit the warning
if now - self._last_cache_warning_time > CACHE_WARNING_COOLDOWN_SECS:
self._last_cache_warning_time = now
self._has_warned_cache_full = True
# UI update logic
used_gb = self.image_cache.currsize / (1024**3)
max_gb = self.image_cache.max_bytes / (1024**3)
msg = f"Cache thrashing! {len(self._eviction_timestamps)} evictions in {CACHE_THRASH_WINDOW_SECS}s. Usage: {used_gb:.1f}GB / {max_gb:.1f}GB."
# Schedule UI work safely on main thread
# QTimer.singleShot(0, ...) is thread-safe entry to main loop
QTimer.singleShot(0, lambda: self.update_status_message(msg))
log.warning(msg)
def _on_cache_evict(self, _key, _value):
"""Callback for when the image cache evicts an item."""
now = time.time()
with self._eviction_lock:
# 1. Record eviction timestamp / prune
self._eviction_timestamps.append(now)
cutoff = now - CACHE_THRASH_WINDOW_SECS
self._eviction_timestamps = [t for t in self._eviction_timestamps if t > cutoff]
# 2. Check for thrashing (e.g., > threshold evictions in window)
if len(self._eviction_timestamps) > CACHE_THRASH_THRESHOLD:
# 3. Rate limit the warning
if now - self._last_cache_warning_time > CACHE_WARNING_COOLDOWN_SECS:
self._last_cache_warning_time = now
self._has_warned_cache_full = True
# UI update logic
used_gb = self.image_cache.currsize / (1024**3)
max_gb = self.image_cache.max_bytes / (1024**3)
msg = f"Cache thrashing! {len(self._eviction_timestamps)} evictions in {CACHE_THRASH_WINDOW_SECS}s. Usage: {used_gb:.1f}GB / {max_gb:.1f}GB."
# Schedule UI work safely on main thread
# QTimer.singleShot(0, ...) is thread-safe entry to main loop
QTimer.singleShot(0, lambda: self.update_status_message(msg))
log.warning(msg)
🧰 Tools
🪛 Ruff (0.15.0)

[warning] 4738-4738: Unused method argument: key

(ARG002)


[warning] 4738-4738: Unused method argument: value

(ARG002)

🤖 Prompt for AI Agents
In `@faststack/app.py` around lines 4738 - 4763, The _on_cache_evict callback
currently declares parameters key and value but doesn't use them, triggering a
lint error; fix by either renaming the parameters to _key and _value in the
_on_cache_evict signature to mark them intentionally unused, or incorporate them
into the warning/log and UI update (e.g., include key or a short value summary
in the msg passed to self.update_status_message and log.warning); adjust
references inside _on_cache_evict (and the lambda to self.update_status_message)
accordingly so the chosen approach is consistent.

Comment thread faststack/app.py
Comment on lines +6908 to 6911
global _debug_mode, _debug_thumb_timing
_debug_mode = debug
_debug_thumb_timing = debug_thumb_timing

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

_debug_thumb_trace global is never updated.
If any code relies on that global, it will stay False even when --debug-thumbtrace is passed. Consider assigning it alongside _debug_thumb_timing.

🔧 Proposed fix
-    global _debug_mode, _debug_thumb_timing
+    global _debug_mode, _debug_thumb_timing, _debug_thumb_trace
@@
     _debug_mode = debug
     _debug_thumb_timing = debug_thumb_timing
+    _debug_thumb_trace = debug_thumb_trace
📝 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
global _debug_mode, _debug_thumb_timing
_debug_mode = debug
_debug_thumb_timing = debug_thumb_timing
global _debug_mode, _debug_thumb_timing, _debug_thumb_trace
_debug_mode = debug
_debug_thumb_timing = debug_thumb_timing
_debug_thumb_trace = debug_thumb_trace
🤖 Prompt for AI Agents
In `@faststack/app.py` around lines 6908 - 6911, The global _debug_thumb_trace is
declared but never updated, so when the CLI flag --debug-thumbtrace is used the
module-level _debug_thumb_trace remains False; update the same block that sets
_debug_mode and _debug_thumb_timing to also set _debug_thumb_trace =
debug_thumb_trace (declared as global at top of that block) so the flag value is
propagated to the module state; ensure you reference the existing globals
_debug_mode, _debug_thumb_timing, and _debug_thumb_trace and use the incoming
parameter name debug_thumb_trace.

Comment thread faststack/check_daemon.py
Comment on lines +14 to +15
with ThreadPoolExecutor(max_workers=1, initializer=set_daemon) as executor:
print(f"Result: {executor.submit(check_daemon).result()}")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Module-level side effect: executor runs on import.

Lines 14–15 execute immediately when this module is imported (not just when run as a script). If anything in the package accidentally imports check_daemon, a ThreadPoolExecutor will spin up as a side effect.

Wrap in a __main__ guard like the sibling script repro_cache_lock.py does:

Proposed fix
-with ThreadPoolExecutor(max_workers=1, initializer=set_daemon) as executor:
-    print(f"Result: {executor.submit(check_daemon).result()}")
+if __name__ == "__main__":
+    with ThreadPoolExecutor(max_workers=1, initializer=set_daemon) as executor:
+        print(f"Result: {executor.submit(check_daemon).result()}")
📝 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
with ThreadPoolExecutor(max_workers=1, initializer=set_daemon) as executor:
print(f"Result: {executor.submit(check_daemon).result()}")
if __name__ == "__main__":
with ThreadPoolExecutor(max_workers=1, initializer=set_daemon) as executor:
print(f"Result: {executor.submit(check_daemon).result()}")
🤖 Prompt for AI Agents
In `@faststack/check_daemon.py` around lines 14 - 15, The module currently creates
a ThreadPoolExecutor and submits check_daemon at import time (using
ThreadPoolExecutor, set_daemon, and check_daemon); move that runtime behavior
into a script entry point by wrapping the executor block in an if __name__ ==
"__main__": guard so the ThreadPoolExecutor(max_workers=1,
initializer=set_daemon) creation and executor.submit(check_daemon).result() call
only run when the file is executed as a script, preserving the same print
behavior but preventing side effects on import.

Comment on lines +1 to +22

import os
from pathlib import Path

def norm_path(p: Path) -> str:
return os.path.normcase(os.path.abspath(str(p)))

p1 = Path("C:/Test/File.JPG")
p2 = Path("C:/Test/file.jpg")

print(f"p1: {p1}")
print(f"p2: {p2}")
print(f"p1 == p2: {p1 == p2}")

n1 = Path(norm_path(p1))
n2 = Path(norm_path(p2))

print(f"n1: {n1}")
print(f"n2: {n2}")
print(f"n1 == n2: {n1 == n2}")
print(f"n1 == p1: {n1 == p1}")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Debug script committed by accident — should be removed.

This file duplicates norm_path from faststack/io/variants.py and contains module-level print statements with hardcoded test paths that execute on import. It appears to be a local debugging artifact and shouldn't ship in a release PR.

🤖 Prompt for AI Agents
In `@faststack/debug_path_norm.py` around lines 1 - 22, This debug script contains
a duplicate norm_path function and module-level print statements that run on
import; remove this file (faststack/debug_path_norm.py) from the PR or at
minimum delete the debugging code: eliminate the duplicate norm_path definition
and all top-level print/test code so the module has no side effects on import;
if you want to keep the behavior, move the example into a proper unit test or a
__main__ guarded block (if truly needed) and ensure only the canonical norm_path
implementation remains in its original module (norm_path symbol in
faststack/io/variants.py).

Comment on lines +13 to +33
def get_decoded_image_size(item) -> int:
"""Calculates the size of a DecodedImage object."""
# In this simplified example, we only store the buffer.
# In the full app, this would also account for the QImage/QTexture.
from faststack.models import DecodedImage

if isinstance(item, DecodedImage):
# Handle both numpy arrays and memoryview buffers
if hasattr(item.buffer, "nbytes"):
return item.buffer.nbytes
elif isinstance(item.buffer, (bytes, bytearray)):
return len(item.buffer)
else:
# Fallback: estimate from dimensions (more accurate for image buffers than sys.getsizeof)
bytes_per_pixel = getattr(item, "channels", 4) # Default to RGBA
return item.width * item.height * bytes_per_pixel

log.warning(
f"Unexpected item type in cache: {type(item)}. Returning estimated size of 1."
)
return 1 # Should not happen
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

get_decoded_image_size: fallback channels attribute doesn't exist on DecodedImage.

Line 27: getattr(item, "channels", 4) will always return the default 4 because DecodedImage (in faststack/models.py, lines 87–97) has no channels attribute. This makes the fallback estimate always assume RGBA, which may over-count for RGB images. Consider deriving channels from bytes_per_line / width if available.

Proposed fix
-            bytes_per_pixel = getattr(item, "channels", 4)  # Default to RGBA
+            bytes_per_pixel = item.bytes_per_line // item.width if item.width > 0 else 4
🤖 Prompt for AI Agents
In `@faststack/imaging/cache.py` around lines 13 - 33, The fallback in
get_decoded_image_size incorrectly assumes 4 channels because DecodedImage has
no channels attribute; update the fallback to compute bytes_per_pixel from
available image metadata: if item has bytes_per_line (or a similar attribute)
and width, derive bytes_per_pixel = bytes_per_line / width and use that (rounded
or cast to int) as channels/bytes-per-pixel, otherwise fall back to 4; adjust
the branch inside get_decoded_image_size that currently uses getattr(item,
"channels", 4) to first check for item.bytes_per_line and item.width and compute
channels from that before defaulting.


# Default mock behavior
self.model.rowCount.return_value = 5000
self.model.thumbnail_size = (256, 256)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

thumbnail_size should be int, not a tuple.

The real ThumbnailModel.thumbnail_size property returns an int (see faststack/thumbnail_view/model.py lines 161-163). Setting it to (256, 256) here won't break the test (since prefetcher.submit is mocked), but it masks any code that expects an int.

Proposed fix
-        self.model.thumbnail_size = (256, 256)
+        self.model.thumbnail_size = 256
📝 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
self.model.thumbnail_size = (256, 256)
self.model.thumbnail_size = 256
🤖 Prompt for AI Agents
In `@faststack/tests/test_ui_prefetch_safety.py` at line 29, The test assigns
self.model.thumbnail_size = (256, 256) but ThumbnailModel.thumbnail_size is an
int; change the assignment in the test to an integer (e.g., 256) so the test
matches the real property type and surfaces any type-dependent logic (refer to
ThumbnailModel.thumbnail_size and to the test's use of prefetcher.submit which
is mocked).

def test_main_selection_priority(self):
"""Verify Main selection priority: Non-dev > Dev > Backup."""

# Cas 1: Pure Main exists
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Typo in comment: "Cas 1" → "Case 1".

Proposed fix
-        # Cas 1: Pure Main exists
+        # Case 1: Pure Main exists
📝 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
# Cas 1: Pure Main exists
# Case 1: Pure Main exists
🤖 Prompt for AI Agents
In `@faststack/tests/test_variants_logic.py` at line 10, The comment "Cas 1: Pure
Main exists" contains a typo; update that comment in
faststack/tests/test_variants_logic.py to read "Case 1: Pure Main exists"
(replace "Cas 1" with "Case 1") so the test comment is spelled correctly.

Comment on lines +147 to +156

# One-shot reason for logging
self._next_source_reason: Optional[str] = None

# Mapping from thumbnail_id (without query params) to row index
# id format: "{size}/{path_hash}/{mtime_ns}"
self._id_to_row: Dict[str, int] = {}

# One-shot reason for logging
self._next_source_reason: Optional[str] = None
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Duplicate initialization of _next_source_reason.

_next_source_reason is initialized twice — at line 149 and again at line 156. This appears to be an accidental duplication from a merge or edit. The second assignment silently overwrites the first.

Remove the duplicate block
         self._active_filter_flags: list = []  # current flag filters (e.g. ["uploaded", "stacked"])
-        
-        # One-shot reason for logging
-        self._next_source_reason: Optional[str] = None

         # Mapping from thumbnail_id (without query params) to row index
         # id format: "{size}/{path_hash}/{mtime_ns}"
         self._id_to_row: Dict[str, int] = {}
         
         # One-shot reason for logging
         self._next_source_reason: Optional[str] = None
🤖 Prompt for AI Agents
In `@faststack/thumbnail_view/model.py` around lines 147 - 156, Remove the
duplicate initialization of the instance variable _next_source_reason: locate
the duplicate assignments to _next_source_reason (both set to Optional[str] =
None) in the class initializer and delete the redundant second one so the
variable is only initialized once; ensure _id_to_row and surrounding comments
remain unchanged and run tests/lint to confirm no other references relied on the
duplicate.

Comment thread faststack/ui/provider.py
Comment on lines +559 to +562
@Property(str, notify=variantSaveHintChanged)
def variantSaveHint(self):
"""Returns a hint message when saving from a variant."""
return self.app_controller.get_variant_save_hint()
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

variantSaveHint lacks a hasattr guard, unlike sibling variant properties.

variantBadges (line 1331) guards with hasattr(self.app_controller, "get_variant_badges"), but variantSaveHint calls self.app_controller.get_variant_save_hint() unconditionally. This will raise AttributeError if the method is absent.

Proposed fix
     `@Property`(str, notify=variantSaveHintChanged)
     def variantSaveHint(self):
         """Returns a hint message when saving from a variant."""
-        return self.app_controller.get_variant_save_hint()
+        if hasattr(self.app_controller, "get_variant_save_hint"):
+            return self.app_controller.get_variant_save_hint()
+        return ""
🤖 Prompt for AI Agents
In `@faststack/ui/provider.py` around lines 559 - 562, The variantSaveHint
property calls self.app_controller.get_variant_save_hint() unguarded and can
raise AttributeError; update the variantSaveHint getter to check
hasattr(self.app_controller, "get_variant_save_hint") and return a safe default
(e.g., "" or None) when missing, mirroring the pattern used by variantBadges;
ensure you keep the `@Property` decorator and notify=variantSaveHintChanged and
only call get_variant_save_hint() when the attribute exists.

Comment thread README.md
- **Crop:** Added the ability to crop and rotate images via the cr(O)p hotkey (or right mouse click). It can be a freeform crop, or constrained to several popular aspect ratios.
- **Zoom & Pan:** Smooth zooming and panning.
- **Stack Selection:** Group images into stacks (`[`, `]`) and select them for processing (`S`).
- **Speak Line**: In grid view, a spark line is visible on each folder, so you can see how far you have gotten in uploading photos in each directory.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Typo: "Speak Line" should be "Spark Line".

The AI summary and PR description reference a "spark line" progress visualization, not "speak line."

-- **Speak Line**: In grid view, a spark line is visible on each folder, so you can see how far you have gotten in uploading photos in each directory.
+- **Spark Line**: In grid view, a spark line is visible on each folder, so you can see how far you have gotten in uploading photos in each directory.
📝 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
- **Speak Line**: In grid view, a spark line is visible on each folder, so you can see how far you have gotten in uploading photos in each directory.
- **Spark Line**: In grid view, a spark line is visible on each folder, so you can see how far you have gotten in uploading photos in each directory.
🤖 Prompt for AI Agents
In `@README.md` at line 15, Replace the incorrect phrase "Speak Line" with "Spark
Line" wherever it appears (starting with the README entry that currently reads
"**Speak Line**: In grid view...") and also update any references in the PR
description or AI summary to use "Spark Line" so the terminology is consistent
across docs.

@AlanRockefeller AlanRockefeller merged commit e27f2b5 into main Feb 16, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant