Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added .codex
Empty file.
59 changes: 39 additions & 20 deletions faststack/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -2021,39 +2021,39 @@ def _on_save_finished(self, save_result: dict):
if save_result.get("started_from_restore_override"):
self._clear_variant_override()

# 2. Update variants and re-select index
# Refresh list to pick up new backup files and update variant map
self.refresh_image_list()
# Record current path to stay on it after refresh (since index may shift)
preserved_path = None
if 0 <= self.current_index < len(self.image_files):
preserved_path = self.image_files[self.current_index].path

# Find and re-select the saved image
new_index = self.current_index
# 1. Update sidecar metadata FIRST so all following refreshes see it
if saved_path:
self.sidecar.update_metadata(saved_path, {"edited": True})
Comment on lines +2030 to +2031
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Write edited metadata to the save-origin sidecar

_on_save_finished now unconditionally calls self.sidecar.update_metadata(saved_path, {"edited": True}), but save_edited_image explicitly allows users to keep navigating while the background save runs, and _switch_to_directory replaces self.sidecar when folders change. If a save started in folder A finishes after the user moved to folder B, this line writes an absolute-key metadata entry into folder B’s faststack.json instead of updating folder A, which corrupts sidecar data and leaves the actual saved image unmarked in its own folder.

Useful? React with 👍 / 👎.

Comment on lines +2029 to +2031
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

Persist the full edited metadata and make it undoable.

These writes only set edited=True. That leaves edited_date blank in the metadata UI, and the undoable flows here (crop, auto_levels, auto_white_balance) will still show the image as edited after the backup is restored because the previous metadata state is never captured/restored.

Also applies to: 7376-7377, 7621-7622, 7717-7718, 7866-7867

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@faststack/app.py` around lines 2029 - 2031, The metadata update only sets
{"edited": True} so edited_date is missing and no previous metadata is captured
for undo; change the flow in the code that updates sidecar metadata (the block
using self.sidecar.update_metadata(saved_path, {"edited": True})) to: 1) read
the current metadata via self.sidecar.get_metadata(saved_path) into old_meta, 2)
create new_meta by merging old_meta with edited=True and a timestamp
(edited_date = now()), 3) call self.sidecar.update_metadata(saved_path,
new_meta), and 4) record the old_meta as an undoable operation via the app's
undo/backup mechanism (e.g., self.undo_manager.record_metadata_change or the
same backup routine used for image data) so operations like crop, auto_levels,
and auto_white_balance can fully revert metadata; apply the same change at the
other same-pattern sites (the blocks used by crop, auto_levels,
auto_white_balance).


if saved_path:
target_key = self._key(saved_path)
for i, img in enumerate(self.image_files):
if self._key(img.path) == target_key:
new_index = i
break

self.current_index = new_index
# 2. Update variants and re-select index
self.refresh_image_list()

# Force UI Sync / Prefetch
if still_on_same_session:
# Still viewing the saved image — pin index and force sync
self._reindex_after_save(saved_path)
self.image_cache.clear()
self.prefetcher.cancel_all()
self.prefetcher.update_prefetch(self.current_index)
self.sync_ui_state()
else:
# User navigated away — clear stale cache entry
# User navigated away — re-find new index for preserved_path and drop stale cache
if preserved_path:
self._reindex_after_save(preserved_path)
if saved_path:
self.image_cache.pop_path(saved_path)

# Always emit badge update — backup file was created
if self.ui_state:
self.ui_state.variantBadgesChanged.emit()

self.update_status_message("Image saved")
else:
self.update_status_message("Failed to save image")
# Success reported but result shape unexpected
log.warning("Save finished with unexpected result shape: %r", result)
Comment on lines +2055 to +2056
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

Clear the stuck “Saving...” status on malformed success results.

This branch only logs, so the UI can remain on the no-timeout "Saving..." message forever when a save reports success with an unexpected payload.

Proposed fix
         else:
             # Success reported but result shape unexpected
             log.warning("Save finished with unexpected result shape: %r", result)
+            self.update_status_message(
+                "Save finished, but the result could not be processed.",
+                timeout=5000,
+            )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@faststack/app.py` around lines 2055 - 2056, The code currently only logs
unexpected save payloads at the log.warning("Save finished with unexpected
result shape: %r", result) branch, which can leave the UI stuck on the
"Saving..." indicator; update that branch to not only log but also invoke the
same completion/cleanup path used for normal successes (e.g., call the success
handler or emit the save-complete/notify-save-done routine that clears the UI
state and resets any in-progress flags), ensuring the client is notified that
the save finished even when the payload shape is malformed.


# --- Actions ---

Expand Down Expand Up @@ -7373,6 +7373,9 @@ def execute_crop(self):
("crop", (str(saved_path), str(backup_path)), timestamp)
)

# Mark as edited in sidecar
self.sidecar.update_metadata(saved_path, {"edited": True})

# Exit crop mode
self.ui_state.isCropping = False
self.ui_state.currentCropBox = (0, 0, 1000, 1000)
Expand Down Expand Up @@ -7615,6 +7618,12 @@ def quick_auto_levels(self):
("auto_levels", (saved_path, backup_path), timestamp)
)

# 1. Update sidecar metadata FIRST so all following refreshes see it
self.sidecar.update_metadata(saved_path, {"edited": True})

# 2. Update list and model to pick up changes
self.refresh_image_list()

# Force reload to ensure disk consistency
self.image_editor.clear()

Expand Down Expand Up @@ -7704,6 +7713,10 @@ def _apply_auto_levels_at_index(self, index: int) -> bool:
if save_result:
saved_path, backup_path = save_result
timestamp = time.time()

# Mark as edited in sidecar
self.sidecar.update_metadata(saved_path, {"edited": True})

self.undo_history.append(
("auto_levels", (saved_path, backup_path), timestamp)
)
Expand Down Expand Up @@ -7849,17 +7862,23 @@ def quick_auto_white_balance(self):

if save_result:
saved_path, backup_path = save_result
# Track this action for undo
timestamp = time.time()
# 1. Update sidecar metadata FIRST so all following refreshes see it
self.sidecar.update_metadata(saved_path, {"edited": True})

# 2. Update list and model to pick up changes
self.refresh_image_list()

# Re-derive current_index
self._reindex_after_save(saved_path)

self.undo_history.append(
("auto_white_balance", (saved_path, backup_path), timestamp)
)

# Force the image editor to clear its current state so it reloads fresh
self.image_editor.clear()

# Re-derive current_index (backup is excluded from visible list)
self._reindex_after_save(saved_path)
t_list = time.perf_counter()

# Invalidate cache for the edited image so it's reloaded from disk
Expand Down
15 changes: 15 additions & 0 deletions faststack/io/sidecar.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,3 +270,18 @@ def _stable_key_from_key(self, key: str, check_fs: bool = False) -> str:

def set_last_index(self, index: int):
self.data.last_index = index

def update_metadata(self, image_ref: Union[str, Path], updates: dict):
"""Update multiple metadata fields for an image and save if changed."""
meta = self.get_metadata(image_ref, create=True)
changed = False
for key, value in updates.items():
if hasattr(meta, key):
if getattr(meta, key) != value:
setattr(meta, key, value)
changed = True
else:
log.warning(f"Unknown metadata key: {key}")

if changed:
self.save()
Loading
Loading